Compare commits
61 Commits
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -1,4 +1,39 @@
|
|||
# Changelog
|
||||
## [0.16.0] - 2025-06-27
|
||||
### Added
|
||||
- Sistema de logs en tiempo real.
|
||||
|
||||
### Improved
|
||||
- Se ha mejorado el comportamiento de algunos filtros en la parte de trazas.
|
||||
|
||||
---
|
||||
## [0.15.0] - 2025-06-26
|
||||
### Added
|
||||
- Se ha añadido integracion con OgGit. Ahora se pueden crear y desplegar imagenes.
|
||||
- Ahora se pueden gestionar cola de acciones y vaciarlas tanto a nivel de aula como de cliente.
|
||||
- Nuevos componentes que ayudan a la mejora general de la UX
|
||||
- Mejora en el comportamiento de los asistentes.
|
||||
- Se puede cancelar tareas desde la parte de Trazas
|
||||
|
||||
### Changed
|
||||
- Se ha cambiado la vista de las imagenes de Git
|
||||
|
||||
|
||||
---
|
||||
## [0.14.1] - 2025-06-09
|
||||
### Fixed
|
||||
- Se han corregido los errores en produccion que hacia que no salieran mensajes desde la API correctamente.
|
||||
|
||||
---
|
||||
## [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.
|
||||
|
|
|
@ -63,8 +63,8 @@
|
|||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "7kb",
|
||||
"maximumError": "10kb"
|
||||
"maximumWarning": "35kb",
|
||||
"maximumError": "40kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
|
|
@ -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,51 +32,52 @@ 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';
|
||||
import { LogoutGuard } from './guards/logout.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'] } },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
component: AuthLayoutComponent,
|
||||
children: [
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'login', component: LoginComponent, canActivate: [LogoutGuard] },
|
||||
],
|
||||
},
|
||||
{ path: '**', component: PageNotFoundComponent },
|
||||
|
|
|
@ -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';
|
||||
|
@ -92,6 +91,7 @@ import { MatSliderModule } from '@angular/material/slider';
|
|||
import { ImagesComponent } from './components/images/images.component';
|
||||
import { CreateImageComponent } from './components/images/create-image/create-image.component';
|
||||
import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component';
|
||||
import { CreateRepositoryModalComponent } from './components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component';
|
||||
import { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component';
|
||||
import { SoftwareComponent } from './components/software/software.component';
|
||||
import { CreateSoftwareComponent } from './components/software/create-software/create-software.component';
|
||||
|
@ -140,7 +140,7 @@ import {
|
|||
SaveScriptComponent
|
||||
} from "./components/groups/components/client-main-view/run-script-assistant/save-script/save-script.component";
|
||||
import { EditImageComponent } from './components/repositories/edit-image/edit-image.component';
|
||||
import { ShowGitImagesComponent } from './components/repositories/show-git-images/show-git-images.component';
|
||||
import { ShowGitCommitsComponent } from './components/repositories/show-git-images/show-git-images.component';
|
||||
import { RenameImageComponent } from './components/repositories/rename-image/rename-image.component';
|
||||
import { ClientDetailsComponent } from './components/groups/shared/client-details/client-details.component';
|
||||
import { PartitionTypeOrganizatorComponent } from './components/groups/shared/partition-type-organizator/partition-type-organizator.component';
|
||||
|
@ -151,6 +151,14 @@ 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';
|
||||
import { SoftwareProfilePartitionComponent } from './components/commands/main-commands/execute-command/software-profile-partition/software-profile-partition.component';
|
||||
import { ClientPendingTasksComponent } from './components/task-logs/client-pending-tasks/client-pending-tasks.component';
|
||||
import { QueueConfirmationModalComponent } from './shared/queue-confirmation-modal/queue-confirmation-modal.component';
|
||||
import { ModalOverlayComponent } from './shared/modal-overlay/modal-overlay.component';
|
||||
import { ScrollToTopComponent } from './shared/scroll-to-top/scroll-to-top.component';
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
|
@ -170,7 +178,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
HeaderComponent,
|
||||
SidebarComponent,
|
||||
LoginComponent,
|
||||
AdminComponent,
|
||||
MainLayoutComponent,
|
||||
UsersComponent,
|
||||
RolesComponent,
|
||||
|
@ -180,6 +187,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
GroupsComponent,
|
||||
ManageClientComponent,
|
||||
DeleteModalComponent,
|
||||
QueueConfirmationModalComponent,
|
||||
ClassroomViewComponent,
|
||||
ClientViewComponent,
|
||||
ShowOrganizationalUnitComponent,
|
||||
|
@ -202,6 +210,8 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
CalendarComponent,
|
||||
CreateCalendarComponent,
|
||||
CreateClientImageComponent,
|
||||
CreateRepositoryModalComponent,
|
||||
PartitionAssistantComponent,
|
||||
CreateCalendarRuleComponent,
|
||||
CommandsGroupsComponent,
|
||||
CommandsTaskComponent,
|
||||
|
@ -214,7 +224,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
StatusComponent,
|
||||
ImagesComponent,
|
||||
CreateImageComponent,
|
||||
PartitionAssistantComponent,
|
||||
SoftwareComponent,
|
||||
CreateSoftwareComponent,
|
||||
SoftwareProfileComponent,
|
||||
|
@ -228,7 +237,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
ExecuteCommandOuComponent,
|
||||
DeployImageComponent,
|
||||
MainRepositoryViewComponent,
|
||||
ExecuteCommandOuComponent,
|
||||
EnvVarsComponent,
|
||||
MenusComponent,
|
||||
CreateMenuComponent,
|
||||
|
@ -249,7 +257,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
RunScriptAssistantComponent,
|
||||
SaveScriptComponent,
|
||||
EditImageComponent,
|
||||
ShowGitImagesComponent,
|
||||
ShowGitCommitsComponent,
|
||||
RenameImageComponent,
|
||||
ClientDetailsComponent,
|
||||
PartitionTypeOrganizatorComponent,
|
||||
|
@ -259,7 +267,14 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
CreateTaskScriptComponent,
|
||||
ViewParametersModalComponent,
|
||||
OutputDialogComponent,
|
||||
ClientTaskLogsComponent
|
||||
ClientTaskLogsComponent,
|
||||
BootSoPartitionComponent,
|
||||
RemoveCacheImageComponent,
|
||||
ChangeParentComponent,
|
||||
SoftwareProfilePartitionComponent,
|
||||
ClientPendingTasksComponent,
|
||||
ModalOverlayComponent,
|
||||
ScrollToTopComponent
|
||||
],
|
||||
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)="toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected}" >
|
||||
|
||||
<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,146 @@
|
|||
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 }) => client.selected = true);
|
||||
|
||||
this.selectedClients = this.data.clients.filter((client: { selected: boolean }) => client.selected);
|
||||
|
||||
this.selectedModelClient = this.data.clients.find((client: { selected: boolean }) => client.selected) || 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,11 @@ 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';
|
||||
import {SoftwareProfilePartitionComponent} from "./software-profile-partition/software-profile-partition.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-command',
|
||||
|
@ -27,9 +32,9 @@ 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.softwareInventory', slug: 'software-inventory', disabled: false },
|
||||
{ translationKey: 'executeCommands.hardwareInventory', slug: 'hardware-inventory', disabled: true },
|
||||
{ translationKey: 'executeCommands.runScript', slug: 'run-script', disabled: false },
|
||||
];
|
||||
|
@ -40,13 +45,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 +64,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 +112,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', 'software-inventory'].includes(command.slug);
|
||||
}
|
||||
} else {
|
||||
if (command.slug === 'create-image') {
|
||||
if (command.slug === 'create-image'|| command.slug === 'software-inventory') {
|
||||
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 +161,18 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
if (action === 'power-on') {
|
||||
this.powerOnClient();
|
||||
}
|
||||
|
||||
if (action === 'remove-cache-image') {
|
||||
this.removeImageCache();
|
||||
}
|
||||
|
||||
if (action === 'hardware-inventory') {
|
||||
this.hardwareInventory();
|
||||
}
|
||||
|
||||
if (action === 'software-inventory') {
|
||||
this.softwareInventory();
|
||||
}
|
||||
}
|
||||
|
||||
rebootClient(): void {
|
||||
|
@ -133,24 +183,108 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loginClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/login-client`, {
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hardwareInventory(): void {
|
||||
if (this.clientData.length === 0) {
|
||||
this.toastService.error('No hay clientes seleccionados');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = this.clientData[0].uuid;
|
||||
|
||||
this.http.post(`${this.baseUrl}/clients/server/${clientId}/hardware-inventory`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.toastService.success('Inventario de hardware actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
softwareInventory(): void {
|
||||
if (this.clientData.length === 0) {
|
||||
this.toastService.error('No hay clientes seleccionados');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientDataToSend = {
|
||||
clientId: this.clientData[0].uuid,
|
||||
name: this.clientData[0].name,
|
||||
mac: this.clientData[0].mac,
|
||||
status: this.clientData[0].status,
|
||||
partitions: this.clientData[0].partitions,
|
||||
firmwareType: this.clientData[0].firmwareType,
|
||||
ip: this.clientData[0].ip
|
||||
}
|
||||
|
||||
const clientId = this.clientData[0].uuid;
|
||||
|
||||
const dialogRef = this.dialog.open(SoftwareProfilePartitionComponent, {
|
||||
width: '70vw',
|
||||
height: 'auto',
|
||||
data: { client: clientDataToSend }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
powerOnClient(): void {
|
||||
this.http.post(`${this.baseUrl}/image-repositories/wol`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
|
@ -159,7 +293,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
this.toastService.success('Petición de encendido enviada correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -172,7 +306,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
this.toastService.success('Petición de apagado enviada correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,55 @@
|
|||
<h2 mat-dialog-title> Seleccionar partición para inventariar</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<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"
|
||||
>
|
||||
<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,71 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SoftwareProfilePartitionComponent } from './software-profile-partition.component';
|
||||
import {FormBuilder, 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 {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
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 {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
|
||||
describe('SoftwareProfilePartitionComponent', () => {
|
||||
let component: SoftwareProfilePartitionComponent;
|
||||
let fixture: ComponentFixture<SoftwareProfilePartitionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SoftwareProfilePartitionComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatTableModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SoftwareProfilePartitionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
import {Component, Inject, OnInit} 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-software-profile-partition',
|
||||
templateUrl: './software-profile-partition.component.html',
|
||||
styleUrl: './software-profile-partition.component.css'
|
||||
})
|
||||
export class SoftwareProfilePartitionComponent 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: { client: any },
|
||||
private dialogRef: MatDialogRef<SoftwareProfilePartitionComponent>,
|
||||
private configService: ConfigService,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.clientId = this.data.client?.clientId
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadPartitions();
|
||||
}
|
||||
|
||||
loadPartitions() {
|
||||
const url = `${this.baseUrl}/clients/${this.data.client?.clientId}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.dataSource.data = response.partitions;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.loading = true;
|
||||
|
||||
this.http.post(`${this.baseUrl}/clients/server/${this.data.client.clientId}/software-inventory`, {
|
||||
partition: this.selectedPartition['@id'],
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Inventario de software actualizado correctamente');
|
||||
this.dialogRef.close(response);
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al actualizar el inventario de software');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -1,44 +1,557 @@
|
|||
/* ===== HEADER DE BIENVENIDA ===== */
|
||||
.welcome-header {
|
||||
background: #3f51b5;
|
||||
color: white;
|
||||
padding: 2rem 2rem 1.5rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.welcome-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="white" opacity="0.1"/><circle cx="75" cy="75" r="1" fill="white" opacity="0.1"/><circle cx="50" cy="10" r="0.5" fill="white" opacity="0.1"/><circle cx="10" cy="60" r="0.5" fill="white" opacity="0.1"/><circle cx="90" cy="40" r="0.5" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.welcome-icon mat-icon {
|
||||
font-size: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.welcome-description {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.help-button,
|
||||
.refresh-button {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
color: white !important;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.help-button:hover,
|
||||
.refresh-button:hover {
|
||||
background: rgba(255, 255, 255, 0.3) !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ===== CONTENIDO PRINCIPAL ===== */
|
||||
mat-dialog-content {
|
||||
height: calc(100% - 64px);
|
||||
overflow: auto;
|
||||
padding-top: 0.5em !important;
|
||||
height: calc(100% - 200px);
|
||||
overflow: auto;
|
||||
padding: 0 !important;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
.main-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* ===== SPINNER DE CARGA ===== */
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== RESUMEN DEL SISTEMA ===== */
|
||||
.system-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overview-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.overview-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.overview-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.overview-icon {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.overview-icon mat-icon {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.overview-content h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.overview-content p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* ===== BADGES DE ESTADO ===== */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-badge.online {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.offline {
|
||||
background: linear-gradient(135deg, #dc3545, #c82333);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.info {
|
||||
background: linear-gradient(135deg, #17a2b8, #138496);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ===== TABS PRINCIPALES ===== */
|
||||
.main-tabs {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::ng-deep .main-tabs .mat-tab-header {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
::ng-deep .main-tabs .mat-tab-label {
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
::ng-deep .main-tabs .mat-tab-label-active {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
::ng-deep .main-tabs .mat-ink-bar {
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* ===== CONTENEDOR DE ERRORES ===== */
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 300px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
margin: 20px auto;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
background-color: rgb(243, 243, 243);
|
||||
color: rgb(48, 48, 48);
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 3rem 2rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e9ecef;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 64px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
color: #dc3545;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-card h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.error-card p {
|
||||
margin-top: 0;
|
||||
margin: 0 0 2rem 0;
|
||||
color: #6c757d;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ===== REPOSITORIOS ===== */
|
||||
.repositories-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.repositories-selector-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.repository-selector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.repository-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.repository-select-field {
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-repository-content {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.no-repository-selected {
|
||||
text-align: center;
|
||||
padding: 3rem 2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.no-repository-selected .no-data-icon {
|
||||
font-size: 4rem;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.no-repository-selected h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.no-repository-selected p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ===== FOOTER CON ACCIONES ===== */
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
background: white;
|
||||
border-top: 1px solid #e9ecef;
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
.action-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.last-update {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
color: #6c757d !important;
|
||||
border: 1px solid #dee2e6 !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background: #f8f9fa !important;
|
||||
border-color: #667eea !important;
|
||||
color: #667eea !important;
|
||||
}
|
||||
|
||||
/* ===== RESPONSIVE ===== */
|
||||
@media (max-width: 768px) {
|
||||
.welcome-header {
|
||||
padding: 1.5rem 1rem 1rem 1rem;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.system-overview {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.welcome-header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.welcome-icon mat-icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.repository-select-field {
|
||||
min-width: 250px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== ANIMACIONES ===== */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.system-overview,
|
||||
.main-tabs {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.overview-card:nth-child(1) { animation-delay: 0.1s; }
|
||||
.overview-card:nth-child(2) { animation-delay: 0.2s; }
|
||||
.overview-card:nth-child(3) { animation-delay: 0.3s; }
|
||||
|
||||
/* ===== ESTILOS GLOBALES DEL DIALOG ===== */
|
||||
::ng-deep .mat-dialog-container {
|
||||
border-radius: 16px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-dialog-content {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* ===== LOADING INDIVIDUAL POR TAB ===== */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 300px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.loading-container .loading-content {
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.loading-container .loading-spinner {
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.loading-container .loading-text {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
|
@ -1,94 +1,209 @@
|
|||
<header>
|
||||
<h1 mat-dialog-title>{{'GlobalStatus' | translate}}</h1>
|
||||
</header>
|
||||
<mat-dialog-content [ngClass]="{'loading': loading}">
|
||||
<div class="spinner-container" *ngIf="loading">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<!-- Header con bienvenida -->
|
||||
<div class="welcome-header">
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-icon">
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
</div>
|
||||
<div class="welcome-text">
|
||||
<h1 class="welcome-title">{{ 'GlobalStatus' | translate }}</h1>
|
||||
<p class="welcome-subtitle">Bienvenido a la consola de administración de OpenGnsys</p>
|
||||
<p class="welcome-description">Estado general de todos los componentes de la plataforma</p>
|
||||
</div>
|
||||
</div>
|
||||
<mat-tab-group (selectedTabChange)="onTabChange($event)">
|
||||
<mat-tab label="OgBoot">
|
||||
<div *ngIf="!loading && !errorOgBoot" class="content-container">
|
||||
<app-status-tab
|
||||
[loading]="loading"
|
||||
[diskUsage]="ogBootDiskUsage"
|
||||
[servicesStatus]="ogBootServicesStatus"
|
||||
[installedOgLives]="installedOgLives"
|
||||
[diskUsageChartData]="ogBootDiskUsageChartData"
|
||||
[view]="view"
|
||||
[colorScheme]="colorScheme"
|
||||
[isDoughnut]="isDoughnut"
|
||||
[showLabels]="showLabels"
|
||||
[isDhcp]="isDhcp"
|
||||
[isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
<mat-card *ngIf="!loading && errorOgBoot" class="error-card">
|
||||
<mat-card-content>
|
||||
<p>{{ 'errorLoadingData' | translate }}</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</mat-tab>
|
||||
<div class="welcome-actions">
|
||||
<button mat-icon-button class="refresh-button" (click)="refreshAll()" matTooltip="Actualizar datos">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-tab label="Dhcp">
|
||||
<div *ngIf="!loading && !errorDhcp" class="content-container">
|
||||
<app-status-tab
|
||||
[loading]="loading"
|
||||
[diskUsage]="dhcpDiskUsage"
|
||||
[servicesStatus]="dhcpServicesStatus"
|
||||
[subnets]="subnets"
|
||||
[diskUsageChartData]="dhcpDiskUsageChartData"
|
||||
[view]="view"
|
||||
[colorScheme]="colorScheme"
|
||||
[isDoughnut]="isDoughnut"
|
||||
[showLabels]="showLabels"
|
||||
[isDhcp]="isDhcp"
|
||||
[isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
<mat-card *ngIf="!loading && errorDhcp" class="error-card">
|
||||
<mat-card-content>
|
||||
<p>{{ 'errorLoadingData' | translate }}</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</mat-tab>
|
||||
<!-- Contenido principal -->
|
||||
<mat-dialog-content [ngClass]="{'loading': loading}">
|
||||
<!-- Spinner de carga -->
|
||||
<div class="spinner-container" *ngIf="loading">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">Cargando estado del sistema...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-tab label="Repositorios">
|
||||
<mat-tab-group>
|
||||
<mat-tab *ngFor="let repository of repositories" [label]="repository.name">
|
||||
<div *ngIf="!loading && !errorRepositories[repository.uuid] && repositoryStatuses[repository.uuid]">
|
||||
<app-status-tab
|
||||
[loading]="loading"
|
||||
[diskUsage]="repositoryStatuses[repository.uuid].disk"
|
||||
[servicesStatus]="repositoryStatuses[repository.uuid].services"
|
||||
[processesStatus]="repositoryStatuses[repository.uuid].processes"
|
||||
[ramUsage]="repositoryStatuses[repository.uuid].ram"
|
||||
[cpuUsage]="repositoryStatuses[repository.uuid].cpu"
|
||||
[diskUsageChartData]="[
|
||||
{ name: 'Usado', value: repositoryStatuses[repository.uuid].disk.used },
|
||||
{ name: 'Disponible', value: repositoryStatuses[repository.uuid].disk.available }
|
||||
]"
|
||||
[ramUsageChartData]="[
|
||||
{ name: 'Usado', value: repositoryStatuses[repository.uuid].ram.used },
|
||||
{ name: 'Disponible', value: repositoryStatuses[repository.uuid].ram.available }
|
||||
]"
|
||||
[view]="view"
|
||||
[colorScheme]="colorScheme"
|
||||
[isDoughnut]="isDoughnut"
|
||||
[showLabels]="showLabels"
|
||||
[isDhcp]="false"
|
||||
[isRepository]="true">
|
||||
</app-status-tab>
|
||||
<!-- Contenido principal cuando no está cargando -->
|
||||
<div *ngIf="!loading" class="main-content">
|
||||
<!-- Resumen rápido del sistema -->
|
||||
<div class="system-overview">
|
||||
<div class="overview-card">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>cloud</mat-icon>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<h3>Repositorios</h3>
|
||||
<p>Total: <span class="status-badge info">{{ repositories.length }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>storage</mat-icon>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<h3>OgBoot Server</h3>
|
||||
<p *ngIf="!errorOgBoot">Estado: <span class="status-badge online">Operativo</span></p>
|
||||
<p *ngIf="errorOgBoot">Estado: <span class="status-badge offline">Error</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>router</mat-icon>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<h3>DHCP Server</h3>
|
||||
<p *ngIf="!errorDhcp">Estado: <span class="status-badge online">Operativo</span></p>
|
||||
<p *ngIf="errorDhcp">Estado: <span class="status-badge offline">Error</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs principales -->
|
||||
<mat-tab-group (selectedTabChange)="onTabChange($event)" class="main-tabs">
|
||||
<mat-tab label="{{ 'repositoryLabel' | translate }}">
|
||||
<div class="repositories-container">
|
||||
<div *ngIf="repositories.length === 0" class="no-repositories">
|
||||
<mat-icon class="no-data-icon">cloud_off</mat-icon>
|
||||
<h3>No hay repositorios disponibles</h3>
|
||||
<p>No se encontraron repositorios configurados en el sistema.</p>
|
||||
</div>
|
||||
<mat-card *ngIf="!loading && errorRepositories[repository.uuid]" class="error-card">
|
||||
<mat-card-content>
|
||||
<p>{{ 'errorLoadingData' | translate }}</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
||||
<div *ngIf="repositories.length > 0" class="repositories-selector-container">
|
||||
<!-- Selector de repositorio -->
|
||||
<div class="repository-selector">
|
||||
<mat-form-field appearance="outline" class="repository-select-field">
|
||||
<mat-label>Seleccionar repositorio</mat-label>
|
||||
<mat-select [(ngModel)]="selectedRepositoryUuid" (selectionChange)="onRepositoryChange($event.value)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository.uuid">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix>storage</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Información del repositorio seleccionado -->
|
||||
<div *ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]" class="selected-repository-content">
|
||||
<div class="repository-item">
|
||||
<div class="repository-header">
|
||||
<h3>{{ getSelectedRepositoryName() }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="repository-content">
|
||||
<app-status-tab [loading]="loading" [diskUsage]="repositoryStatuses[selectedRepositoryUuid].disk"
|
||||
[servicesStatus]="repositoryStatuses[selectedRepositoryUuid].services"
|
||||
[processesStatus]="repositoryStatuses[selectedRepositoryUuid].processes"
|
||||
[ramUsage]="repositoryStatuses[selectedRepositoryUuid].ram" [cpuUsage]="repositoryStatuses[selectedRepositoryUuid].cpu"
|
||||
[diskUsageChartData]="[
|
||||
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].disk.used },
|
||||
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].disk.available }
|
||||
]" [ramUsageChartData]="[
|
||||
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].ram.used },
|
||||
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].ram.available }
|
||||
]" [view]="view" [colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels"
|
||||
[isDhcp]="false" [isRepository]="true">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mensaje cuando no hay repositorio seleccionado -->
|
||||
<div *ngIf="!selectedRepositoryUuid" class="no-repository-selected">
|
||||
<mat-icon class="no-data-icon">storage</mat-icon>
|
||||
<h3>Selecciona un repositorio</h3>
|
||||
<p>Elige un repositorio de la lista para ver su estado detallado.</p>
|
||||
</div>
|
||||
|
||||
<!-- Error al cargar repositorio -->
|
||||
<div *ngIf="selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el repositorio seleccionado</p>
|
||||
<button mat-raised-button color="primary" (click)="retryRepositoryStatus(selectedRepositoryUuid)">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="OgBoot Server">
|
||||
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
|
||||
<app-status-tab [loading]="loadingOgBoot" [diskUsage]="ogBootDiskUsage" [servicesStatus]="ogBootServicesStatus"
|
||||
[installedOgLives]="installedOgLives" [diskUsageChartData]="ogBootDiskUsageChartData" [view]="view"
|
||||
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp"
|
||||
[isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
<div *ngIf="loadingOgBoot" class="loading-container">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">Cargando estado de OgBoot...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="errorOgBoot" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el servidor OgBoot</p>
|
||||
<button mat-raised-button color="primary" (click)="loadOgBootStatus()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="DHCP Server">
|
||||
<div *ngIf="!errorDhcp && !loadingDhcp" class="tab-content">
|
||||
<app-status-tab [loading]="loadingDhcp" [diskUsage]="dhcpDiskUsage" [servicesStatus]="dhcpServicesStatus"
|
||||
[subnets]="subnets" [diskUsageChartData]="dhcpDiskUsageChartData" [view]="view" [colorScheme]="colorScheme"
|
||||
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true" [isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
<div *ngIf="loadingDhcp" class="loading-container">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">Cargando estado de DHCP...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="errorDhcp" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el servidor DHCP</p>
|
||||
<button mat-raised-button color="primary" (click)="loadDhcpStatus()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<!-- Footer con acciones -->
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" [mat-dialog-close]="true">{{ 'closeButton' | translate }}</button>
|
||||
<div class="action-info">
|
||||
<p class="last-update">Última actualización: {{ lastUpdateTime }}</p>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button mat-button class="secondary-button" (click)="refreshAll()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Actualizar
|
||||
</button>
|
||||
<button mat-raised-button color="primary" [mat-dialog-close]="true">
|
||||
{{ 'closeButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
|
@ -12,6 +12,9 @@ import {ToastrService} from "ngx-toastr";
|
|||
export class GlobalStatusComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
loading: boolean = false;
|
||||
loadingOgBoot: boolean = false;
|
||||
loadingDhcp: boolean = false;
|
||||
loadingRepositories: boolean = false;
|
||||
errorOgBoot: boolean = false;
|
||||
errorDhcp: boolean = false;
|
||||
errorRepositories: { [key: string]: boolean } = {};
|
||||
|
@ -26,6 +29,8 @@ export class GlobalStatusComponent implements OnInit {
|
|||
repositoriesUrl: string;
|
||||
repositories: any[] = [];
|
||||
repositoryStatuses: { [key: string]: any } = {};
|
||||
lastUpdateTime: string = '';
|
||||
selectedRepositoryUuid: string = '';
|
||||
|
||||
ogBootApiUrl: string;
|
||||
ogBootDiskUsage: any = {};
|
||||
|
@ -39,6 +44,14 @@ export class GlobalStatusComponent implements OnInit {
|
|||
isDhcp: boolean = false;
|
||||
isRepository: boolean = false;
|
||||
|
||||
// Loading específicos para cada sección
|
||||
loadingOgBootOgLives: boolean = false;
|
||||
loadingOgBootServices: boolean = false;
|
||||
loadingOgBootDisk: boolean = false;
|
||||
loadingDhcpSubnets: boolean = false;
|
||||
loadingDhcpServices: boolean = false;
|
||||
loadingDhcpDisk: boolean = false;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
|
@ -48,13 +61,21 @@ export class GlobalStatusComponent implements OnInit {
|
|||
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
|
||||
this.dhcpApiUrl = `${this.baseUrl}/og-dhcp/status`;
|
||||
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
|
||||
|
||||
this.ogBootDiskUsageChartData = [];
|
||||
this.dhcpDiskUsageChartData = [];
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updateLastUpdateTime();
|
||||
|
||||
this.loadOgBootStatus();
|
||||
this.syncSubnets()
|
||||
this.syncTemplates()
|
||||
this.syncOgLives()
|
||||
this.loadDhcpStatus();
|
||||
this.loadRepositories(false);
|
||||
|
||||
this.syncSubnets();
|
||||
this.syncTemplates();
|
||||
this.syncOgLives();
|
||||
}
|
||||
|
||||
syncSubnets() {
|
||||
|
@ -69,7 +90,7 @@ export class GlobalStatusComponent implements OnInit {
|
|||
},
|
||||
error: (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.error('Error al sincronizar las subredes DHCP');
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las subredes');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -85,7 +106,7 @@ export class GlobalStatusComponent implements OnInit {
|
|||
this.toastService.success('Sincronización de las plantillas Pxe completada');
|
||||
}, error => {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.error('Error al sincronizar las plantillas Pxe');
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las plantillas Pxe');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -100,17 +121,32 @@ export class GlobalStatusComponent implements OnInit {
|
|||
this.toastService.success('Sincronización con los ogLives completada');
|
||||
}, error => {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.error('Error al sincronizar imágenes ogLive');
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las imagenes ogLive');
|
||||
});
|
||||
}
|
||||
|
||||
[key: string]: any;
|
||||
|
||||
loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string): void {
|
||||
this.loading = true;
|
||||
loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string, showLoading: boolean = true): void {
|
||||
if (isDhcp) {
|
||||
this.loadingDhcp = true;
|
||||
} else {
|
||||
this.loadingOgBoot = true;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
this.loading = true;
|
||||
}
|
||||
this[errorState] = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.loading = false;
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
if (isDhcp) {
|
||||
this.loadingDhcp = false;
|
||||
} else {
|
||||
this.loadingOgBoot = false;
|
||||
}
|
||||
this[errorState] = true;
|
||||
}, 3500);
|
||||
this.http.get<any>(apiUrl).subscribe({
|
||||
|
@ -132,6 +168,7 @@ export class GlobalStatusComponent implements OnInit {
|
|||
if (data.message.installed_oglives) {
|
||||
installedOgLives.push(...data.message.installed_oglives);
|
||||
}
|
||||
this.loadingOgBootOgLives = false;
|
||||
}
|
||||
|
||||
diskUsageChartData.length = 0;
|
||||
|
@ -140,23 +177,41 @@ export class GlobalStatusComponent implements OnInit {
|
|||
{ name: 'Disponible', value: parseFloat(diskUsage.available) }
|
||||
);
|
||||
|
||||
this.loading = false;
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
if (isDhcp) {
|
||||
this.loadingDhcp = false;
|
||||
} else {
|
||||
this.loadingOgBoot = false;
|
||||
}
|
||||
clearTimeout(timeoutId);
|
||||
},
|
||||
error: error => {
|
||||
console.log(error);
|
||||
this.loading = false;
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado de ogBoot');
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
if (isDhcp) {
|
||||
this.loadingDhcp = false;
|
||||
} else {
|
||||
this.loadingOgBoot = false;
|
||||
}
|
||||
this[errorState] = true;
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadRepositories(): void {
|
||||
this.loading = true;
|
||||
loadRepositories(showLoading: boolean = true): void {
|
||||
if (showLoading) {
|
||||
this.loading = true;
|
||||
}
|
||||
this.errorRepositories = {};
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.loading = false;
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
this.repositories.forEach(repository => {
|
||||
if (!(repository.uuid in this.errorRepositories)) {
|
||||
this.errorRepositories[repository.uuid] = true;
|
||||
|
@ -176,14 +231,15 @@ export class GlobalStatusComponent implements OnInit {
|
|||
this.errorRepositories[repository.uuid] = errorOccurred;
|
||||
|
||||
if (remainingRepositories === 0) {
|
||||
this.loading = false;
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching repositories', error);
|
||||
this.loading = false;
|
||||
this.repositories.forEach(repository => {
|
||||
this.errorRepositories[repository.uuid] = true;
|
||||
|
@ -217,7 +273,7 @@ export class GlobalStatusComponent implements OnInit {
|
|||
callback(false);
|
||||
},
|
||||
error => {
|
||||
console.error(`Error fetching status for repository ${repositoryUuid}`, error);
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
|
||||
clearTimeout(timeoutId);
|
||||
callback(true);
|
||||
}
|
||||
|
@ -226,23 +282,86 @@ export class GlobalStatusComponent implements OnInit {
|
|||
|
||||
loadOgBootStatus(): void {
|
||||
this.isDhcp = false;
|
||||
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot');
|
||||
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot', false);
|
||||
}
|
||||
|
||||
loadDhcpStatus(): void {
|
||||
this.isDhcp = true;
|
||||
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp');
|
||||
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp', false);
|
||||
}
|
||||
|
||||
onTabChange(event: MatTabChangeEvent): void {
|
||||
if (event.tab.textLabel === 'OgBoot') {
|
||||
this.loadOgBootStatus();
|
||||
} else if (event.tab.textLabel === 'Dhcp') {
|
||||
this.loadDhcpStatus();
|
||||
} else if (event.tab.textLabel === 'Repositorios') {
|
||||
if (this.repositories.length === 0) {
|
||||
this.loadRepositories();
|
||||
}
|
||||
switch (event.index) {
|
||||
case 0:
|
||||
if (this.repositories.length === 0) {
|
||||
this.loadRepositories(false);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
this.loadOgBootStatus();
|
||||
break;
|
||||
case 2:
|
||||
this.loadDhcpStatus();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onRepositoryChange(repositoryUuid: string): void {
|
||||
this.selectedRepositoryUuid = repositoryUuid;
|
||||
if (repositoryUuid && !this.repositoryStatuses[repositoryUuid]) {
|
||||
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
|
||||
if (errorOccurred) {
|
||||
this.errorRepositories[repositoryUuid] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedRepositoryName(): string {
|
||||
const selectedRepo = this.repositories.find(repo => repo.uuid === this.selectedRepositoryUuid);
|
||||
return selectedRepo ? selectedRepo.name : '';
|
||||
}
|
||||
|
||||
refreshAll(): void {
|
||||
this.loading = true;
|
||||
this.updateLastUpdateTime();
|
||||
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, false, 'errorOgBoot', true);
|
||||
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, true, 'errorDhcp', true);
|
||||
this.loadRepositories(true);
|
||||
this.syncSubnets();
|
||||
this.syncTemplates();
|
||||
this.syncOgLives();
|
||||
this.toastService.success('Datos actualizados correctamente');
|
||||
}
|
||||
|
||||
updateLastUpdateTime(): void {
|
||||
const now = new Date();
|
||||
this.lastUpdateTime = now.toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentTime(): string {
|
||||
const now = new Date();
|
||||
return now.toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
retryRepositoryStatus(repositoryUuid: string): void {
|
||||
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
|
||||
this.errorRepositories[repositoryUuid] = errorOccurred;
|
||||
if (!errorOccurred) {
|
||||
this.toastService.success('Estado del repositorio actualizado correctamente');
|
||||
} else {
|
||||
this.toastService.error('Error al cargar el estado del repositorio');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,84 +1,533 @@
|
|||
/* ===== LAYOUT PRINCIPAL ===== */
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.disk-usage-container,
|
||||
.ram-usage-container {
|
||||
/* ===== SECCIÓN DE RECURSOS ===== */
|
||||
.resources-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Layout específico para repositorios - gráficas en una sola fila */
|
||||
.resources-section.repository-layout {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.resources-section.repository-layout .resource-card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.resource-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resource-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.resource-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.resource-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.resource-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.disk-usage,
|
||||
.ram-usage {
|
||||
.resource-icon {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resource-icon mat-icon {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.resource-title {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.resource-content {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.service-list,
|
||||
.process-list {
|
||||
margin-top: 0em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.services-status,
|
||||
.processes-status {
|
||||
.resource-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.services-status li {
|
||||
margin: 5px 0;
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 0.9rem;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.usage-percentage {
|
||||
color: #667eea;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ===== CPU USAGE DISPLAY ===== */
|
||||
.cpu-usage-display {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cpu-circle {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 50%;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.cpu-percentage {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.cpu-label {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ===== SECCIÓN DE SERVICIOS ===== */
|
||||
.services-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.service-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.processes-status li {
|
||||
margin: 5px 0;
|
||||
.service-icon {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-led {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
.service-icon mat-icon {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.service-title {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.service-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
background: #e9ecef;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.service-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-led.active {
|
||||
background-color: green;
|
||||
.status-indicator.active {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
box-shadow: 0 0 8px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
|
||||
.status-led.inactive {
|
||||
background-color: red;
|
||||
.status-indicator.inactive {
|
||||
background: linear-gradient(135deg, #dc3545, #c82333);
|
||||
box-shadow: 0 0 8px rgba(220, 53, 69, 0.4);
|
||||
}
|
||||
|
||||
.disk-title,
|
||||
.ram-title {
|
||||
margin-bottom: 0px;
|
||||
.service-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.service-title,
|
||||
.process-title {
|
||||
margin-top: 0px;
|
||||
.service-state {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
table {
|
||||
.service-state.active {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.service-state.inactive {
|
||||
background: linear-gradient(135deg, #dc3545, #c82333);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ===== SECCIÓN DE DATOS ===== */
|
||||
.data-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.data-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.data-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.data-icon {
|
||||
background: linear-gradient(135deg, #17a2b8, #138496);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.data-icon mat-icon {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.data-title {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
.data-table th {
|
||||
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||
color: #495057;
|
||||
padding: 1rem;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f4f4f4;
|
||||
.data-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
color: #2c3e50;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.data-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* ===== RESPONSIVE ===== */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.resources-section {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.services-section {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.resource-content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.resource-card,
|
||||
.service-card,
|
||||
.data-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.cpu-circle {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.cpu-percentage {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.resource-header,
|
||||
.service-header,
|
||||
.data-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.resource-icon,
|
||||
.service-icon,
|
||||
.data-icon {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.resource-icon mat-icon,
|
||||
.service-icon mat-icon,
|
||||
.data-icon mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.resource-title,
|
||||
.service-title,
|
||||
.data-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== ANIMACIONES ===== */
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.resource-card,
|
||||
.service-card,
|
||||
.data-card {
|
||||
animation: slideInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.resource-card:nth-child(1) { animation-delay: 0.1s; }
|
||||
.resource-card:nth-child(2) { animation-delay: 0.2s; }
|
||||
.resource-card:nth-child(3) { animation-delay: 0.3s; }
|
||||
|
||||
.service-card:nth-child(1) { animation-delay: 0.4s; }
|
||||
.service-card:nth-child(2) { animation-delay: 0.5s; }
|
||||
|
||||
.data-card { animation-delay: 0.6s; }
|
||||
|
||||
/* ===== ESTILOS PARA LOS GRÁFICOS ===== */
|
||||
::ng-deep .chart-container ngx-charts-pie-chart {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
::ng-deep .chart-container ngx-charts-pie-chart .chart-legend {
|
||||
position: relative !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
display: block !important;
|
||||
margin-top: 1rem !important;
|
||||
}
|
||||
|
||||
::ng-deep .chart-container ngx-charts-pie-chart .chart-legend .legend-labels {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
::ng-deep .chart-container ngx-charts-pie-chart .chart-legend .legend-label {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
margin: 0.25rem 0 !important;
|
||||
}
|
|
@ -1,113 +1,206 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading" class="dashboard">
|
||||
<!-- Disk Usage Section -->
|
||||
<div class="disk-usage-container">
|
||||
<h3 class="disk-title">{{ 'diskUsageTitle' | translate }}</h3>
|
||||
<div class="disk-usage" joyrideStep="diskUsageStep" text="{{ 'diskUsageDescription' | translate }}">
|
||||
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="diskUsageChartData" [doughnut]="isDoughnut"
|
||||
[labels]="showLabels">
|
||||
</ngx-charts-pie-chart>
|
||||
<div class="disk-usage-info">
|
||||
<p>{{ 'totalLabel' | translate }}: <strong>{{ isRepository ? diskUsage.total : formatBytes(diskUsage.total) }}</strong></p>
|
||||
<p>{{ 'usedLabel' | translate }}: <strong>{{ isRepository ? diskUsage.used : formatBytes(diskUsage.used) }}</strong></p>
|
||||
<p>{{ 'availableLabel' | translate }}: <strong>{{ isRepository ? diskUsage.available : formatBytes(diskUsage.available) }}</strong></p>
|
||||
<p>{{ 'usedPercentageLabel' | translate }}: <strong>{{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}</strong></p>
|
||||
<!-- Sección de uso de recursos -->
|
||||
<div class="resources-section">
|
||||
<!-- Disk Usage Section -->
|
||||
<div class="resource-card disk-usage-container">
|
||||
<div class="resource-header">
|
||||
<div class="resource-icon">
|
||||
<mat-icon>storage</mat-icon>
|
||||
</div>
|
||||
<h3 class="resource-title">{{ 'diskUsageTitle' | translate }}</h3>
|
||||
</div>
|
||||
<div class="resource-content">
|
||||
<div class="chart-container" joyrideStep="diskUsageStep" text="{{ 'diskUsageDescription' | translate }}">
|
||||
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="diskUsageChartData" [doughnut]="isDoughnut"
|
||||
[labels]="showLabels">
|
||||
</ngx-charts-pie-chart>
|
||||
</div>
|
||||
<div class="resource-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'totalLabel' | translate }}:</span>
|
||||
<span class="info-value">{{ isRepository ? diskUsage.total : formatBytes(diskUsage.total) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'usedLabel' | translate }}:</span>
|
||||
<span class="info-value">{{ isRepository ? diskUsage.used : formatBytes(diskUsage.used) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'availableLabel' | translate }}:</span>
|
||||
<span class="info-value">{{ isRepository ? diskUsage.available : formatBytes(diskUsage.available) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
|
||||
<span class="info-value usage-percentage">{{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RAM Usage Section -->
|
||||
<div class="resource-card ram-usage-container" *ngIf="isRepository">
|
||||
<div class="resource-header">
|
||||
<div class="resource-icon">
|
||||
<mat-icon>memory</mat-icon>
|
||||
</div>
|
||||
<h3 class="resource-title">{{ 'RamUsage' | translate }}</h3>
|
||||
</div>
|
||||
<div class="resource-content">
|
||||
<div class="chart-container">
|
||||
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="ramUsageChartData" [doughnut]="isDoughnut"
|
||||
[labels]="showLabels">
|
||||
</ngx-charts-pie-chart>
|
||||
</div>
|
||||
<div class="resource-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'totalLabel' | translate }}:</span>
|
||||
<span class="info-value">{{ ramUsage.total }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'usedLabel' | translate }}:</span>
|
||||
<span class="info-value">{{ ramUsage.used }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'availableLabel' | translate }}:</span>
|
||||
<span class="info-value">{{ ramUsage.available }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
|
||||
<span class="info-value usage-percentage">{{ ramUsage.used_percentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU Usage Section -->
|
||||
<div class="resource-card cpu-usage-container" *ngIf="isRepository">
|
||||
<div class="resource-header">
|
||||
<div class="resource-icon">
|
||||
<mat-icon>speed</mat-icon>
|
||||
</div>
|
||||
<h3 class="resource-title">{{ 'CpuUsage' | translate }}</h3>
|
||||
</div>
|
||||
<div class="resource-content">
|
||||
<div class="cpu-usage-display">
|
||||
<div class="cpu-circle">
|
||||
<div class="cpu-percentage">{{ cpuUsage.used_percentage }}%</div>
|
||||
<div class="cpu-label">Uso actual</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RAM Usage Section -->
|
||||
<div class="ram-usage-container" *ngIf="isRepository">
|
||||
<h3 class="ram-title">{{ 'RamUsage' | translate }}</h3>
|
||||
<div class="ram-usage">
|
||||
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="ramUsageChartData" [doughnut]="isDoughnut"
|
||||
[labels]="showLabels">
|
||||
</ngx-charts-pie-chart>
|
||||
<div class="ram-usage-info">
|
||||
<p>{{ 'totalLabel' | translate }}: <strong>{{ ramUsage.total }}</strong></p>
|
||||
<p>{{ 'usedLabel' | translate }}: <strong>{{ ramUsage.used }}</strong></p>
|
||||
<p>{{ 'availableLabel' | translate }}: <strong>{{ ramUsage.available }}</strong></p>
|
||||
<p>{{ 'usedPercentageLabel' | translate }}: <strong>{{ ramUsage.used_percentage }}</strong></p>
|
||||
<!-- Sección de servicios y procesos -->
|
||||
<div class="services-section">
|
||||
<!-- Services Status Section -->
|
||||
<div class="service-card services-status" joyrideStep="servicesStatusStep" text="{{ 'servicesStatusDescription' | translate }}">
|
||||
<div class="service-header">
|
||||
<div class="service-icon">
|
||||
<mat-icon>settings</mat-icon>
|
||||
</div>
|
||||
<h3 class="service-title">{{ 'servicesTitle' | translate }}</h3>
|
||||
</div>
|
||||
<div class="service-list">
|
||||
<div class="service-item" *ngFor="let service of getServices()">
|
||||
<div class="service-status">
|
||||
<span class="status-indicator"
|
||||
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"></span>
|
||||
<span class="service-name">{{ service.name }}</span>
|
||||
</div>
|
||||
<span class="service-state" [ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }">
|
||||
{{ service.status | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processes Status Section -->
|
||||
<div class="service-card processes-status" *ngIf="isRepository">
|
||||
<div class="service-header">
|
||||
<div class="service-icon">
|
||||
<mat-icon>list_alt</mat-icon>
|
||||
</div>
|
||||
<h3 class="service-title">{{ 'processes' | translate }}</h3>
|
||||
</div>
|
||||
<div class="service-list">
|
||||
<div class="service-item" *ngFor="let process of getProcesses()">
|
||||
<div class="service-status">
|
||||
<span class="status-indicator"
|
||||
[ngClass]="{ 'active': process.status === 'running', 'inactive': process.status !== 'running' }"></span>
|
||||
<span class="service-name">{{ process.name }}</span>
|
||||
</div>
|
||||
<span class="service-state" [ngClass]="{ 'active': process.status === 'running', 'inactive': process.status !== 'running' }">
|
||||
{{ process.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU Usage Section -->
|
||||
<div class="cpu-usage-container" *ngIf="isRepository">
|
||||
<h3 class="cpu-title">{{ 'CpuUsage' | translate }}</h3>
|
||||
<div class="cpu-usage">
|
||||
<p>{{ 'usedLabel' | translate }}: <strong>{{ cpuUsage.used_percentage }}</strong></p>
|
||||
<!-- Sección de datos específicos -->
|
||||
<div class="data-section">
|
||||
<!-- Installed OgLives Section -->
|
||||
<div class="data-card" *ngIf="!isRepository && !isDhcp">
|
||||
<div class="data-header">
|
||||
<div class="data-icon">
|
||||
<mat-icon>computer</mat-icon>
|
||||
</div>
|
||||
<h3 class="data-title">{{ 'InstalledOglivesTitle' | translate }}</h3>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'idLabel' | translate }}</th>
|
||||
<th>{{ 'kernelLabel' | translate }}</th>
|
||||
<th>{{ 'architectureLabel' | translate }}</th>
|
||||
<th>{{ 'revisionLabel' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of installedOgLives">
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.kernel }}</td>
|
||||
<td>{{ item.architecture }}</td>
|
||||
<td>{{ item.revision }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services Status Section -->
|
||||
<div class="services-status" joyrideStep="servicesStatusStep" text="{{ 'servicesStatusDescription' | translate }}">
|
||||
<h3 class="service-title">{{ 'servicesTitle' | translate }}</h3>
|
||||
<ul class="service-list">
|
||||
<li *ngFor="let service of getServices()">
|
||||
<span class="status-led"
|
||||
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"></span>
|
||||
{{ service.name }}: {{ service.status | translate }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Processes Status Section -->
|
||||
<div class="processes-status" *ngIf="isRepository">
|
||||
<h3 class="process-title">{{ 'processes' | translate }}</h3>
|
||||
<ul class="process-list">
|
||||
<li *ngFor="let process of getProcesses()">
|
||||
<span class="status-led"
|
||||
[ngClass]="{ 'active': process.status === 'running', 'inactive': process.status !== 'running' }"></span>
|
||||
{{ process.name }}: {{ process.status }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Installed OgLives / Subnets Section -->
|
||||
<div *ngIf="!isRepository && !isDhcp">
|
||||
<h3>{{ 'InstalledOglivesTitle' | translate }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'idLabel' | translate }}</th>
|
||||
<th>{{ 'kernelLabel' | translate }}</th>
|
||||
<th>{{ 'architectureLabel' | translate }}</th>
|
||||
<th>{{ 'revisionLabel' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of installedOgLives">
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.kernel }}</td>
|
||||
<td>{{ item.architecture }}</td>
|
||||
<td>{{ item.revision }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isDhcp">
|
||||
<h3>{{ 'subnets' | translate }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'idLabel' | translate }}</th>
|
||||
<th>{{ 'bootFileNameLabel' | translate }}</th>
|
||||
<th>{{ 'nextServerLabel' | translate }}</th>
|
||||
<th>{{ 'ipLabel' | translate }}</th>
|
||||
<th>{{ 'clientsLabel' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of subnets">
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item['boot-file-name'] }}</td>
|
||||
<td>{{ item['next-server'] }}</td>
|
||||
<td>{{ item.subnet }}</td>
|
||||
<td>{{ item.reservations.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Subnets Section -->
|
||||
<div class="data-card" *ngIf="isDhcp">
|
||||
<div class="data-header">
|
||||
<div class="data-icon">
|
||||
<mat-icon>router</mat-icon>
|
||||
</div>
|
||||
<h3 class="data-title">{{ 'subnets' | translate }}</h3>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'idLabel' | translate }}</th>
|
||||
<th>{{ 'bootFileNameLabel' | translate }}</th>
|
||||
<th>{{ 'nextServerLabel' | translate }}</th>
|
||||
<th>{{ 'ipLabel' | translate }}</th>
|
||||
<th>{{ 'clientsLabel' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of subnets">
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item['boot-file-name'] }}</td>
|
||||
<td>{{ item['next-server'] }}</td>
|
||||
<td>{{ item.subnet }}</td>
|
||||
<td>{{ item.reservations.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -31,7 +31,10 @@
|
|||
</div>
|
||||
<div class="table-header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
|
||||
<mat-chip> {{ clientData.firmwareType }}</mat-chip>
|
||||
<mat-chip *ngIf="clientData.firmwareType" class="firmware-chip">
|
||||
<mat-icon>memory</mat-icon>
|
||||
{{ clientData.firmwareType }}
|
||||
</mat-chip>
|
||||
</div>
|
||||
|
||||
<div class="disk-container">
|
||||
|
|
|
@ -1,105 +1,677 @@
|
|||
.title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.calendar-button-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card.unidad-card {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
background-color: #eaeff6;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Contenedor principal modernizado */
|
||||
.select-container {
|
||||
gap: 16px;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
padding: 32px;
|
||||
background: white !important;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Secciones del formulario */
|
||||
.form-section {
|
||||
background: white !important;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-section-title mat-icon {
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.selector {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
margin-top: 16px;
|
||||
box-sizing: border-box;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
flex: 1;
|
||||
max-width: 50%;
|
||||
max-width: calc(50% - 10px);
|
||||
}
|
||||
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
.full-width {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Header modernizado */
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
padding: 24px 32px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.header-container-title h2 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Estilos modernos para el badge de destino */
|
||||
.destination-info {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.destination-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #bbdefb;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.destination-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.destination-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.destination-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #1976d2;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.destination-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #0d47a1;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
gap: 12px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Tabla de particiones modernizada */
|
||||
.partition-table-container {
|
||||
background-color: #eaeff6;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
margin-top: 20px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.partition-table-container h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.repository-label {
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
mat-chip {
|
||||
margin-top: 8px !important;
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Botón de crear repositorio modernizado */
|
||||
.create-repository-button {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.create-repository-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.create-repository-button mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Botones modernizados */
|
||||
.action-button {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
padding: 12px 24px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Campos de formulario modernizados */
|
||||
mat-form-field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex {
|
||||
border-radius: 8px;
|
||||
background-color: white !important;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
::ng-deep .mat-form-field-appearance-fill.mat-focused .mat-form-field-flex {
|
||||
background-color: white;
|
||||
border-color: #2196f3;
|
||||
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
|
||||
}
|
||||
|
||||
/* Overlay de carga para creación de repositorio */
|
||||
.creating-repository-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
color: white;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.creating-repository-overlay p {
|
||||
margin-top: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Estilo para hacer el backdrop no clickeable */
|
||||
::ng-deep .non-clickable-backdrop {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.select-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.selector {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.create-repository-button {
|
||||
min-width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.destination-badge {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.destination-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.destination-value {
|
||||
max-width: 150px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.destination-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilos para elementos específicos */
|
||||
.unit-name {
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Estilos para las opciones de acción Git */
|
||||
.git-action-selector {
|
||||
margin: 24px 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.action-chips-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
::ng-deep .action-chip {
|
||||
margin: 8px !important;
|
||||
padding: 12px 20px !important;
|
||||
border-radius: px !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 14px !important;
|
||||
transition: all 0.3s ease !important;
|
||||
border: 2px solid transparent !important;
|
||||
background: white !important;
|
||||
color: #6c757d !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
cursor: pointer !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
min-height: 48px !important;
|
||||
}
|
||||
|
||||
::ng-deep .action-chip:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
::ng-deep .action-chip.mat-mdc-chip-selected {
|
||||
border-color: #667eea !important;
|
||||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2) !important;
|
||||
}
|
||||
|
||||
::ng-deep .create-chip.mat-mdc-chip-selected {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
::ng-deep .update-chip.mat-mdc-chip-selected {
|
||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
::ng-deep .action-chip mat-icon {
|
||||
font-size: 18px !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
|
||||
.action-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
color: #495057;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-hint mat-icon {
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.git-action-section {
|
||||
background: white !important;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
/* Eliminar sombra de la tabla */
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Animaciones para transiciones de formulario */
|
||||
.form-transition {
|
||||
transition: all 0.3s ease-in-out;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.form-transition.ng-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.form-transition.ng-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.form-transition.ng-leave {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.form-transition.ng-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
/* Estilos para los formularios específicos */
|
||||
.git-form-section {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-top: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.git-form-section:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Estilos para la sección de repositorio Git */
|
||||
.git-repository-section {
|
||||
background: white !important;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.repository-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.repository-header h4 {
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.repository-selector {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.repository-field {
|
||||
flex: 0 0 300px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.repository-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
color: #6c757d;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.info-item mat-icon {
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.create-repository-button {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.2);
|
||||
}
|
||||
|
||||
.create-repository-button:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #218838 0%, #1ea085 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.create-repository-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.create-repository-button mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.no-repositories-hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
color: #dc3545;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
padding: 4px 4px 4px 12px;
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #dc3545;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.no-repositories-hint mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Estilos para el hint del formulario */
|
||||
::ng-deep .mat-form-field-hint {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
color: #6c757d !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-form-field-hint mat-icon {
|
||||
font-size: 14px !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
color: #667eea !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.header-container {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.selector {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.input-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Responsive para repositorio Git */
|
||||
.repository-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.repository-selector {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.repository-field {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.repository-info {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.create-repository-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Botón flotante para scroll hacia arriba */
|
||||
.scroll-to-top-button {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 30px;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.scroll-to-top-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Animación de entrada/salida */
|
||||
.scroll-to-top-button {
|
||||
animation: fadeInUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive para el botón */
|
||||
@media (max-width: 768px) {
|
||||
.scroll-to-top-button {
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,87 +1,205 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<!-- Overlay de carga para creación de repositorio -->
|
||||
<app-modal-overlay
|
||||
[isVisible]="creatingRepository"
|
||||
message="Creando repositorio...">
|
||||
</app-modal-overlay>
|
||||
|
||||
<div class="header-container">
|
||||
<div class="header-container-title">
|
||||
<h2 >
|
||||
<h2>
|
||||
Crear imagen desde {{ clientName }}
|
||||
</h2>
|
||||
<div class="destination-info">
|
||||
<div class="destination-badge">
|
||||
<mat-icon class="destination-icon">cloud_upload</mat-icon>
|
||||
<div class="destination-content">
|
||||
<span class="destination-label">Destino</span>
|
||||
<span class="destination-value">{{ selectedRepository?.name || 'No hay repositorio asociado' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="action-button" [disabled]="!selectedPartition" (click)="save()">Ejecutar</button>
|
||||
<button class="action-button" id="execute-button" [disabled]="!selectedPartition" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Tipo de imagen</mat-label>
|
||||
<mat-select [(ngModel)]="imageType" class="full-width" (selectionChange)="onImageTypeSelected($event.value)">
|
||||
<mat-option [value]="'monolithic'">Monolítica</mat-option>
|
||||
<!--
|
||||
<mat-option [value]="'git'">Git</mat-option>
|
||||
-->
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- Sección: Configuración de tipo de imagen -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>settings</mat-icon>
|
||||
Configuración de tipo de imagen
|
||||
</div>
|
||||
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Tipo de imagen</mat-label>
|
||||
<mat-select [(ngModel)]="imageType" class="full-width" (selectionChange)="onImageTypeSelected($event.value)">
|
||||
<mat-option [value]="'monolithic'">Monolítica</mat-option>
|
||||
<mat-option [value]="'git'">Git</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Nombre canónico</mat-label>
|
||||
<input matInput [disabled]="selectedImage" [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
||||
</mat-form-field>
|
||||
<!-- Sección: Configuración Git (solo para tipo git) -->
|
||||
<div class="form-section" *ngIf="imageType === 'git'">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>code</mat-icon>
|
||||
Configuración Git
|
||||
</div>
|
||||
|
||||
<div class="git-repository-section">
|
||||
<div class="repository-header">
|
||||
<button *ngIf="imageType === 'git'"
|
||||
class="create-repository-button"
|
||||
(click)="openCreateRepositoryModal()"
|
||||
[disabled]="creatingRepository">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>Crear nuevo repositorio / SO</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccionar repositorio Git</mat-label>
|
||||
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositorySelected($event.value)" required>
|
||||
<mat-option [value]="null">Seleccionar repositorio git / SO</mat-option>
|
||||
<mat-option *ngFor="let repo of gitRepositories" [value]="repo">{{ repo.name }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-spinner *ngIf="loadingGitRepositories" matSuffix diameter="20"></mat-spinner>
|
||||
<mat-hint>
|
||||
<mat-icon>info</mat-icon>
|
||||
Selecciona el repositorio git para obtener las imágenes disponibles.
|
||||
<span *ngIf="gitRepositories.length === 0" class="no-repositories-hint">
|
||||
No hay repositorios disponibles. Crea uno nuevo para continuar.
|
||||
</span>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [disabled]="!imageType" [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
|
||||
<mat-option *ngFor="let image of images" [value]="image">{{ image?.name }}</mat-option>
|
||||
</mat-select>
|
||||
<button *ngIf="selectedImage" mat-icon-button matSuffix aria-label="Clear client search"
|
||||
(click)="selectedImage = null; resetCanonicalName()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint>Seleccione la imagen para sobreescribir si se requiere. </mat-hint>
|
||||
</mat-form-field>
|
||||
<!-- Opciones de acción Git -->
|
||||
<div class="git-action-selector">
|
||||
<div class="action-chips-container">
|
||||
<mat-chip-listbox [(ngModel)]="gitAction" required class="action-chip-listbox">
|
||||
<mat-chip-option value="create" class="action-chip create-chip firmware-chip" (click)="onGitActionSelected({value: 'create'})">
|
||||
<span>Crear imagen</span>
|
||||
</mat-chip-option>
|
||||
<mat-chip-option value="update" class="action-chip update-chip firmware-chip" (click)="onGitActionSelected({value: 'update'})">
|
||||
<span>Actualizar imagen</span>
|
||||
</mat-chip-option>
|
||||
</mat-chip-listbox>
|
||||
</div>
|
||||
<div class="action-hint">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span *ngIf="gitAction === 'create'">Crea una nueva imagen con el nombre especificado</span>
|
||||
<span *ngIf="gitAction === 'update'">Actualiza una imagen existente seleccionada</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección: Configuración general -->
|
||||
<div class="form-section" *ngIf="imageType !== 'git'">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>image</mat-icon>
|
||||
Configuración de imagen
|
||||
</div>
|
||||
|
||||
<!-- Opciones de acción para imágenes monolíticas -->
|
||||
<div class="action-chips-container">
|
||||
<mat-chip-listbox [(ngModel)]="monolithicAction" required class="action-chip-listbox">
|
||||
<mat-chip-option value="create" class="action-chip create-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'create'})">
|
||||
<span>Crear imagen</span>
|
||||
</mat-chip-option>
|
||||
<mat-chip-option value="update" class="action-chip update-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'update'})">
|
||||
<span>Actualizar imagen</span>
|
||||
</mat-chip-option>
|
||||
</mat-chip-listbox>
|
||||
</div>
|
||||
<div class="action-hint">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span *ngIf="monolithicAction === 'create'">Crea una nueva imagen con el nombre especificado</span>
|
||||
<span *ngIf="monolithicAction === 'update'">Actualiza una imagen existente seleccionada</span>
|
||||
</div>
|
||||
|
||||
<div 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>
|
||||
<div class="selector" *ngIf="monolithicAction === 'create'">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Nombre canónico</mat-label>
|
||||
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
||||
<mat-hint>Introduce el nombre para la nueva imagen que se creará.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<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 class="selector" *ngIf="monolithicAction === 'update'">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
|
||||
<mat-option [value]="null">Seleccionar imagen para actualizar</mat-option>
|
||||
<mat-option *ngFor="let image of images" [value]="image">{{ image?.name }}</mat-option>
|
||||
</mat-select>
|
||||
<button *ngIf="selectedImage" mat-icon-button matSuffix aria-label="Clear client search"
|
||||
(click)="selectedImage = null; resetCanonicalName()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint>Selecciona la imagen existente que quieres actualizar.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Sección: Selección de partición -->
|
||||
<div class="form-section" #partitionSection id="partition-selection">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>storage</mat-icon>
|
||||
Selección de partición
|
||||
</div>
|
||||
|
||||
<div class="partition-table-container">
|
||||
<table mat-table [dataSource]="dataSource">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-scroll-to-top
|
||||
[threshold]="200"
|
||||
targetElement=".header-container"
|
||||
position="bottom-right"
|
||||
[showTooltip]="true"
|
||||
tooltipText="Volver arriba"
|
||||
tooltipPosition="left">
|
||||
</app-scroll-to-top>
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import {Component, EventEmitter, OnInit, Output} from '@angular/core';
|
||||
import {Component, EventEmitter, OnInit, Output, ViewChild, ElementRef} from '@angular/core';
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
||||
import {CreateRepositoryModalComponent} from "./create-repository-modal/create-repository-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-image',
|
||||
|
@ -14,18 +17,32 @@ import { ConfigService } from '@services/config.service';
|
|||
export class CreateClientImageComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
@ViewChild('partitionSection', { static: false }) partitionSection!: ElementRef;
|
||||
|
||||
errorMessage = '';
|
||||
clientId: string | null = null;
|
||||
partitions: any[] = [];
|
||||
images: any[] = [];
|
||||
clientName: string = '';
|
||||
selectedPartition: any = null;
|
||||
private _selectedPartition: any = null;
|
||||
name: string = '';
|
||||
client: any = null;
|
||||
loading: boolean = false;
|
||||
selectedImage: any = null;
|
||||
imageType : string = 'monolithic';
|
||||
private _imageType: string = 'monolithic';
|
||||
selectedRepository: any = null;
|
||||
gitRepositories: any[] = [];
|
||||
selectedGitRepository: any = null;
|
||||
gitImageRepositories: any[] = [];
|
||||
gitImageName: string = '';
|
||||
loadingGitRepositories: boolean = false;
|
||||
loadingGitImageRepositories: boolean = false;
|
||||
creatingRepository: boolean = false;
|
||||
gitAction: string = 'create';
|
||||
monolithicAction: string = 'create';
|
||||
existingImages: any[] = [];
|
||||
selectedExistingImage: any = null;
|
||||
loadingExistingImages: boolean = false;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
columns = [
|
||||
{
|
||||
|
@ -69,11 +86,13 @@ export class CreateClientImageComponent implements OnInit{
|
|||
private configService: ConfigService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
console.log('CreateImageComponent ngOnInit ejecutado');
|
||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
||||
this.loadPartitions();
|
||||
this.loadImages();
|
||||
|
@ -86,6 +105,7 @@ export class CreateClientImageComponent implements OnInit{
|
|||
if (response.partitions) {
|
||||
this.client = response;
|
||||
this.clientName = response.name;
|
||||
this.selectedRepository = response.repository;
|
||||
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0;
|
||||
|
@ -100,7 +120,6 @@ export class CreateClientImageComponent implements OnInit{
|
|||
|
||||
onImageTypeSelected(event: any) {
|
||||
this.imageType = event;
|
||||
this.loadImages();
|
||||
}
|
||||
|
||||
loadImages() {
|
||||
|
@ -115,45 +134,285 @@ export class CreateClientImageComponent implements OnInit{
|
|||
);
|
||||
}
|
||||
|
||||
loadGitRepositories() {
|
||||
this.loadingGitRepositories = true;
|
||||
const url = `${this.baseUrl}/git-repositories?repository=${this.selectedRepository.id}&page=1&itemsPerPage=100`;
|
||||
return this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
this.gitRepositories = response['hydra:member'];
|
||||
this.loadingGitRepositories = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los repositorios git:', error);
|
||||
this.loadingGitRepositories = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadGitImageRepositories(gitRepository: any) {
|
||||
this.loadingGitImageRepositories = true;
|
||||
const url = `${this.baseUrl}/git-image-repositories?gitRepository.id=${gitRepository.id}&page=1&itemsPerPage=100`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
this.gitImageRepositories = response['hydra:member'];
|
||||
this.loadingGitImageRepositories = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar las imágenes de repositorio git:', error);
|
||||
this.loadingGitImageRepositories = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onGitRepositorySelected(gitRepository: any) {
|
||||
this.selectedGitRepository = gitRepository;
|
||||
this.selectedExistingImage = null;
|
||||
this.existingImages = [];
|
||||
if (gitRepository) {
|
||||
this.loadGitImageRepositories(gitRepository);
|
||||
} else {
|
||||
this.gitImageRepositories = [];
|
||||
}
|
||||
}
|
||||
|
||||
onGitActionSelected(event: any) {
|
||||
console.log('onGitActionSelected llamado con:', event);
|
||||
this.gitAction = event.value;
|
||||
this.selectedExistingImage = null;
|
||||
this.gitImageName = '';
|
||||
|
||||
// Si se selecciona 'update' y ya hay un repositorio Git seleccionado, cargar los repositorios de imágenes
|
||||
if (event.value === 'update' && this.selectedGitRepository) {
|
||||
this.loadGitImageRepositories(this.selectedGitRepository);
|
||||
}
|
||||
|
||||
console.log('Antes del setTimeout');
|
||||
// Hacer scroll hacia la sección de partición después de un delay más largo
|
||||
setTimeout(() => {
|
||||
console.log('Dentro del setTimeout, llamando a scrollToPartitionSection');
|
||||
this.scrollToPartitionSection();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
onMonolithicActionSelected(event: any) {
|
||||
console.log('onMonolithicActionSelected llamado con:', event);
|
||||
this.monolithicAction = event.value;
|
||||
this.selectedImage = null;
|
||||
this.name = '';
|
||||
|
||||
// Si se selecciona 'update', cargar las imágenes existentes
|
||||
if (event.value === 'update') {
|
||||
this.loadImages();
|
||||
}
|
||||
|
||||
console.log('Antes del setTimeout (monolithic)');
|
||||
// Hacer scroll hacia la sección de partición después de un delay más largo
|
||||
setTimeout(() => {
|
||||
console.log('Dentro del setTimeout (monolithic), llamando a scrollToPartitionSection');
|
||||
this.scrollToPartitionSection();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
loadExistingImages() {
|
||||
if (!this.selectedExistingImage) return;
|
||||
|
||||
this.loadingExistingImages = true;
|
||||
// Aquí deberías hacer el GET al endpoint externo
|
||||
// Por ahora uso un endpoint de ejemplo, ajusta según tu API
|
||||
const url = `${this.baseUrl}/images?gitImageRepository.id=${this.selectedExistingImage.id}&page=1&itemsPerPage=100`;
|
||||
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
this.existingImages = response['hydra:member'] || [];
|
||||
this.loadingExistingImages = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar las imágenes existentes:', error);
|
||||
this.loadingExistingImages = false;
|
||||
this.toastService.error('Error al cargar las imágenes existentes');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resetGitSelections() {
|
||||
this.selectedGitRepository = null;
|
||||
this.selectedExistingImage = null;
|
||||
this.gitImageName = '';
|
||||
this.gitAction = 'create';
|
||||
this.existingImages = [];
|
||||
this.gitRepositories = [];
|
||||
this.gitImageRepositories = [];
|
||||
|
||||
this.selectedImage = null;
|
||||
this.name = '';
|
||||
this.monolithicAction = 'create';
|
||||
}
|
||||
|
||||
resetCanonicalName() {
|
||||
this.name = this.selectedImage ? this.selectedImage.name : '';
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.loading = true;
|
||||
|
||||
if (!this.selectedPartition) {
|
||||
this.toastService.error('Debes seleccionar una partición');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.imageType === 'git') {
|
||||
if (!this.selectedGitRepository) {
|
||||
this.toastService.error('Debes seleccionar un repositorio Git');
|
||||
return;
|
||||
}
|
||||
if (this.gitAction === 'update' && !this.selectedExistingImage) {
|
||||
this.toastService.error('Debes seleccionar un repositorio de imágenes Git');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.imageType === 'monolithic') {
|
||||
if (this.monolithicAction === 'create' && !this.name) {
|
||||
this.toastService.error('Debes introducir un nombre canónico para la imagen');
|
||||
return;
|
||||
}
|
||||
if (this.monolithicAction === 'update' && !this.selectedImage) {
|
||||
this.toastService.error('Debes seleccionar una imagen para actualizar');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectedImage) {
|
||||
this.toastService.warning('Aviso: Está seleccionando una imagen previamente creada. Se procede a crear un backup de la misma. ');
|
||||
}
|
||||
|
||||
const payload = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
name: this.name,
|
||||
partition: this.selectedPartition['@id'],
|
||||
source: 'assistant',
|
||||
type: this.imageType,
|
||||
selectedImage: this.selectedImage?.['@id']
|
||||
};
|
||||
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
|
||||
width: '400px',
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result !== undefined) {
|
||||
this.loading = true;
|
||||
|
||||
let payload: any = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
partition: this.selectedPartition['@id'],
|
||||
source: 'assistant',
|
||||
type: this.imageType,
|
||||
queue: result
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/images`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Petición de creación de imagen enviada');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
if (this.imageType === 'git') {
|
||||
payload.gitRepository = this.selectedGitRepository.name
|
||||
payload.name = this.selectedGitRepository.name;
|
||||
|
||||
if (this.gitAction === 'create') {
|
||||
payload.action = 'create';
|
||||
} else {
|
||||
payload.action = 'update';
|
||||
}
|
||||
} else {
|
||||
if (this.monolithicAction === 'create') {
|
||||
payload.name = this.name;
|
||||
payload.action = 'create';
|
||||
} else {
|
||||
payload.selectedImage = this.selectedImage['@id'];
|
||||
payload.action = 'update';
|
||||
}
|
||||
}
|
||||
|
||||
this.http.post(`${this.baseUrl}/images`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
let actionText = 'creación';
|
||||
if (this.imageType === 'git' && this.gitAction === 'update') {
|
||||
actionText = 'actualización';
|
||||
} else if (this.imageType === 'monolithic' && this.monolithicAction === 'update') {
|
||||
actionText = 'actualización';
|
||||
}
|
||||
this.toastService.success(`Petición de ${actionText} de imagen enviada`);
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
openCreateRepositoryModal(): void {
|
||||
this.creatingRepository = true;
|
||||
const dialogRef = this.dialog.open(CreateRepositoryModalComponent, {
|
||||
width: '600px',
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
data: {
|
||||
clientRepository: this.selectedRepository
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
this.creatingRepository = false;
|
||||
if (result) {
|
||||
this.loadGitRepositories();
|
||||
setTimeout(() => {
|
||||
const newRepository = this.gitRepositories.find(repo => repo['@id'] === result['@id']);
|
||||
if (newRepository) {
|
||||
this.selectedGitRepository = newRepository;
|
||||
this.onGitRepositorySelected(newRepository);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get imageType(): string {
|
||||
return this._imageType;
|
||||
}
|
||||
|
||||
set imageType(value: string) {
|
||||
this._imageType = value;
|
||||
this.loadImages();
|
||||
if (value === 'git') {
|
||||
this.loadGitRepositories();
|
||||
this.selectedImage = null;
|
||||
this.name = '';
|
||||
this.monolithicAction = 'create';
|
||||
} else {
|
||||
this.resetGitSelections();
|
||||
}
|
||||
}
|
||||
|
||||
onGitImageRepositorySelected(gitImageRepository: any) {
|
||||
this.selectedExistingImage = gitImageRepository;
|
||||
this.existingImages = [];
|
||||
|
||||
if (gitImageRepository) {
|
||||
this.loadExistingImages();
|
||||
}
|
||||
}
|
||||
|
||||
scrollToPartitionSection() {
|
||||
const partitionSection = document.getElementById('partition-selection');
|
||||
|
||||
if (partitionSection) {
|
||||
partitionSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get selectedPartition(): any {
|
||||
return this._selectedPartition;
|
||||
}
|
||||
|
||||
set selectedPartition(value: any) {
|
||||
this._selectedPartition = value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.repository-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
@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,17 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h2 mat-dialog-title>Crear nuevo repositorio de imágenes git</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<form [formGroup]="repositoryForm" (ngSubmit)="save()" class="repository-form">
|
||||
<mat-form-field appearance="fill" class="form-field">
|
||||
<mat-label>Nombre del repositorio</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="save()">Guardar</button>
|
||||
</div>
|
|
@ -0,0 +1,64 @@
|
|||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-repository-modal',
|
||||
templateUrl: './create-repository-modal.component.html',
|
||||
styleUrl: './create-repository-modal.component.css'
|
||||
})
|
||||
export class CreateRepositoryModalComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
repositoryForm: FormGroup<any>;
|
||||
loading: boolean = false;
|
||||
clientRepository: any = null;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<CreateRepositoryModalComponent>,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.clientRepository = this.data?.clientRepository || null;
|
||||
this.repositoryForm = this.fb.group({
|
||||
name: [null, Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// El componente se inicializa
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (this.repositoryForm.valid) {
|
||||
this.loading = true;
|
||||
const payload = {
|
||||
name: this.repositoryForm.value.name,
|
||||
repository: this.clientRepository ? this.clientRepository.id : null
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/git-repositories`, payload).subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Repositorio creado correctamente');
|
||||
this.dialogRef.close(response);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error?.['hydra:description'] || 'Error al crear el repositorio');
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.toastService.error('Por favor, complete todos los campos requeridos');
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
|
@ -1,101 +1,140 @@
|
|||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
background-color: #eaeff6;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
padding: 24px 32px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.option-container {
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.deploy-container {
|
||||
.header-container-title h2 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-container-title h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: 1em;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
padding: 10px;
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Contenedor principal */
|
||||
.select-container {
|
||||
background: white !important;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
/* Secciones del formulario */
|
||||
.form-section {
|
||||
background: white !important;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.form-section-title mat-icon {
|
||||
color: #667eea;
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Selectores */
|
||||
.selector {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
mat-option .unit-name {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1 1 calc(33.33% - 16px);
|
||||
.half-width {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
/* Campos de formulario */
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
::ng-deep .mat-form-field-appearance-fill.mat-focused .mat-form-field-flex {
|
||||
background-color: white;
|
||||
border-color: #2196f3;
|
||||
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
|
||||
}
|
||||
|
||||
/* Grid de clientes */
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
|
@ -103,96 +142,381 @@ mat-option .unit-name {
|
|||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
::ng-deep .custom-tooltip {
|
||||
white-space: pre-line !important;
|
||||
max-width: 200px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
.client-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.selected-client {
|
||||
background-color: #a0c2e5 !important; /* Azul */
|
||||
color: white !important;
|
||||
background: linear-gradient(135deg, #8fa1f0 0%, #9b7bc8 100%);
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.selected-client .client-name,
|
||||
.selected-client .client-ip {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.client-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 0.9em;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 2px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
font-size: 10px;
|
||||
color: #6c757d;
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
margin-bottom: 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Tabla de particiones */
|
||||
.partition-table-container {
|
||||
background-color: #eaeff6;
|
||||
padding: 20px;
|
||||
background: white !important;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::ng-deep .mat-table {
|
||||
background: white;
|
||||
}
|
||||
|
||||
::ng-deep .mat-header-cell {
|
||||
background: white !important;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-cell {
|
||||
padding: 16px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Opciones avanzadas */
|
||||
.input-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Mensajes de error */
|
||||
.error-message {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
||||
color: white;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 16px rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
/* Instrucciones */
|
||||
.instructions-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.instructions-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
::ng-deep .instructions-card .mat-card-title {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
|
||||
::ng-deep .instructions-card .mat-card-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.instructions-card pre {
|
||||
background: white !important;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
overflow-x: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Tooltip personalizado */
|
||||
::ng-deep .custom-tooltip {
|
||||
background: rgba(0, 0, 0, 0.9) !important;
|
||||
color: white !important;
|
||||
padding: 12px !important;
|
||||
border-radius: 8px !important;
|
||||
font-size: 12px !important;
|
||||
max-width: 250px !important;
|
||||
white-space: pre-line !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.header-container {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.selector {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.client-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.destination-badge {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.destination-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.destination-value {
|
||||
max-width: 150px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.destination-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilos para elementos específicos */
|
||||
.unit-name {
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Eliminar sombra de la tabla */
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Estilos para el expansion panel */
|
||||
::ng-deep .mat-expansion-panel {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
|
||||
border-radius: 12px !important;
|
||||
margin-bottom: 20px;
|
||||
background: #f7fbff !important;
|
||||
border: 1px solid #bbdefb !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-panel-header {
|
||||
padding: 20px 24px !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-panel-header-title {
|
||||
font-weight: 600 !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-panel-header-description {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
/* Otros estilos */
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.disabled-client {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #de2323;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-top: 20px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.instructions-textarea textarea {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Estilo para hacer el backdrop no clickeable */
|
||||
::ng-deep .non-clickable-backdrop {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Estilos modernos para el badge de destino */
|
||||
.destination-info {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.destination-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #bbdefb;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.destination-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.destination-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.destination-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #1976d2;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.destination-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #0d47a1;
|
||||
}
|
||||
|
||||
.filters-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.git-gap {
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.monolithic-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.monolithic-row .half-width {
|
||||
flex: 1 1 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.monolithic-row .full-width {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.monolithic-row .full-width {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -5,24 +5,37 @@
|
|||
<h2>
|
||||
{{ 'deployImage' | translate }}
|
||||
</h2>
|
||||
<h4>
|
||||
{{ runScriptTitle }}
|
||||
</h4>
|
||||
<div class="destination-info">
|
||||
<div class="destination-badge">
|
||||
<mat-icon class="destination-icon">cloud_download</mat-icon>
|
||||
<div class="destination-content">
|
||||
<span class="destination-label">Destino</span>
|
||||
<span class="destination-value">{{ runScriptTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="action-button" [disabled]="!allSelected || !selectedModelClient || !selectedImage || !selectedMethod || !selectedPartition" (click)="save()">Ejecutar</button>
|
||||
<button class="action-button" id="execute-button"
|
||||
[disabled]="!isFormValid()"
|
||||
(click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button mat-stroked-button color="accent" [disabled]="!allSelected || !selectedModelClient || !selectedImage || !selectedMethod || !selectedPartition" (click)="openScheduleModal()">
|
||||
<mat-icon>schedule</mat-icon> Opciones de programación
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="generateOgInstructions()">
|
||||
Generar instrucciones
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" color="accent"
|
||||
[disabled]="!isFormValid()"
|
||||
(click)="openScheduleModal()">
|
||||
Opciones de programación
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
|
@ -43,7 +56,7 @@
|
|||
<div *ngFor="let client of clientData" 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'}"
|
||||
[ngClass]="{'selected-client': client.selected}"
|
||||
[matTooltip]="getPartitionsTooltip(client)"
|
||||
matTooltipPosition="above"
|
||||
matTooltipClass="custom-tooltip">
|
||||
|
@ -78,84 +91,183 @@
|
|||
<mat-divider style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<div class="deploy-container">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Tipo de imagen</mat-label>
|
||||
<mat-select [(ngModel)]="imageType" (selectionChange)="onImageTypeSelected($event.value)">
|
||||
<mat-option [value]="'monolithic'">Monolítica</mat-option>
|
||||
<!--
|
||||
<mat-option [value]="'git'">Git</mat-option>
|
||||
-->
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- Sección: Configuración de imagen -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>image</mat-icon>
|
||||
Configuración de imagen
|
||||
</div>
|
||||
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Tipo de imagen</mat-label>
|
||||
<mat-select [(ngModel)]="imageType" (selectionChange)="onImageTypeSelected($event.value)">
|
||||
<mat-option [value]="'monolithic'">Monolítica</mat-option>
|
||||
<mat-option [value]="'git'">Git</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Selectores Git (solo si imageType === 'git') -->
|
||||
<div *ngIf="imageType === 'git'" class="filters-row git-gap">
|
||||
<mat-form-field appearance="fill" style="width: 300px;">
|
||||
<mat-label>Seleccionar Repositorio</mat-label>
|
||||
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositoryChange($event.value)" [disabled]="loadingRepositories">
|
||||
<mat-option *ngFor="let repo of repositories" [value]="repo">
|
||||
{{ repo }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingRepositories">hourglass_empty</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" style="width: 300px;">
|
||||
<mat-label>Seleccionar Rama</mat-label>
|
||||
<mat-select [(ngModel)]="selectedBranch" (selectionChange)="onGitBranchChange()" [disabled]="loadingBranches || !selectedRepository">
|
||||
<mat-option *ngFor="let branch of branches" [value]="branch">
|
||||
{{ branch }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingBranches">hourglass_empty</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de commits (solo si imageType === 'git') -->
|
||||
<div *ngIf="imageType === 'git' && commits.length > 0" class="commits-table-container">
|
||||
<table mat-table [dataSource]="commits" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>Seleccionar</th>
|
||||
<td mat-cell *matCellDef="let commit">
|
||||
<mat-radio-group [(ngModel)]="selectedCommit" name="selectedCommit">
|
||||
<mat-radio-button [value]="commit"></mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="hexsha">
|
||||
<th mat-header-cell *matHeaderCellDef>Commit ID</th>
|
||||
<td mat-cell *matCellDef="let commit">
|
||||
<code style="background-color: #f5f5f5; padding: 2px 4px; border-radius: 3px; font-family: monospace;">
|
||||
{{ commit.hexsha }}
|
||||
</code>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="message">
|
||||
<th mat-header-cell *matHeaderCellDef>Mensaje</th>
|
||||
<td mat-cell *matCellDef="let commit">{{ commit.message }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="committed_date">
|
||||
<th mat-header-cell *matHeaderCellDef>Fecha</th>
|
||||
<td mat-cell *matCellDef="let commit">{{ commit.committed_date * 1000 | date:'dd/MM/yyyy HH:mm:ss' }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="tags">
|
||||
<th mat-header-cell *matHeaderCellDef>Tags</th>
|
||||
<td mat-cell *matCellDef="let commit">
|
||||
<mat-chip-list>
|
||||
<mat-chip *ngFor="let tag of commit.tags" color="primary" selected>{{ tag }}</mat-chip>
|
||||
<span *ngIf="!commit.tags || commit.tags.length === 0" style="color: #999; font-style: italic;">Sin tags</span>
|
||||
</mat-chip-list>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="['select','hexsha','message','committed_date','tags']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['select','hexsha','message','committed_date','tags'];"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Selector de método y de imagen solo si imageType === 'monolithic' -->
|
||||
<div class="monolithic-row" *ngIf="imageType === 'monolithic'">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Seleccione método de deploy</mat-label>
|
||||
<mat-select [(ngModel)]="selectedMethod" name="selectedMethod" (selectionChange)="validateImageSize()">
|
||||
<mat-option *ngFor="let method of allMethods" [value]="method.value">{{ method.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" [disabled] = "!imageType" >
|
||||
<mat-option *ngFor="let image of images" [value]="image">
|
||||
<div class="unit-name"> {{ image.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ image.description }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deploy-container">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" [disabled] = "!imageType" >
|
||||
<mat-option *ngFor="let image of images" [value]="image">
|
||||
<div class="unit-name"> {{ image.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ image.description }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- Sección: Selección de partición -->
|
||||
<div class="form-section" id="partition-selection">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>storage</mat-icon>
|
||||
Selección de partición
|
||||
</div>
|
||||
|
||||
<div class="partition-table-container">
|
||||
<div *ngIf="showInstructions" class="instructions-box">
|
||||
<mat-card class="instructions-card">
|
||||
<mat-card-title>
|
||||
Instrucciones generadas
|
||||
<button mat-icon-button (click)="showInstructions = false" style="float: right;">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<pre>{{ ogInstructions }}</pre>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione método de deploy</mat-label>
|
||||
<mat-select [(ngModel)]="selectedMethod" name="selectedMethod" (selectionChange)="validateImageSize()">
|
||||
<mat-option *ngFor="let method of allMethods" [value]="method.value">{{ method.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<table mat-table [dataSource]="filteredPartitions">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: start">Seleccionar partición</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group [(ngModel)]="selectedPartition" name="selectedPartition">
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
{{ column.cell(image) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<div class="partition-table-container">
|
||||
<table mat-table [dataSource]="filteredPartitions" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: start">Seleccionar partición</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group [(ngModel)]="selectedPartition" name="selectedPartition" (change)="validateImageSize()">
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
{{ column.cell(image) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
<div class="options-container">
|
||||
<h3 *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')" class="input-group">Opciones multicast</h3>
|
||||
<h3 *ngIf="isMethod('p2p')" class="input-group">Opciones torrent</h3>
|
||||
<div *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')" class="input-group">
|
||||
<!-- Sección: Opciones avanzadas -->
|
||||
<div class="form-section" id="advanced-options" *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct') || isMethod('p2p')">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">Opciones multicast</span>
|
||||
<span *ngIf="isMethod('p2p')">Opciones torrent</span>
|
||||
</div>
|
||||
|
||||
<div class="input-group" *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
<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,37 +275,50 @@
|
|||
|
||||
<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">
|
||||
<div class="input-group" *ngIf="isMethod('p2p')">
|
||||
<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>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Semilla</mat-label>
|
||||
<input matInput [(ngModel)]="p2pTime" name="p2pTime" type="number">
|
||||
<mat-form-field appearance="fill" class="input-field" *ngIf="p2pMode === 'seeder'">
|
||||
<mat-label>Semilla (minutos)</mat-label>
|
||||
<input matInput [(ngModel)]="p2pTime" name="p2pTime" type="number"
|
||||
[required]="isMethod('p2p') && p2pMode === 'seeder'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<app-scroll-to-top
|
||||
[threshold]="100"
|
||||
targetElement=".header-container"
|
||||
position="bottom-right"
|
||||
[showTooltip]="true"
|
||||
tooltipText="Volver arriba"
|
||||
tooltipPosition="left">
|
||||
</app-scroll-to-top>
|
||||
|
||||
|
||||
|
|
|
@ -15,10 +15,12 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { ActivatedRoute, provideRouter } from '@angular/router';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatExpansionModule } from "@angular/material/expansion";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { MatTooltipModule } from "@angular/material/tooltip";
|
||||
import { LoadingComponent } from "../../../../../shared/loading/loading.component";
|
||||
import { ScrollToTopComponent } from "../../../../../shared/scroll-to-top/scroll-to-top.component";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('DeployImageComponent', () => {
|
||||
|
@ -32,7 +34,7 @@ describe('DeployImageComponent', () => {
|
|||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DeployImageComponent, LoadingComponent],
|
||||
declarations: [DeployImageComponent, LoadingComponent, ScrollToTopComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
|
@ -46,6 +48,8 @@ describe('DeployImageComponent', () => {
|
|||
MatDividerModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
BrowserAnimationsModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
|
@ -73,8 +77,7 @@ describe('DeployImageComponent', () => {
|
|||
}
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||
import { ConfigService } from '@services/config.service';
|
||||
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-deploy-image',
|
||||
|
@ -23,7 +24,7 @@ export class DeployImageComponent implements OnInit{
|
|||
images: any[] = [];
|
||||
selectedImage: any = null;
|
||||
selectedMethod: string | null = null;
|
||||
selectedPartition: any = null;
|
||||
private _selectedPartition: any = null;
|
||||
mcastIp: string = '';
|
||||
mcastPort: Number = 0;
|
||||
mcastMode: string = '';
|
||||
|
@ -37,6 +38,11 @@ export class DeployImageComponent implements OnInit{
|
|||
loading: boolean = false;
|
||||
allSelected = true;
|
||||
runScriptContext: any = null;
|
||||
ogInstructions: string = '';
|
||||
deployImage: boolean = true;
|
||||
showInstructions: boolean = false;
|
||||
loadingCommits: boolean = false;
|
||||
selectedGitRepository: string = '';
|
||||
|
||||
protected p2pModeOptions = [
|
||||
{ name: 'Leecher', value: 'leecher' },
|
||||
|
@ -94,6 +100,15 @@ export class DeployImageComponent implements OnInit{
|
|||
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
|
||||
selection = new SelectionModel(true, []);
|
||||
|
||||
repositories: string[] = [];
|
||||
loadingRepositories: boolean = false;
|
||||
branches: string[] = [];
|
||||
selectedBranch: string = '';
|
||||
loadingBranches: boolean = false;
|
||||
commits: any[] = [];
|
||||
selectedCommit: any = null;
|
||||
private initialGitLoad = true;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
|
@ -112,18 +127,11 @@ export class DeployImageComponent implements OnInit{
|
|||
}
|
||||
});
|
||||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string}) => {
|
||||
if (client.status === 'og-live') {
|
||||
client.selected = true;
|
||||
}
|
||||
});
|
||||
this.selectedClients = this.clientData.filter(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
);
|
||||
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
|
||||
|
||||
this.selectedModelClient = this.clientData.find(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
) || null;
|
||||
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
|
||||
|
||||
this.selectedModelClient = this.clientData.find((client: { selected: boolean; status: string}) => client.selected) || null;
|
||||
|
||||
if (this.selectedModelClient) {
|
||||
this.loadPartitions(this.selectedModelClient);
|
||||
|
@ -138,7 +146,14 @@ export class DeployImageComponent implements OnInit{
|
|||
|
||||
onImageTypeSelected(event: any) {
|
||||
this.imageType = event;
|
||||
if (event === 'git') {
|
||||
this.selectedMethod = null;
|
||||
this.loadGitRepositories();
|
||||
}
|
||||
this.loadImages();
|
||||
setTimeout(() => {
|
||||
this.scrollToPartitionSection();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
get runScriptTitle(): string {
|
||||
|
@ -208,7 +223,7 @@ export class DeployImageComponent implements OnInit{
|
|||
this.loadImages();
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los datos completos del cliente:', error);
|
||||
console.error('Error al cargar las particiones:', error);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
@ -220,11 +235,7 @@ export class DeployImageComponent implements OnInit{
|
|||
|
||||
toggleSelectAll() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === "og-live") {
|
||||
client.selected = this.allSelected;
|
||||
}
|
||||
});
|
||||
this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; });
|
||||
}
|
||||
|
||||
loadImages() {
|
||||
|
@ -262,74 +273,147 @@ export class DeployImageComponent implements OnInit{
|
|||
}
|
||||
}
|
||||
this.errorMessage = "";
|
||||
|
||||
if (this.selectedMethod) {
|
||||
setTimeout(() => {
|
||||
this.scrollToPartitionSection();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.loading = true;
|
||||
|
||||
if (!this.selectedClients.length) {
|
||||
this.toastService.error('Debe seleccionar al menos un cliente');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selectedImage) {
|
||||
if (!this.selectedImage && this.imageType !== 'git') {
|
||||
this.toastService.error('Debe seleccionar una imagen');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selectedMethod) {
|
||||
if (!this.selectedMethod && this.imageType !== 'git') {
|
||||
this.toastService.error('Debe seleccionar un método');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selectedPartition) {
|
||||
this.toastService.error('Debe seleccionar una partición');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.imageType === 'git' && !this.selectedCommit) {
|
||||
this.toastService.error('Debe seleccionar un commit');
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
|
||||
width: '400px',
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop'
|
||||
});
|
||||
|
||||
this.toastService.info('Preparando petición de despliegue');
|
||||
|
||||
const payload = {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
method: this.selectedMethod,
|
||||
// partition: this.selectedPartition['@id'],
|
||||
diskNumber: this.selectedPartition.diskNumber,
|
||||
partitionNumber: this.selectedPartition.partitionNumber,
|
||||
p2pMode: this.p2pMode,
|
||||
p2pTime: this.p2pTime,
|
||||
mcastIp: this.mcastIp,
|
||||
mcastPort: this.mcastPort,
|
||||
mcastMode: this.mcastMode,
|
||||
mcastSpeed: this.mcastSpeed,
|
||||
maxTime: this.mcastMaxTime,
|
||||
maxClients: this.mcastMaxClients,
|
||||
};
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result !== undefined) {
|
||||
this.loading = true;
|
||||
|
||||
let payload: any;
|
||||
let url: string;
|
||||
|
||||
this.http.post(`${this.baseUrl}/image-image-repositories/${this.selectedImage.uuid}/deploy-image`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Petición de despliegue enviada correctamente');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description'], 'Se ha detectado un error en el despliegue de imágenes.', {
|
||||
"closeButton": true,
|
||||
"newestOnTop": false,
|
||||
"progressBar": false,
|
||||
"positionClass": "toast-bottom-right",
|
||||
"timeOut": 0,
|
||||
"extendedTimeOut": 0,
|
||||
"tapToDismiss": false
|
||||
});
|
||||
this.loading = false;
|
||||
if (this.imageType === 'git') {
|
||||
payload = {
|
||||
type: 'git',
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
diskNumber: this.selectedPartition.diskNumber,
|
||||
partitionNumber: this.selectedPartition.partitionNumber,
|
||||
repositoryName: this.selectedGitRepository,
|
||||
branch: this.selectedBranch,
|
||||
hexsha: this.selectedCommit.hexsha,
|
||||
queue: result
|
||||
};
|
||||
url = `${this.baseUrl}/git-repositories/deploy-image`;
|
||||
} else {
|
||||
payload = {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
method: this.selectedMethod,
|
||||
diskNumber: this.selectedPartition.diskNumber,
|
||||
partitionNumber: this.selectedPartition.partitionNumber,
|
||||
p2pMode: this.p2pMode,
|
||||
p2pTime: this.p2pMode === 'seeder' ? this.p2pTime : 0,
|
||||
mcastIp: this.mcastIp,
|
||||
mcastPort: this.mcastPort,
|
||||
mcastMode: this.mcastMode,
|
||||
mcastSpeed: this.mcastSpeed,
|
||||
maxTime: this.mcastMaxTime,
|
||||
maxClients: this.mcastMaxClients,
|
||||
type: this.imageType,
|
||||
queue: result
|
||||
};
|
||||
url = `${this.baseUrl}/image-image-repositories/${this.selectedImage.uuid}/deploy-image`;
|
||||
}
|
||||
});
|
||||
|
||||
this.http.post(url, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Petición de despliegue enviada correctamente');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description'], 'Se ha detectado un error en el despliegue de imágenes.', {
|
||||
"closeButton": true,
|
||||
"newestOnTop": false,
|
||||
"progressBar": false,
|
||||
"positionClass": "toast-bottom-right",
|
||||
"timeOut": 0,
|
||||
"extendedTimeOut": 0,
|
||||
"tapToDismiss": false
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isFormValid(): boolean {
|
||||
if (!this.allSelected || !this.selectedModelClient || !this.selectedPartition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.imageType === 'git') {
|
||||
if (!this.selectedCommit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.imageType !== 'git' && !this.selectedMethod) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.imageType !== 'git') {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
if (this.p2pMode === 'seeder' && !this.p2pTime) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
@ -338,7 +422,8 @@ export class DeployImageComponent implements OnInit{
|
|||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id']
|
||||
organizationalUnit: this.runScriptContext['@id'],
|
||||
source: 'assistant'
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -348,20 +433,20 @@ export class DeployImageComponent implements OnInit{
|
|||
method: this.selectedMethod,
|
||||
diskNumber: this.selectedPartition.diskNumber,
|
||||
partitionNumber: this.selectedPartition.partitionNumber,
|
||||
p2pMode: this.selectedMethod === 'torrent' ? this.p2pMode : null,
|
||||
p2pTime: this.selectedMethod === 'torrent' ? this.p2pTime : null,
|
||||
mcastIp: this.selectedMethod === 'multicast' ? this.mcastIp : null,
|
||||
mcastPort: this.selectedMethod === 'multicast' ? this.mcastPort : null,
|
||||
mcastMode: this.selectedMethod === 'multicast' ? this.mcastMode : null,
|
||||
mcastSpeed: this.selectedMethod === 'multicast' ? this.mcastSpeed : null,
|
||||
maxTime: this.selectedMethod === 'multicast' ? this.mcastMaxTime : null,
|
||||
maxClients: this.selectedMethod === 'multicast' ? this.mcastMaxClients : null,
|
||||
p2pMode: this.selectedMethod === 'p2p' ? this.p2pMode : null,
|
||||
p2pTime: this.selectedMethod === 'p2p' && this.p2pMode === 'seeder' ? this.p2pTime : null,
|
||||
mcastIp: this.selectedMethod === 'udpcast' ? this.mcastIp : null,
|
||||
mcastPort: this.selectedMethod === 'udpcast' ? this.mcastPort : null,
|
||||
mcastMode: this.selectedMethod === 'udpcast' ? this.mcastMode : null,
|
||||
mcastSpeed: this.selectedMethod === 'udpcast' ? this.mcastSpeed : null,
|
||||
maxTime: this.selectedMethod === 'udpcast' ? this.mcastMaxTime : null,
|
||||
maxClients: this.selectedMethod === 'udpcast' ? this.mcastMaxClients : null,
|
||||
};
|
||||
|
||||
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: () => {
|
||||
|
@ -374,4 +459,110 @@ export class DeployImageComponent implements OnInit{
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateOgInstructions() {
|
||||
this.showInstructions = true;
|
||||
this.ogInstructions = `og-deploy-image --image ${this.selectedImage.name} --partition ${this.selectedPartition.partitionNumber} --method ${this.selectedMethod}`;
|
||||
}
|
||||
|
||||
scrollToPartitionSection() {
|
||||
const partitionSection = document.getElementById('partition-selection');
|
||||
if (partitionSection) {
|
||||
partitionSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
console.log('Scroll ejecutado');
|
||||
}
|
||||
}
|
||||
|
||||
scrollToAdvancedOptions() {
|
||||
const advancedOptions = document.getElementById('advanced-options');
|
||||
|
||||
if (advancedOptions) {
|
||||
advancedOptions.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
console.log('Scroll hacia opciones avanzadas ejecutado');
|
||||
}
|
||||
}
|
||||
|
||||
get selectedPartition(): any {
|
||||
return this._selectedPartition;
|
||||
}
|
||||
|
||||
set selectedPartition(value: any) {
|
||||
this._selectedPartition = value;
|
||||
}
|
||||
|
||||
loadGitRepositories() {
|
||||
this.loadingRepositories = true;
|
||||
this.http.get<any>(`${this.baseUrl}/image-repositories/server/git/${this.selectedRepository?.uuid}/get-collection`).subscribe(
|
||||
data => {
|
||||
this.repositories = data.repositories || [];
|
||||
this.loadingRepositories = false;
|
||||
if (this.repositories.length > 0) {
|
||||
this.selectedGitRepository = this.repositories[0];
|
||||
this.loadGitBranches();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error al cargar los repositorios git');
|
||||
this.loadingRepositories = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onGitRepositoryChange(event: any) {
|
||||
this.selectedGitRepository = event;
|
||||
this.selectedBranch = '';
|
||||
this.branches = [];
|
||||
this.commits = [];
|
||||
this.selectedCommit = null;
|
||||
this.loadGitBranches();
|
||||
}
|
||||
|
||||
loadGitBranches() {
|
||||
if (!this.selectedGitRepository) return;
|
||||
this.loadingBranches = true;
|
||||
this.http.post<any>(`${this.baseUrl}/image-repositories/server/git/${this.selectedRepository?.uuid}/branches`, { repositoryName: this.selectedGitRepository }).subscribe(
|
||||
data => {
|
||||
this.branches = data.branches || [];
|
||||
this.loadingBranches = false;
|
||||
if (this.branches.length > 0) {
|
||||
this.selectedBranch = this.branches[0];
|
||||
this.loadGitCommits();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error al cargar las ramas');
|
||||
this.loadingBranches = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onGitBranchChange() {
|
||||
this.selectedCommit = null;
|
||||
this.commits = [];
|
||||
this.loadGitCommits();
|
||||
}
|
||||
|
||||
loadGitCommits() {
|
||||
if (!this.selectedGitRepository || !this.selectedBranch) return;
|
||||
this.loadingCommits = true;
|
||||
this.http.post<any>(`${this.baseUrl}/image-repositories/server/git/${this.selectedRepository?.uuid}/commits`, {
|
||||
repositoryName: this.selectedGitRepository,
|
||||
branch: this.selectedBranch
|
||||
}).subscribe(
|
||||
data => {
|
||||
this.commits = data.commits || [];
|
||||
this.loadingCommits = false;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error al cargar los commits');
|
||||
this.loadingCommits = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,17 +5,29 @@
|
|||
<h2>
|
||||
{{ 'partitionTitle' | translate }}
|
||||
</h2>
|
||||
<h4>
|
||||
{{ runScriptTitle }}
|
||||
</h4>
|
||||
<div class="destination-info">
|
||||
<div class="destination-badge">
|
||||
<mat-icon class="destination-icon">cloud_download</mat-icon>
|
||||
<div class="destination-content">
|
||||
<span class="destination-label">Destino</span>
|
||||
<span class="destination-value">{{ runScriptTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="action-button" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="save()">Ejecutar</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
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="generateInstructions()">
|
||||
Generar instrucciones
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" color="accent" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="openScheduleModal()">
|
||||
Opciones de programación
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,7 +52,7 @@
|
|||
<div *ngFor="let client of clientData" 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'}"
|
||||
[ngClass]="{'selected-client': client.selected}"
|
||||
[matTooltip]="getPartitionsTooltip(client)"
|
||||
matTooltipPosition="above"
|
||||
matTooltipClass="custom-tooltip">
|
||||
|
@ -71,51 +83,131 @@
|
|||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="disk-select">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Seleccionar disco</mat-label>
|
||||
<mat-select [(ngModel)]="selectedDiskNumber">
|
||||
<mat-option *ngFor="let disk of disks" [value]="disk.diskNumber">
|
||||
Disco {{ disk.diskNumber }} ({{ (disk.totalDiskSize / 1024).toFixed(2) }} GB)
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="disk-selector-card">
|
||||
<div class="card-header">
|
||||
<mat-icon class="card-icon">storage</mat-icon>
|
||||
<div class="card-title">
|
||||
<h3>Selección de Disco</h3>
|
||||
<p>Elige el disco donde se realizarán las operaciones de particionado</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<mat-form-field appearance="fill" class="disk-select-field">
|
||||
<mat-label>Seleccionar disco</mat-label>
|
||||
<mat-select [(ngModel)]="selectedDiskNumber" (selectionChange)="onDiskSelectionChange()">
|
||||
<mat-option *ngFor="let disk of disks" [value]="disk.diskNumber">
|
||||
<div class="disk-option">
|
||||
<div class="disk-info">
|
||||
<span class="disk-name">Disco {{ disk.diskNumber }}</span>
|
||||
<span class="disk-size"> {{ (disk.totalDiskSize / 1024).toFixed(2) }} GB</span>
|
||||
</div>
|
||||
<div class="disk-details">
|
||||
<span class="usage-percent"> {{ (disk.percentage || 0).toFixed(1) }}% usado</span>
|
||||
</div>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-hint>Selecciona el disco que deseas particionar</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="selection-info" *ngIf="selectedDisk">
|
||||
<mat-icon class="info-icon">info</mat-icon>
|
||||
<div class="info-text">
|
||||
<span class="info-title">Disco seleccionado: {{ selectedDisk.diskNumber }}</span>
|
||||
<span class="info-subtitle">Tamaño total: {{ (selectedDisk.totalDiskSize / 1024).toFixed(2) }} GB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="no-disks-message" *ngIf="!disks || disks.length === 0">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<div class="message-text">
|
||||
<span class="message-title">No hay discos disponibles</span>
|
||||
<span class="message-subtitle">Asegúrate de que el cliente modelo tenga discos configurados</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="partition-assistant" *ngIf="selectedDisk">
|
||||
<div *ngIf="showInstructions" class="instructions-box">
|
||||
<mat-card class="instructions-card">
|
||||
<mat-card-title>
|
||||
Instrucciones generadas
|
||||
<button mat-icon-button (click)="showInstructions = false" style="float: right;">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<pre>{{ generatedInstructions }}</pre>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</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">
|
||||
Firmware: {{ selectedModelClient.firmwareType }}
|
||||
</mat-chip>
|
||||
<mat-chip color="info" *ngIf="partitionCode">
|
||||
Tabla de particiones: {{ partitionCode }}
|
||||
</mat-chip>
|
||||
|
||||
|
||||
<div class="info-badge" *ngIf="selectedModelClient.firmwareType">
|
||||
<mat-icon class="info-icon">memory</mat-icon>
|
||||
<div class="info-content">
|
||||
<span class="info-label">Firmware</span>
|
||||
<span class="info-value">{{ selectedModelClient.firmwareType }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-badge" *ngIf="partitionCode">
|
||||
<mat-icon class="info-icon">storage</mat-icon>
|
||||
<div class="info-content">
|
||||
<span class="info-label">Tabla de particiones</span>
|
||||
<span class="info-value">{{ partitionCode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider style="padding: 10px;"></mat-divider>
|
||||
|
||||
<div class="disk-space-info-container">
|
||||
<div class="disk-space-info" [ngClass]="selectedDisk.used < selectedDisk.totalDiskSize ? 'chip-free' : 'chip-full'">
|
||||
Espacio usado: {{ selectedDisk.used | number:'1.2-2' }} MB
|
||||
</div>
|
||||
|
||||
<div class="disk-space-info" [ngClass]="selectedDisk.used < selectedDisk.totalDiskSize ? 'chip-free' : 'chip-full'">
|
||||
Espacio libre: {{ (selectedDisk.totalDiskSize - selectedDisk.used) | number:'1.2-2' }} MB
|
||||
</div>
|
||||
|
||||
<div class="disk-space-info">
|
||||
Espacio total: {{ selectedDisk.totalDiskSize | number:'1.2-2' }} MB
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-container">
|
||||
<table class="partition-table">
|
||||
<div class="disk-space-info-container" id="disk-info">
|
||||
<div class="disk-space-card">
|
||||
<div class="space-info-item">
|
||||
<mat-icon class="space-icon used-icon">storage</mat-icon>
|
||||
<div class="space-details">
|
||||
<span class="space-label">Espacio usado</span>
|
||||
<span class="space-value">{{ selectedDisk.used | number:'1.2-2' }} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-info-item">
|
||||
<mat-icon class="space-icon free-icon" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">cloud_done</mat-icon>
|
||||
<div class="space-details">
|
||||
<span class="space-label">Espacio libre</span>
|
||||
<span class="space-value" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">{{ (selectedDisk.totalDiskSize - selectedDisk.used) | number:'1.2-2' }} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-info-item">
|
||||
<mat-icon class="space-icon total-icon">dns</mat-icon>
|
||||
<div class="space-details">
|
||||
<span class="space-label">Espacio total</span>
|
||||
<span class="space-value">{{ selectedDisk.totalDiskSize | number:'1.2-2' }} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="disk-usage-bar">
|
||||
<div class="usage-bar-container">
|
||||
<div class="usage-bar-fill"
|
||||
[style.width.%]="(selectedDisk.used / selectedDisk.totalDiskSize) * 100"
|
||||
[ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}"></div>
|
||||
</div>
|
||||
<span class="usage-percentage" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">{{ ((selectedDisk.used / selectedDisk.totalDiskSize) * 100) | number:'1.1-1' }}% usado</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="partition-table" id="partition-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Partición</th>
|
||||
|
@ -163,11 +255,29 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<ngx-charts-pie-chart [view]="view" [results]="selectedDisk.chartData" [doughnut]="true">
|
||||
|
||||
<div class="chart-container" *ngIf="selectedDisk">
|
||||
<div class="chart-header">
|
||||
<h3>Distribución de Particiones</h3>
|
||||
</div>
|
||||
<ngx-charts-pie-chart
|
||||
[results]="selectedDisk.chartData"
|
||||
[doughnut]="true"
|
||||
[gradient]="true"
|
||||
[labels]="true"
|
||||
[tooltipDisabled]="false"
|
||||
[animations]="true">
|
||||
</ngx-charts-pie-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<app-scroll-to-top
|
||||
[threshold]="200"
|
||||
targetElement=".header-container"
|
||||
position="bottom-right"
|
||||
[showTooltip]="true"
|
||||
tooltipText="Volver arriba"
|
||||
tooltipPosition="left">
|
||||
</app-scroll-to-top>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { FILESYSTEM_TYPES } from '../../../../../shared/constants/filesystem-typ
|
|||
import { ConfigService } from '@services/config.service';
|
||||
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
||||
|
||||
interface Partition {
|
||||
uuid?: string;
|
||||
|
@ -44,14 +45,16 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
clientData: any = [];
|
||||
loading: boolean = false;
|
||||
runScriptContext: any = null;
|
||||
showInstructions = false;
|
||||
|
||||
view: [number, number] = [400, 300];
|
||||
view: [number, number] = [300, 200];
|
||||
showLegend = true;
|
||||
showLabels = true;
|
||||
allSelected = true;
|
||||
selectedClients: any[] = [];
|
||||
selectedModelClient: any = null;
|
||||
partitionCode: string = '';
|
||||
generatedInstructions: string = '';
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
|
@ -72,19 +75,11 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
}
|
||||
});
|
||||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string}) => {
|
||||
if (client.status === 'og-live') {
|
||||
client.selected = true;
|
||||
}
|
||||
});
|
||||
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
|
||||
|
||||
this.selectedClients = this.clientData.filter(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
);
|
||||
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
|
||||
|
||||
this.selectedModelClient = this.clientData.find(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
) || null;
|
||||
this.selectedModelClient = this.clientData.find((client: { selected: boolean; status: string}) => client.selected) || null;
|
||||
|
||||
if (this.selectedModelClient) {
|
||||
this.loadPartitions(this.selectedModelClient);
|
||||
|
@ -134,15 +129,18 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
|
||||
toggleSelectAll() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === "og-live") {
|
||||
client.selected = this.allSelected;
|
||||
}
|
||||
});
|
||||
this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; });
|
||||
}
|
||||
|
||||
initializeDisks() {
|
||||
this.disks = [];
|
||||
|
||||
// Verificar que hay datos válidos
|
||||
if (!this.data || !this.data.partitions || !Array.isArray(this.data.partitions)) {
|
||||
console.warn('No hay datos de particiones válidos');
|
||||
return;
|
||||
}
|
||||
|
||||
const partitionsFromData = this.data.partitions;
|
||||
this.originalPartitions = JSON.parse(JSON.stringify(partitionsFromData));
|
||||
|
||||
|
@ -318,56 +316,63 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const totalPartitionSize = this.selectedDisk.partitions
|
||||
.filter((partition: any) => !partition.removed)
|
||||
.reduce((sum: any, partition: any) => sum + partition.size, 0);
|
||||
|
||||
if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
|
||||
this.toastService.error('El tamaño total de las particiones en el disco seleccionado excede el tamaño total del disco.');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const modifiedPartitions = this.selectedDisk.partitions.filter((partition: { removed: any; format: any; }) => !partition.removed || partition.format);
|
||||
|
||||
if (modifiedPartitions.length === 0) {
|
||||
this.loading = false;
|
||||
this.toastService.info('No hay cambios para guardar en el disco seleccionado.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newPartitions = modifiedPartitions.map((partition: { partitionNumber: any; memoryUsage: any; size: any; partitionCode: any; filesystem: any; uuid: any; removed: any; format: any; }) => ({
|
||||
diskNumber: this.selectedDisk.diskNumber,
|
||||
partitionNumber: partition.partitionNumber,
|
||||
memoryUsage: partition.memoryUsage,
|
||||
size: partition.size,
|
||||
partitionCode: partition.partitionCode,
|
||||
filesystem: partition.filesystem,
|
||||
uuid: partition.uuid,
|
||||
removed: partition.removed || false,
|
||||
format: partition.format || false,
|
||||
}));
|
||||
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
|
||||
width: '400px',
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop'
|
||||
});
|
||||
|
||||
if (newPartitions.length > 0) {
|
||||
const bulkPayload = {
|
||||
partitions: newPartitions,
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
};
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result !== undefined) {
|
||||
this.loading = true;
|
||||
const newPartitions = modifiedPartitions.map((partition: { partitionNumber: any; memoryUsage: any; size: any; partitionCode: any; filesystem: any; uuid: any; removed: any; format: any; }) => ({
|
||||
diskNumber: this.selectedDisk.diskNumber,
|
||||
partitionNumber: partition.partitionNumber,
|
||||
memoryUsage: partition.memoryUsage,
|
||||
size: partition.size,
|
||||
partitionCode: partition.partitionCode,
|
||||
filesystem: partition.filesystem,
|
||||
uuid: partition.uuid,
|
||||
removed: partition.removed || false,
|
||||
format: partition.format || false,
|
||||
}));
|
||||
|
||||
this.http.post(this.apiUrl, bulkPayload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
(error) => {
|
||||
this.loading = false;
|
||||
this.toastService.error('Error al crear las particiones.');
|
||||
}
|
||||
);
|
||||
}
|
||||
const bulkPayload = {
|
||||
partitions: newPartitions,
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
queue: result
|
||||
};
|
||||
|
||||
this.http.post(this.apiUrl, bulkPayload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
(error) => {
|
||||
this.loading = false;
|
||||
this.toastService.error('Error al crear las particiones.');
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -407,11 +412,20 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
|
||||
|
||||
generateChartData(partitions: Partition[]): any[] {
|
||||
return partitions.map((partition) => ({
|
||||
name: `Partición ${partition.partitionNumber}`,
|
||||
value: partition.percentage,
|
||||
color: partition.color
|
||||
}));
|
||||
const colors = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
|
||||
'#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D7BDE2'
|
||||
];
|
||||
|
||||
return partitions
|
||||
.filter(partition => !partition.removed)
|
||||
.map((partition, index) => ({
|
||||
name: `Partición ${partition.partitionNumber}`,
|
||||
value: partition.size,
|
||||
color: colors[index % colors.length],
|
||||
partition: partition
|
||||
}));
|
||||
}
|
||||
|
||||
updateDiskChart(disk: any) {
|
||||
|
@ -426,7 +440,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 +488,52 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateInstructions(): void {
|
||||
this.showInstructions = true;
|
||||
this.generatedInstructions = `og-partition --disk ${this.selectedDiskNumber} --partitions ${this.selectedDisk.partitions.map((p: Partition) => `${p.partitionNumber}:${p.size}:${p.partitionCode}:${p.filesystem}:${p.format}`).join(',')}`;
|
||||
}
|
||||
|
||||
onDiskSelected(diskNumber: number) {
|
||||
this.selectedDiskNumber = diskNumber;
|
||||
this.scrollToPartitionTable();
|
||||
}
|
||||
|
||||
onDiskSelectionChange() {
|
||||
if (this.selectedDiskNumber) {
|
||||
this.scrollToPartitionTable();
|
||||
}
|
||||
}
|
||||
|
||||
scrollToPartitionTable() {
|
||||
// Pequeño delay para asegurar que el contenido se haya renderizado
|
||||
setTimeout(() => {
|
||||
const diskInfo = document.getElementById('disk-info');
|
||||
|
||||
if (diskInfo) {
|
||||
diskInfo.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
scrollToExecuteButton() {
|
||||
console.log('scrollToExecuteButton llamado');
|
||||
|
||||
const executeButton = document.getElementById('execute-button');
|
||||
console.log('Botón ejecutar encontrado:', executeButton);
|
||||
|
||||
if (executeButton) {
|
||||
executeButton.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
console.log('Scroll hacia botón ejecutar completado');
|
||||
} else {
|
||||
console.error('No se encontró el botón execute-button');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
@ -102,10 +101,287 @@ table {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 24px 32px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.header-container-title h2 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-container-title h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
background: white !important;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: white !important;
|
||||
border-radius: 16px;
|
||||
padding: 20px !important;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.form-section-title mat-icon {
|
||||
color: #667eea;
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Badges y chips */
|
||||
.destination-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #bbdefb;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.destination-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.destination-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.destination-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #1976d2;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.destination-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #0d47a1;
|
||||
}
|
||||
|
||||
.info-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #c8e6c9;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.info-badge:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.info-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #388e3c;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #1b5e20;
|
||||
}
|
||||
|
||||
/* Clientes y tarjetas */
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.client-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.client-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 2px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
font-size: 10px;
|
||||
color: #6c757d;
|
||||
display: block;
|
||||
margin-bottom: 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.selected-client {
|
||||
background: linear-gradient(135deg, #8fa1f0 0%, #9b7bc8 100%);
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.selected-client .client-name,
|
||||
.selected-client .client-ip {
|
||||
color: white;
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-panel {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
|
||||
border-radius: 12px !important;
|
||||
margin-bottom: 20px;
|
||||
background: #f7fbff !important;
|
||||
border: 1px solid #bbdefb !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-panel-header {
|
||||
padding: 20px 24px !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-panel-header-title {
|
||||
font-weight: 600 !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-panel-header-description {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
@ -116,117 +392,11 @@ table {
|
|||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .custom-tooltip {
|
||||
white-space: pre-line !important;
|
||||
max-width: 200px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.selected-client {
|
||||
background-color: #a0c2e5 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.disabled-client {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.new-command-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -261,15 +431,85 @@ table {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.script-selector-card {
|
||||
margin: 20px 20px;
|
||||
padding: 16px;
|
||||
/* Secciones del formulario */
|
||||
.form-section {
|
||||
background: white !important;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #bbdefb;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-section-title mat-icon {
|
||||
color: #2196f3;
|
||||
}
|
||||
.toggle-options {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
margin: 16px 0;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.selected-toggle {
|
||||
background: linear-gradient(135deg, #8fa1f0 0%, #9b7bc8 100%) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 20px auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Estilo para hacer el backdrop no clickeable */
|
||||
::ng-deep .non-clickable-backdrop {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
::ng-deep .action-chip {
|
||||
margin: 8px !important;
|
||||
padding: 12px 20px !important;
|
||||
border-radius: px !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 14px !important;
|
||||
transition: all 0.3s ease !important;
|
||||
border: 2px solid transparent !important;
|
||||
background: white !important;
|
||||
color: #6c757d !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
cursor: pointer !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
min-height: 48px !important;
|
||||
}
|
||||
|
||||
::ng-deep .action-chip:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
::ng-deep .action-chip.mat-mdc-chip-selected {
|
||||
border-color: #667eea !important;
|
||||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2) !important;
|
||||
}
|
||||
|
||||
::ng-deep .create-chip.mat-mdc-chip-selected {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
::ng-deep .update-chip.mat-mdc-chip-selected {
|
||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%) !important;
|
||||
color: white !important;
|
||||
}
|
|
@ -10,12 +10,12 @@
|
|||
</h4>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript)" (click)="save()">Ejecutar</button>
|
||||
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || loading" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button mat-stroked-button color="accent" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript)" (click)="openScheduleModal()">
|
||||
<mat-icon>schedule</mat-icon> Opciones de programación
|
||||
<div class="button-row">
|
||||
<button color="accent" class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || loading" (click)="openScheduleModal()">
|
||||
Opciones de programación
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,8 +40,8 @@
|
|||
<div class="clients-grid">
|
||||
<div *ngFor="let client of clientData" 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'}"
|
||||
(click)="toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected}"
|
||||
[matTooltip]="getPartitionsTooltip(client)"
|
||||
matTooltipPosition="above"
|
||||
matTooltipClass="custom-tooltip">
|
||||
|
@ -62,55 +62,69 @@
|
|||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider style="margin-top: 20px;"></mat-divider>
|
||||
<div class="select-container">
|
||||
|
||||
<mat-card class="script-selector-card">
|
||||
<mat-card-title>Seleccione el tipo de comando</mat-card-title>
|
||||
|
||||
<div class="toggle-options">
|
||||
<mat-button-toggle-group [(ngModel)]="commandType" exclusive>
|
||||
<mat-button-toggle value="new">
|
||||
<mat-icon>edit</mat-icon> Nuevo Script
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="existing">
|
||||
<mat-icon>storage</mat-icon> Script Guardado
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'new'" class="new-command-container">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Ingrese el script</mat-label>
|
||||
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
|
||||
</mat-form-field>
|
||||
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'existing'">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Seleccione script a ejecutar</mat-label>
|
||||
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
|
||||
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
|
||||
<div class="script-content">
|
||||
<h3>Script:</h3>
|
||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>code</mat-icon>
|
||||
Configuración de script
|
||||
</div>
|
||||
|
||||
<div class="script-params" *ngIf="parameterNames.length > 0 && selectedScript.parameters">
|
||||
<h3>Ingrese los parámetros:</h3>
|
||||
<div *ngFor="let paramName of parameterNames">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ paramName }}</mat-label>
|
||||
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
|
||||
</mat-form-field>
|
||||
<div class="action-chips-container">
|
||||
<mat-chip-listbox [(ngModel)]="commandType" required class="action-chip-listbox">
|
||||
<mat-chip-option value="new" class="action-chip create-chip firmware-chip" (click)="commandType = 'new'">
|
||||
<span>Nuevo Script</span>
|
||||
</mat-chip-option>
|
||||
<mat-chip-option value="existing" class="action-chip update-chip firmware-chip" (click)="commandType = 'existing'">
|
||||
<span>Script Guardado</span>
|
||||
</mat-chip-option>
|
||||
</mat-chip-listbox>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="commandType === 'new'" class="new-command-container">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Ingrese el script</mat-label>
|
||||
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
|
||||
</mat-form-field>
|
||||
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'existing'">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Seleccione script a ejecutar</mat-label>
|
||||
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
|
||||
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
|
||||
<div class="script-content">
|
||||
<h3>Script:</h3>
|
||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
||||
</div>
|
||||
|
||||
<div class="script-params" *ngIf="parameterNames.length > 0 && selectedScript.parameters">
|
||||
<h3>Ingrese los parámetros:</h3>
|
||||
<div *ngFor="let paramName of parameterNames">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ paramName }}</mat-label>
|
||||
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<app-scroll-to-top
|
||||
[threshold]="200"
|
||||
targetElement=".header-container"
|
||||
position="bottom-right"
|
||||
[showTooltip]="true"
|
||||
tooltipText="Volver arriba"
|
||||
tooltipPosition="left">
|
||||
</app-scroll-to-top>
|
|
@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||
import { RunScriptAssistantComponent } from './run-script-assistant.component';
|
||||
import { DeployImageComponent } from "../deploy-image/deploy-image.component";
|
||||
import { LoadingComponent } from "../../../../../shared/loading/loading.component";
|
||||
import { ScrollToTopComponent } from "../../../../../shared/scroll-to-top/scroll-to-top.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";
|
||||
|
@ -28,6 +29,8 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
import { MatChipsModule } from "@angular/material/chips";
|
||||
import { MatTooltipModule } from "@angular/material/tooltip";
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http);
|
||||
|
@ -44,7 +47,7 @@ describe('RunScriptAssistantComponent', () => {
|
|||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RunScriptAssistantComponent, DeployImageComponent, LoadingComponent],
|
||||
declarations: [RunScriptAssistantComponent, DeployImageComponent, LoadingComponent, ScrollToTopComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
|
@ -63,6 +66,8 @@ describe('RunScriptAssistantComponent', () => {
|
|||
MatIconModule,
|
||||
MatCardModule,
|
||||
MatButtonToggleModule,
|
||||
MatChipsModule,
|
||||
MatTooltipModule,
|
||||
ToastrModule.forRoot(),
|
||||
HttpClientTestingModule,
|
||||
TranslateModule.forRoot({
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||
import { SaveScriptComponent } from "./save-script/save-script.component";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
|
||||
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-run-script-assistant',
|
||||
|
@ -53,15 +54,9 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
}
|
||||
});
|
||||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string}) => {
|
||||
if (client.status === 'og-live') {
|
||||
client.selected = true;
|
||||
}
|
||||
});
|
||||
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
|
||||
|
||||
this.selectedClients = this.clientData.filter(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
);
|
||||
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
|
||||
|
||||
this.loadScripts()
|
||||
}
|
||||
|
@ -122,18 +117,12 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
}
|
||||
|
||||
updateSelectedClients() {
|
||||
this.selectedClients = this.clientData.filter(
|
||||
(client: { selected: boolean; status: string }) => client.selected && client.status === "og-live"
|
||||
);
|
||||
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === "og-live") {
|
||||
client.selected = this.allSelected;
|
||||
}
|
||||
});
|
||||
this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; });
|
||||
}
|
||||
|
||||
getPartitionsTooltip(client: any): string {
|
||||
|
@ -179,23 +168,33 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
}
|
||||
|
||||
save(): void {
|
||||
this.loading = true;
|
||||
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
|
||||
width: '400px',
|
||||
disableClose: true,
|
||||
hasBackdrop: true
|
||||
});
|
||||
|
||||
this.http.post(`${this.baseUrl}/commands/run-script`, {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
script: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Script ejecutado correctamente');
|
||||
this.dataChange.emit();
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error al ejecutar el script');
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result !== undefined) {
|
||||
this.loading = true;
|
||||
this.http.post(`${this.baseUrl}/commands/run-script`, {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
script: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||
queue: result
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Script ejecutado correctamente');
|
||||
this.dataChange.emit();
|
||||
this.router.navigate(['/commands-logs']);
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error al ejecutar el script');
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
openScheduleModal(): void {
|
||||
|
@ -203,7 +202,8 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id']
|
||||
organizationalUnit: this.runScriptContext['@id'],
|
||||
source: 'assistant'
|
||||
}
|
||||
});
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,8 @@
|
|||
<app-modal-overlay
|
||||
[isVisible]="loading"
|
||||
message="Cargando...">
|
||||
</app-modal-overlay>
|
||||
|
||||
<div class="groups-container">
|
||||
<!-- HEADER -->
|
||||
<div class="header-container">
|
||||
|
@ -11,11 +16,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
|
||||
|
@ -95,8 +101,6 @@
|
|||
</button>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-divider class="tree-mat-divider" style="padding-top: 10px;"></mat-divider>
|
||||
|
||||
<!-- Funcionalidad actualmente deshabilitada-->
|
||||
<!-- <mat-form-field appearance="outline">
|
||||
<mat-select (selectionChange)="loadSelectedFilter($event.value)" placeholder="Cargar filtro" disabled>
|
||||
|
@ -119,48 +123,107 @@
|
|||
|
||||
<!-- Tree -->
|
||||
<div class="tree-container" joyrideStep="treePanelStep" text="{{ 'treePanelStepText' | translate }}">
|
||||
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
|
||||
<div class="tree-header">
|
||||
<h3 class="tree-title">
|
||||
<mat-icon>account_tree</mat-icon>
|
||||
{{ 'organizationalStructure' | translate }}
|
||||
</h3>
|
||||
<div class="tree-actions">
|
||||
<button mat-icon-button (click)="expandAll()" matTooltip="{{ 'expandAll' | translate }}">
|
||||
<mat-icon>unfold_more</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="collapseAll()" matTooltip="{{ 'collapseAll' | translate }}">
|
||||
<mat-icon>unfold_less</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl" class="modern-tree">
|
||||
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id, 'tree-node': true}"
|
||||
*matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick($event, node)">
|
||||
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable"
|
||||
[ngClass]="{'disabled-toggle': !node.expandable}">
|
||||
<mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon>
|
||||
</button>
|
||||
<mat-icon class="node-icon {{ node.type }}">
|
||||
{{
|
||||
node.type === 'organizational-unit' ? 'apartment'
|
||||
: node.type === 'classrooms-group' ? 'meeting_room'
|
||||
: node.type === 'classroom' ? 'school'
|
||||
: node.type === 'clients-group' ? 'lan'
|
||||
: node.type === 'client' ? 'computer'
|
||||
: 'group'
|
||||
}}
|
||||
</mat-icon>
|
||||
<span>{{ node.name }}</span>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onMenuClick($event, node)">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="node-content">
|
||||
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable"
|
||||
[ngClass]="{'disabled-toggle': !node.expandable}" class="expand-button">
|
||||
<mat-icon class="expand-icon">{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="node-info">
|
||||
<div class="node-main">
|
||||
<mat-icon class="node-icon {{ node.type }}" [matTooltip]="getNodeTypeTooltip(node.type)">
|
||||
{{
|
||||
node.type === 'organizational-unit' ? 'business'
|
||||
: node.type === 'classrooms-group' ? 'meeting_room'
|
||||
: node.type === 'classroom' ? 'school'
|
||||
: node.type === 'clients-group' ? 'dns'
|
||||
: node.type === 'client' ? 'computer'
|
||||
: 'folder'
|
||||
}}
|
||||
</mat-icon>
|
||||
<span class="node-name" [matTooltip]="node.name">{{ node.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="node-details">
|
||||
<ng-container *ngIf="node.type === 'client'">
|
||||
<span class="node-ip">{{ node.ip }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="node.children && node.children.length > 0">
|
||||
<span class="node-count">{{ node.children.length }} {{ getNodeCountLabel(node.children.length) }}</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="node-actions">
|
||||
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onMenuClick($event, node)"
|
||||
class="menu-button" matTooltip="{{ 'moreActions' | translate }}">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tree-node>
|
||||
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
|
||||
|
||||
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id, 'tree-node': true}"
|
||||
*matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick($event, node)">
|
||||
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button>
|
||||
<mat-icon style="color: green;">
|
||||
{{
|
||||
node.type === 'organizational-unit' ? 'apartment'
|
||||
: node.type === 'classrooms-group' ? 'meeting_room'
|
||||
: node.type === 'classroom' ? 'school'
|
||||
: node.type === 'clients-group' ? 'lan'
|
||||
: node.type === 'client' ? 'computer'
|
||||
: 'group'
|
||||
}}
|
||||
</mat-icon>
|
||||
<span>{{ node.name }}</span>
|
||||
<ng-container *ngIf="node.type === 'client'">
|
||||
<span> - IP: {{ node.ip }}</span>
|
||||
</ng-container>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onMenuClick($event, node)">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="node-content">
|
||||
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle expand-button">
|
||||
<mat-icon class="expand-icon">chevron_right</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="node-info">
|
||||
<div class="node-main">
|
||||
<mat-icon class="node-icon {{ node.type }}" [ngClass]="{'client-status': node.type === 'client'}"
|
||||
[matTooltip]="getNodeTypeTooltip(node.type)">
|
||||
{{
|
||||
node.type === 'organizational-unit' ? 'business'
|
||||
: node.type === 'classrooms-group' ? 'meeting_room'
|
||||
: node.type === 'classroom' ? 'school'
|
||||
: node.type === 'clients-group' ? 'dns'
|
||||
: node.type === 'client' ? 'computer'
|
||||
: 'folder'
|
||||
}}
|
||||
</mat-icon>
|
||||
<span class="node-name" [matTooltip]="node.name">{{ node.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="node-details">
|
||||
<ng-container *ngIf="node.type === 'client'">
|
||||
<span class="node-ip">{{ node.ip }}</span>
|
||||
<span class="node-mac">{{ node.mac }}</span>
|
||||
<span class="node-status" [ngClass]="'status-' + (node.status || 'off')">
|
||||
{{ getStatusLabel(node.status) }}
|
||||
</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="node-actions">
|
||||
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onMenuClick($event, node)"
|
||||
class="menu-button" matTooltip="{{ 'moreActions' | translate }}">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tree-node>
|
||||
</mat-tree>
|
||||
</div>
|
||||
|
@ -180,23 +243,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>
|
||||
|
@ -204,6 +267,10 @@
|
|||
<mat-icon>storage</mat-icon>
|
||||
<span>{{ 'partitions' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="openOUPendingTasks($event, selectedNode)">
|
||||
<mat-icon>pending_actions</mat-icon>
|
||||
<span>{{ 'colaAcciones' | translate }}</span>
|
||||
</button>
|
||||
<app-execute-command [clientData]="selectedNode?.clients || []" [buttonType]="'menu-item'"
|
||||
[buttonText]="'ejecutarComandos' | translate" [icon]="'terminal'"
|
||||
[disabled]="!((selectedNode?.clients ?? []).length > 0)" [runScriptContext]="selectedNode?.name || ''"
|
||||
|
@ -224,11 +291,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'">
|
||||
|
@ -248,6 +320,45 @@
|
|||
text="{{ 'clientsViewStepText' | translate }}">
|
||||
<div *ngIf="hasClients; else noClientsTemplate">
|
||||
|
||||
<div class="stats-container" *ngIf="currentView === 'list'">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<mat-icon>computer</mat-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ totalStats.total }}</div>
|
||||
<div class="stat-label">{{ 'totalClients' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon offline">
|
||||
<mat-icon>wifi_off</mat-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ getStatusCount('off') }}</div>
|
||||
<div class="stat-label">{{ 'offline' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon online">
|
||||
<mat-icon>wifi</mat-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ getStatusCount('og-live') + getStatusCount('linux') + getStatusCount('windows') }}</div>
|
||||
<div class="stat-label">{{ 'online' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon busy">
|
||||
<mat-icon>hourglass_empty</mat-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ getStatusCount('busy') }}</div>
|
||||
<div class="stat-label">{{ 'busy' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards view -->
|
||||
<div *ngIf="currentView === 'card'">
|
||||
<section class="cards-view">
|
||||
|
@ -256,19 +367,18 @@
|
|||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
</mat-checkbox>
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of arrayClients" class="client-item">
|
||||
<div *ngFor="let client of arrayClients" class="client-item" [ngClass]="'status-' + client.status">
|
||||
<div class="client-card">
|
||||
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(client)"
|
||||
[checked]="selection.isSelected(client)"
|
||||
[disabled]="client.status === 'busy' || client.status === 'off' || client.status === 'disconnected'">
|
||||
[checked]="selection.isSelected(client)">
|
||||
</mat-checkbox>
|
||||
<img style="margin-top: 0.5em;" [src]="'assets/images/computer_' + client.status + '.svg'"
|
||||
alt="Client Icon" class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name }}</span>
|
||||
<span class="client-name truncate-cell-wide" [matTooltip]="client.name">{{ client.name }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
<span class="client-mac">{{ client.mac }}</span>
|
||||
<div class="action-icons">
|
||||
|
||||
<app-execute-command [clientState]="client.status" [clientData]="[client]"
|
||||
|
@ -287,7 +397,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 +414,15 @@
|
|||
<mat-icon>list_alt</mat-icon>
|
||||
<span>{{ 'procedimientosCliente' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<button mat-menu-item (click)="openClientPendingTasks($event, client)">
|
||||
<mat-icon>pending_actions</mat-icon>
|
||||
<span>{{ 'colaAcciones' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item *ngIf="client.status === 'og-live'" (click)="openClientLogsInNewTab($event, client)">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>Logs en tiempo real</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
|
@ -322,10 +440,24 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List view -->
|
||||
<!-- List view mejorada -->
|
||||
<div *ngIf="currentView === 'list'" class="list-view">
|
||||
<div class="table-header">
|
||||
<div class="table-info">
|
||||
<span>{{ 'showingResults' | translate: { from: getPaginationFrom(), to: getPaginationTo(), total: getPaginationTotal() } }}</span>
|
||||
</div>
|
||||
<div class="table-actions">
|
||||
<button mat-icon-button (click)="refreshClientData()" matTooltip="{{ 'refresh' | translate }}">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="exportToCSV()" matTooltip="{{ 'exportCSV' | translate }}">
|
||||
<mat-icon>download</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="clients-table" tabindex="0">
|
||||
<table mat-table matSort [dataSource]="selectedClients">
|
||||
<table mat-table [dataSource]="selectedClients" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox (change)="$event ? toggleAllRows() : null"
|
||||
|
@ -335,13 +467,19 @@
|
|||
</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(row)"
|
||||
[checked]="selection.isSelected(row)"
|
||||
[disabled]="row.status === 'busy' || row.status === 'off' || row.status === 'disconnected'">
|
||||
[checked]="selection.isSelected(row)">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="column-header">
|
||||
<span>{{ 'status' | translate }}</span>
|
||||
<button mat-icon-button (click)="sortColumn('status')" class="sort-button">
|
||||
<mat-icon>{{ getSortIcon('status') }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
|
||||
matTooltipPosition="left" matTooltipShowDelay="500">
|
||||
<div class="client-status-container">
|
||||
|
@ -355,74 +493,103 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="column-header">
|
||||
<span>{{ 'name' | translate }}</span>
|
||||
<button mat-icon-button (click)="sortColumn('name')" class="sort-button">
|
||||
<mat-icon>{{ getSortIcon('name') }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
|
||||
matTooltipPosition="left" matTooltipShowDelay="500">
|
||||
<p>{{ client.name }}</p>
|
||||
<div class="client-cell">
|
||||
<span class="client-name truncate-cell-wide" [matTooltip]="client.name">{{ client.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="ip">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>IP </th>
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="column-header">
|
||||
<span>IP</span>
|
||||
<button mat-icon-button (click)="sortColumn('ip')" class="sort-button">
|
||||
<mat-icon>{{ getSortIcon('ip') }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
|
||||
matTooltipPosition="left" matTooltipShowDelay="500">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span>{{ client.ip }}</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ client.mac }}</span>
|
||||
<div class="client-cell">
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-mac">{{ client.mac }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="firmwareType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'firmwareType' | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="column-header">
|
||||
<span>{{ 'firmwareType' | translate }}</span>
|
||||
<button mat-icon-button (click)="sortColumn('firmwareType')" class="sort-button">
|
||||
<mat-icon>{{ getSortIcon('firmwareType') }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<mat-chip *ngIf="client.firmwareType">
|
||||
<mat-chip *ngIf="client.firmwareType" class="firmware-chip">
|
||||
{{ client.firmwareType }}
|
||||
</mat-chip>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="oglive">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
|
||||
<th mat-header-cell *matHeaderCellDef> OG Live </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span>{{ client.ogLive?.kernel }} </span>
|
||||
<span style="font-size: 0.75rem; color: gray;"> {{ client.ogLive?.date | date }}</span>
|
||||
<div class="oglive-cell">
|
||||
<span class="oglive-kernel">{{ client.ogLive?.kernel }}</span>
|
||||
<span class="oglive-date">{{ client.ogLive?.date | date }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="maintenace">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'maintenance' | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'maintenance' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.maintenance }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="subnet">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'subnet' | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'subnet' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.subnet }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="pxeTemplate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'pxeTemplate' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.pxeTemplate?.name }} </td>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'pxeTemplate' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client" class="truncate-cell-medium" [matTooltip]="client.pxeTemplate?.name"> {{ client.pxeTemplate?.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="parentName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'parent' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.parentName }} </td>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'parent' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client" class="truncate-cell-medium" [matTooltip]="client.parentName"> {{ client.parentName }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'actions' | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'actions' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<button
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
|
||||
mat-icon-button [matMenuTriggerFor]="clientMenu" color="primary">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<app-execute-command [clientState]="client.status" [clientData]="[client]" [buttonType]="'icon'"
|
||||
[icon]="'terminal'"
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
|
||||
[runScriptContext]="getRunScriptContext([client])">
|
||||
</app-execute-command>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
|
||||
mat-icon-button [matMenuTriggerFor]="clientMenu" color="primary" matTooltip="{{ 'moreActions' | translate }}">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<app-execute-command [clientState]="client.status" [clientData]="[client]" [buttonType]="'icon'"
|
||||
[icon]="'terminal'"
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
|
||||
[runScriptContext]="getRunScriptContext([client])" matTooltip="{{ 'executeCommand' | translate }}">
|
||||
</app-execute-command>
|
||||
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)" matTooltip="{{ 'viewDetails' | translate }}">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<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,16 +605,26 @@
|
|||
<mat-icon>list_alt</mat-icon>
|
||||
<span>{{ 'procedimientosCliente' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<button mat-menu-item (click)="openClientPendingTasks($event, client)">
|
||||
<mat-icon>pending_actions</mat-icon>
|
||||
<span>{{ 'colaAcciones' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item *ngIf="client.status === 'og-live'" (click)="openClientLogsInNewTab($event, client)">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>Logs en tiempo real</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row style="background-color: #f3f3f3;"
|
||||
*matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;" class="mat-row"
|
||||
[ngClass]="'status-' + row.status"
|
||||
[class.selected-row]="selectedClient?.uuid === row.uuid"
|
||||
(click)="selectClient(row)"></tr>
|
||||
</table>
|
||||
</section>
|
||||
<mat-paginator class="list-paginator" [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page"
|
||||
|
@ -469,4 +646,4 @@
|
|||
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -27,6 +27,7 @@ import { TreeNode } from './model/model';
|
|||
import { LoadingComponent } from '../../shared/loading/loading.component';
|
||||
import { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ModalOverlayComponent } from '../../shared/modal-overlay/modal-overlay.component';
|
||||
|
||||
describe('GroupsComponent', () => {
|
||||
let component: GroupsComponent;
|
||||
|
@ -39,7 +40,7 @@ describe('GroupsComponent', () => {
|
|||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent],
|
||||
declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent, ModalOverlayComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
|
|
|
@ -15,7 +15,6 @@ import { ShowOrganizationalUnitComponent } from './shared/organizational-units/s
|
|||
import { LegendComponent } from './shared/legend/legend.component';
|
||||
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { PageEvent } from '@angular/material/paginator';
|
||||
import { CreateMultipleClientComponent } from "./shared/clients/create-multiple-client/create-multiple-client.component";
|
||||
|
@ -29,6 +28,9 @@ 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';
|
||||
import { ClientPendingTasksComponent } from '../task-logs/client-pending-tasks/client-pending-tasks.component';
|
||||
|
||||
enum NodeType {
|
||||
OrganizationalUnit = 'organizational-unit',
|
||||
|
@ -77,6 +79,29 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
arrayClients: any[] = [];
|
||||
filters: { [key: string]: string } = {};
|
||||
private clientFilterSubject = new Subject<string>();
|
||||
loading = false;
|
||||
|
||||
// Nuevas propiedades para funcionalidades mejoradas
|
||||
selectedClient: any = null;
|
||||
sortBy: string = 'name';
|
||||
sortDirection: 'asc' | 'desc' = 'asc';
|
||||
currentSortColumn: string = 'name';
|
||||
|
||||
// Estadísticas totales
|
||||
totalStats: {
|
||||
total: number;
|
||||
off: number;
|
||||
online: number;
|
||||
busy: number;
|
||||
} = {
|
||||
total: 0,
|
||||
off: 0,
|
||||
online: 0,
|
||||
busy: 0
|
||||
};
|
||||
|
||||
// Tipos de firmware disponibles
|
||||
firmwareTypes: string[] = [];
|
||||
|
||||
protected status = [
|
||||
{ value: 'off', name: 'Apagado' },
|
||||
|
@ -93,16 +118,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
displayedColumns: string[] = ['select', 'status', 'ip', 'firmwareType', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
|
||||
|
||||
private _sort!: MatSort;
|
||||
|
||||
@ViewChild(MatSort)
|
||||
set matSort(ms: MatSort) {
|
||||
this._sort = ms;
|
||||
if (this.selectedClients) {
|
||||
this.selectedClients.sort = this._sort;
|
||||
}
|
||||
}
|
||||
|
||||
private subscriptions: Subscription = new Subscription();
|
||||
|
||||
constructor(
|
||||
|
@ -114,6 +129,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 +148,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';
|
||||
}
|
||||
|
||||
|
||||
|
@ -401,8 +417,14 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
public fetchClientsForNode(node: any, selectedClientsBeforeEdit: string[] = []): void {
|
||||
const params = new HttpParams({ fromObject: this.filters });
|
||||
|
||||
// Agregar parámetros de ordenamiento al backend
|
||||
let backendParams = { ...this.filters };
|
||||
if (this.sortBy) {
|
||||
backendParams['order[' + this.sortBy + ']'] = this.sortDirection;
|
||||
}
|
||||
|
||||
this.isLoadingClients = true;
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params }).subscribe({
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: backendParams }).subscribe({
|
||||
next: (response: any) => {
|
||||
this.selectedClients.data = response['hydra:member'];
|
||||
if (this.selectedNode) {
|
||||
|
@ -420,6 +442,9 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.selection.select(client);
|
||||
}
|
||||
});
|
||||
|
||||
// Calcular estadísticas después de cargar los clientes
|
||||
this.calculateLocalStats();
|
||||
},
|
||||
error: () => {
|
||||
this.isLoadingClients = false;
|
||||
|
@ -435,25 +460,35 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
addOU(event: MouseEvent, parent: TreeNode | null = null): void {
|
||||
this.loading = true;
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(ManageOrganizationalUnitComponent, {
|
||||
data: { parent },
|
||||
width: '900px',
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((newUnit) => {
|
||||
if (newUnit) {
|
||||
this.refreshData(newUnit.uuid);
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
||||
this.loading = true;
|
||||
event.stopPropagation();
|
||||
const targetNode = organizationalUnit || this.selectedNode;
|
||||
const dialogRef = this.dialog.open(ManageClientComponent, {
|
||||
data: { organizationalUnit: targetNode },
|
||||
width: '900px',
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
|
@ -466,17 +501,22 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.refreshData(parentNode.uuid);
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
||||
this.loading = true;
|
||||
event.stopPropagation();
|
||||
const targetNode = organizationalUnit || this.selectedNode;
|
||||
|
||||
const dialogRef = this.dialog.open(CreateMultipleClientComponent, {
|
||||
data: { organizationalUnit: targetNode },
|
||||
width: '900px',
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result?.success) {
|
||||
|
@ -491,29 +531,33 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
console.error('No se encontró el nodo padre después de la creación masiva.');
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
onEditNode(event: MouseEvent, node: TreeNode | null): void {
|
||||
event.stopPropagation();
|
||||
this.loading = true;
|
||||
const uuid = node ? this.extractUuid(node['@id']) : null;
|
||||
if (!uuid) return;
|
||||
|
||||
const dialogRef = node?.type !== NodeType.Client
|
||||
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
|
||||
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
|
||||
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' })
|
||||
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' });
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result?.success) {
|
||||
this.refreshData(node?.id);
|
||||
}
|
||||
this.menuTriggers.forEach(trigger => trigger.closeMenu());
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteClick(event: MouseEvent, entity: TreeNode | Client | null): void {
|
||||
event.stopPropagation();
|
||||
this.loading = true;
|
||||
if (!entity) return;
|
||||
|
||||
const uuid = entity['@id'] ? this.extractUuid(entity['@id']) : null;
|
||||
|
@ -530,6 +574,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
if (result === true) {
|
||||
this.deleteEntityorClient(uuid, type);
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -567,16 +612,18 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
onEditClick(event: MouseEvent, type: string, uuid: string): void {
|
||||
event.stopPropagation();
|
||||
this.loading = true;
|
||||
const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
|
||||
const dialogRef = type !== NodeType.Client
|
||||
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
|
||||
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
|
||||
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' })
|
||||
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' });
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result?.success) {
|
||||
this.refreshData(this.selectedNode?.id, selectedClientsBeforeEdit);
|
||||
}
|
||||
this.menuTriggers.forEach(trigger => trigger.closeMenu());
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -584,11 +631,14 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
onRoomMap(room: TreeNode | null): void {
|
||||
if (!room || !room['@id']) return;
|
||||
this.subscriptions.add(
|
||||
this.http.get<{ clients: Client[] }>(`${this.baseUrl}${room['@id']}`).subscribe(
|
||||
(response) => {
|
||||
this.http.get<{ clients: Client[] }>(`${this.baseUrl}/clients?organizationalUnit.id=${room.id}`).subscribe(
|
||||
(response: any) => {
|
||||
this.dialog.open(ClassroomViewDialogComponent, {
|
||||
width: '90vw',
|
||||
data: { clients: response.clients },
|
||||
data: { clients: response['hydra:member'] },
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
|
@ -600,35 +650,46 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
|
||||
executeCommand(command: Command, selectedNode: TreeNode | null): void {
|
||||
|
||||
this.loading = true;
|
||||
if (!selectedNode) {
|
||||
this.toastr.error('No hay un nodo seleccionado.');
|
||||
return;
|
||||
} else {
|
||||
this.toastr.success(`Ejecutando comando: ${command.name} en ${selectedNode.name}`);
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
|
||||
onShowClientDetail(event: MouseEvent, client: Client): void {
|
||||
event.stopPropagation();
|
||||
this.dialog.open(ClientDetailsComponent, {
|
||||
this.loading = true;
|
||||
const dialogRef = this.dialog.open(ClientDetailsComponent, {
|
||||
width: '70vw',
|
||||
height: '90vh',
|
||||
data: { clientData: client },
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
})
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void {
|
||||
event.stopPropagation();
|
||||
this.loading = true;
|
||||
if (data && data.type !== NodeType.Client) {
|
||||
this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '800px' });
|
||||
this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '800px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' });
|
||||
} else {
|
||||
if (data) {
|
||||
this.router.navigate(['clients', this.extractUuid(data['@id'])], { state: { clientData: data } });
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
|
||||
|
@ -747,8 +808,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.syncingClientId = null;
|
||||
this.refreshData(parentNodeId)
|
||||
},
|
||||
() => {
|
||||
this.toastr.error('Error de conexión con el cliente');
|
||||
(error) => {
|
||||
this.toastr.error(error.error['hydra:description'] || 'Error al actualizar el cliente');
|
||||
this.syncStatus = false;
|
||||
this.syncingClientId = null;
|
||||
this.refreshData(parentNodeId)
|
||||
|
@ -864,12 +925,306 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
openClientTaskLogs(event: MouseEvent, client: Client): void {
|
||||
changeParent(event: MouseEvent, ): void {
|
||||
event.stopPropagation();
|
||||
|
||||
this.dialog.open(ClientTaskLogsComponent, {
|
||||
const dialogRef = this.dialog.open(ChangeParentComponent, {
|
||||
data: { clients: this.selection.selected },
|
||||
width: '700px',
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.refreshData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openClientTaskLogs(event: MouseEvent, client: Client): void {
|
||||
this.loading = true;
|
||||
event.stopPropagation();
|
||||
|
||||
const dialogRef = this.dialog.open(ClientTaskLogsComponent, {
|
||||
width: '1200px',
|
||||
data: {client}
|
||||
data: { client },
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
})
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
openClientPendingTasks(event: MouseEvent, client: Client): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(ClientPendingTasksComponent, {
|
||||
width: '90vw',
|
||||
height: '80vh',
|
||||
data: {
|
||||
client: client,
|
||||
parentNode: this.selectedNode
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.refreshClientData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openClientLogsInNewTab(event: MouseEvent, client: Client): void {
|
||||
event.stopPropagation();
|
||||
if (client.ip) {
|
||||
const logsUrl = `${this.baseUrl}/pcclients/${client.ip}/cgi-bin/httpd-log.sh`;
|
||||
const windowName = `logs_${client.ip.replace(/\./g, '_')}`;
|
||||
const newWindow = window.open(logsUrl, windowName);
|
||||
if (newWindow) {
|
||||
newWindow.document.write(`
|
||||
<title>Logs - ${client.ip}</title>
|
||||
<iframe src="${logsUrl}" width="100%" height="100%" style="border:none;"></iframe>
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
this.toastr.error('No se puede acceder a los logs: IP del cliente no disponible', 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
openOUPendingTasks(event: MouseEvent, node: any): void {
|
||||
event.stopPropagation();
|
||||
this.loading = true;
|
||||
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=1&itemsPerPage=10000`).subscribe({
|
||||
next: (response) => {
|
||||
const allClients = response['hydra:member'] || [];
|
||||
|
||||
if (allClients.length === 0) {
|
||||
this.toastr.warning('Esta unidad organizativa no tiene clientes');
|
||||
return;
|
||||
}
|
||||
|
||||
const ouClientData = {
|
||||
name: node.name,
|
||||
id: node.id,
|
||||
uuid: node.uuid,
|
||||
type: 'organizational-unit',
|
||||
clients: allClients
|
||||
};
|
||||
|
||||
const dialogRef = this.dialog.open(ClientPendingTasksComponent, {
|
||||
width: '1200px',
|
||||
data: { client: ouClientData, isOrganizationalUnit: true },
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al obtener los clientes de la unidad organizativa:', error);
|
||||
this.toastr.error('Error al cargar los clientes de la unidad organizativa');
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Métodos para paginación
|
||||
getPaginationFrom(): number {
|
||||
return (this.page * this.itemsPerPage) + 1;
|
||||
}
|
||||
|
||||
getPaginationTo(): number {
|
||||
return Math.min((this.page + 1) * this.itemsPerPage, this.length);
|
||||
}
|
||||
|
||||
getPaginationTotal(): number {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
refreshClientData(): void {
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
this.toastr.success('Datos actualizados', 'Éxito');
|
||||
}
|
||||
|
||||
exportToCSV(): void {
|
||||
const headers = ['Nombre', 'IP', 'MAC', 'Estado', 'Firmware', 'Subnet', 'Parent'];
|
||||
const csvData = this.arrayClients.map(client => [
|
||||
client.name,
|
||||
client.ip || '',
|
||||
client.mac || '',
|
||||
client.status || '',
|
||||
client.firmwareType || '',
|
||||
client.subnet || '',
|
||||
client.parentName || ''
|
||||
]);
|
||||
|
||||
const csvContent = [headers, ...csvData]
|
||||
.map(row => row.map(cell => `"${cell}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `clients_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.toastr.success('Archivo CSV exportado correctamente', 'Éxito');
|
||||
}
|
||||
|
||||
private calculateLocalStats(): void {
|
||||
const clients = this.arrayClients;
|
||||
this.totalStats = {
|
||||
total: clients.length,
|
||||
off: clients.filter(client => client.status === 'off').length,
|
||||
online: clients.filter(client => ['og-live', 'linux', 'windows', 'linux-session', 'windows-session'].includes(client.status)).length,
|
||||
busy: clients.filter(client => client.status === 'busy').length
|
||||
};
|
||||
|
||||
// Actualizar tipos de firmware disponibles
|
||||
this.firmwareTypes = [...new Set(clients.map(client => client.firmwareType).filter(Boolean))];
|
||||
}
|
||||
|
||||
// Métodos para funcionalidades mejoradas
|
||||
|
||||
selectClient(client: any): void {
|
||||
this.selectedClient = client;
|
||||
}
|
||||
|
||||
sortColumn(columnDef: string): void {
|
||||
if (this.currentSortColumn === columnDef) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.currentSortColumn = columnDef;
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
this.sortBy = columnDef;
|
||||
this.onSortChange();
|
||||
}
|
||||
|
||||
getSortIcon(columnDef: string): string {
|
||||
if (this.currentSortColumn !== columnDef) {
|
||||
return 'unfold_more';
|
||||
}
|
||||
return this.sortDirection === 'asc' ? 'expand_less' : 'expand_more';
|
||||
}
|
||||
|
||||
onSortChange(): void {
|
||||
// Hacer nueva llamada al backend con el ordenamiento actualizado
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
}
|
||||
|
||||
getStatusCount(status: string): number {
|
||||
switch(status) {
|
||||
case 'off':
|
||||
return this.totalStats.off;
|
||||
case 'online':
|
||||
return this.totalStats.online;
|
||||
case 'busy':
|
||||
return this.totalStats.busy;
|
||||
default:
|
||||
return this.arrayClients.filter(client => client.status === status).length;
|
||||
}
|
||||
}
|
||||
|
||||
// Métodos para el árbol mejorado
|
||||
|
||||
expandAll(): void {
|
||||
this.treeControl.expandAll();
|
||||
}
|
||||
|
||||
collapseAll(): void {
|
||||
this.treeControl.collapseAll();
|
||||
}
|
||||
|
||||
getNodeTypeTooltip(nodeType: string): string {
|
||||
switch (nodeType) {
|
||||
case 'organizational-unit':
|
||||
return 'Unidad Organizacional - Estructura principal de la organización';
|
||||
case 'classrooms-group':
|
||||
return 'Grupo de Aulas - Conjunto de aulas relacionadas';
|
||||
case 'classroom':
|
||||
return 'Aula - Espacio físico con equipos informáticos';
|
||||
case 'clients-group':
|
||||
return 'Grupo de Equipos - Conjunto de equipos informáticos';
|
||||
case 'client':
|
||||
return 'Equipo Informático - Computadora o dispositivo individual';
|
||||
case 'group':
|
||||
return 'Grupo - Agrupación lógica de elementos';
|
||||
default:
|
||||
return 'Elemento del árbol organizacional';
|
||||
}
|
||||
}
|
||||
|
||||
getNodeCountLabel(count: number): string {
|
||||
if (count === 1) return 'elemento';
|
||||
return 'elementos';
|
||||
}
|
||||
|
||||
getStatusLabel(status: string): string {
|
||||
const statusLabels: { [key: string]: string } = {
|
||||
'off': 'Apagado',
|
||||
'og-live': 'OG Live',
|
||||
'linux': 'Linux',
|
||||
'linux-session': 'Linux Session',
|
||||
'windows': 'Windows',
|
||||
'windows-session': 'Windows Session',
|
||||
'busy': 'Ocupado',
|
||||
'mac': 'Mac',
|
||||
'disconnected': 'Desconectado',
|
||||
'initializing': 'Inicializando'
|
||||
};
|
||||
return statusLabels[status] || status;
|
||||
}
|
||||
|
||||
// Funciones para el dashboard de estadísticas
|
||||
getTotalOrganizationalUnits(): number {
|
||||
let total = 0;
|
||||
const countOrganizationalUnits = (nodes: TreeNode[]) => {
|
||||
nodes.forEach(node => {
|
||||
if (node.type === 'organizational-unit') {
|
||||
total += 1;
|
||||
}
|
||||
if (node.children) {
|
||||
countOrganizationalUnits(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
countOrganizationalUnits(this.originalTreeData);
|
||||
return total;
|
||||
}
|
||||
|
||||
getTotalClassrooms(): number {
|
||||
let total = 0;
|
||||
const countClassrooms = (nodes: TreeNode[]) => {
|
||||
nodes.forEach(node => {
|
||||
if (node.type === 'classroom') {
|
||||
total += 1;
|
||||
}
|
||||
if (node.children) {
|
||||
countClassrooms(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
countClassrooms(this.originalTreeData);
|
||||
return total;
|
||||
}
|
||||
|
||||
// Función para actualizar estadísticas cuando cambian los datos
|
||||
private updateDashboardStats(): void {
|
||||
// Las estadísticas de equipos ya se calculan en calculateLocalStats()
|
||||
// Solo necesitamos asegurar que se actualicen cuando cambian los datos
|
||||
this.calculateLocalStats();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -72,7 +72,6 @@ export class ClassroomViewComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
handleClientClick(client: any): void {
|
||||
console.log('Client clicked:', client);
|
||||
this.dialog.open(ClientViewComponent, { data: { client }, width: '800px', height: '700px' });
|
||||
}
|
||||
|
||||
|
@ -108,4 +107,4 @@ export class ClassroomViewComponent implements OnInit, OnChanges {
|
|||
} else
|
||||
this.toastService.success('Cliente actualizado!', 'Éxito');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export class ClientViewComponent {
|
|||
{ property: 'Fecha de creación', value: this.data.client.createdAt },
|
||||
{ property: 'NTP', value: this.data.client.organizationalUnit?.networkSettings?.ntp || '' },
|
||||
{ property: 'Modo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pMode || '' },
|
||||
{ property: 'Tiempo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' },
|
||||
...(this.data.client.organizationalUnit?.networkSettings?.p2pMode === 'seeder' ? [{ property: 'Tiempo p2p (minutos)', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' }] : []),
|
||||
{ property: 'IP multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastIp || '' },
|
||||
{ property: 'Modo multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastMode || '' },
|
||||
{ property: 'Puerto multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastPort || '' },
|
||||
|
@ -51,7 +51,7 @@ export class ClientViewComponent {
|
|||
{ property: 'Router', value: this.data.client.organizationalUnit?.networkSettings?.router || '' },
|
||||
{ property: 'NTP', value: this.data.client.organizationalUnit?.networkSettings?.ntp || '' },
|
||||
{ property: 'Modo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pMode || '' },
|
||||
{ property: 'Tiempo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' },
|
||||
...(this.data.client.organizationalUnit?.networkSettings?.p2pMode === 'seeder' ? [{ property: 'Tiempo p2p (minutos)', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' }] : []),
|
||||
{ property: 'IP multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastIp || '' },
|
||||
{ property: 'Modo multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastMode || '' },
|
||||
{ property: 'Puerto multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastPort || '' },
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.create-client-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh;
|
||||
padding: 16px 16px 0px 16px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="create-client-container">
|
||||
<h1 mat-dialog-title i18n="@@add-client-dialog-title">Añadir multiples clientes</h1>
|
||||
<mat-dialog-content class="create-client-container">
|
||||
<h1 mat-dialog-title>{{ 'newMultipleClientButton' | translate }}</h1>
|
||||
<div class="inputs-container">
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
|
||||
|
@ -57,10 +57,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" [disabled]="!organizationalUnit" (click)="onSubmit()">{{ 'saveButton' | translate
|
||||
}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" [disabled]="!organizationalUnit" (click)="onSubmit()">{{ 'saveButton' | translate
|
||||
}}</button>
|
||||
</mat-dialog-actions>
|
|
@ -11,10 +11,6 @@ h1 {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
padding: 15px 50px 15px 50px;
|
||||
}
|
||||
|
||||
mat-option .unit-name {
|
||||
display: block;
|
||||
}
|
||||
|
@ -33,7 +29,12 @@ mat-option .unit-path {
|
|||
}
|
||||
|
||||
.create-client-container {
|
||||
position: relative;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1em 4em 2em 4em;
|
||||
}
|
||||
|
||||
.grid-form {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="create-client-container">
|
||||
<mat-dialog-content class="create-client-container">
|
||||
<h1 mat-dialog-title>{{ dialogTitle | translate }}</h1>
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
<div [ngClass]="{'loading': loading}">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<form *ngIf="clientForm && !loading" [formGroup]="clientForm" class="client-form grid-form">
|
||||
<mat-form-field class="form-field">
|
||||
|
@ -102,16 +102,15 @@
|
|||
<mat-error>{{ 'menuError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox formControlName="maintenance">
|
||||
{{ 'maintenance' | translate }}
|
||||
</mat-checkbox>
|
||||
<mat-checkbox formControlName="maintenance">
|
||||
{{ 'maintenance' | translate }}
|
||||
</mat-checkbox>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" [disabled]="!clientForm.valid" (click)="onSubmit()">
|
||||
{{ isEditMode ? 'Guardar' : 'Crear' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" [disabled]="!clientForm.valid" (click)="onSubmit()">
|
||||
{{ isEditMode ? 'Guardar' : 'Crear' }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,25 @@
|
|||
mat-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(100px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.client-types,
|
||||
.other-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: large;
|
||||
font-weight: 550;
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
mat-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
}
|
|
@ -1,32 +1,76 @@
|
|||
<mat-list>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>apartment</mat-icon>
|
||||
<div matListItemTitle>{{ 'orgUnitTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>meeting_room</mat-icon>
|
||||
<div matListItemTitle>{{ 'classroomGroupsTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>school</mat-icon>
|
||||
<div matListItemTitle>{{ 'classroomTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>lan</mat-icon>
|
||||
<div matListItemTitle>{{ 'clientGroupsTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>computer</mat-icon>
|
||||
<div matListItemTitle>{{ 'clientTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<div class="client-types">
|
||||
<p class="section-title">Clientes</p>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_off.svg">
|
||||
<div matListItemTitle>{{ 'clientOff' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_busy.svg">
|
||||
<div matListItemTitle>{{ 'clientBusy' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_disconnected.svg">
|
||||
<div matListItemTitle>{{ 'clientDisconnected' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_initializing.svg">
|
||||
<div matListItemTitle>{{ 'clientInitializing' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_linux-session.svg">
|
||||
<div matListItemTitle>{{ 'clientLinuxSession' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_linux.svg">
|
||||
<div matListItemTitle>{{ 'clientLinux' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_macos.svg">
|
||||
<div matListItemTitle>{{ 'clientMacOS' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_og-live.svg">
|
||||
<div matListItemTitle>{{ 'clientOgLive' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_windows-session.svg">
|
||||
<div matListItemTitle>{{ 'clientWindowsSession' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<img matListItemIcon src="assets/images/computer_windows.svg">
|
||||
<div matListItemTitle>{{ 'clientWindows' | translate }}</div>
|
||||
</mat-list-item>
|
||||
</div>
|
||||
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon style="color: green;">school</mat-icon>
|
||||
<div matListItemTitle>{{ 'remoteAccess' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon style="color: rgb(209, 5, 5);">school</mat-icon>
|
||||
<div matListItemTitle>{{ 'noRemoteAccess' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<div class="other-items">
|
||||
<p class="section-title">Entidades</p>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>apartment</mat-icon>
|
||||
<div matListItemTitle>{{ 'orgUnitTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>meeting_room</mat-icon>
|
||||
<div matListItemTitle>{{ 'classroomGroupsTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>school</mat-icon>
|
||||
<div matListItemTitle>{{ 'classroomTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon style="color: green;">school</mat-icon>
|
||||
<div matListItemTitle>{{ 'remoteAccess' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon style="color: rgb(209, 5, 5);">school</mat-icon>
|
||||
<div matListItemTitle>{{ 'noRemoteAccess' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>lan</mat-icon>
|
||||
<div matListItemTitle>{{ 'clientGroupsTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
</div>
|
||||
|
||||
</mat-list>
|
|
@ -7,7 +7,12 @@ h1 {
|
|||
}
|
||||
|
||||
.create-ou-container {
|
||||
position: relative;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1em 4em 2em 4em;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
|
@ -18,25 +23,18 @@ h1 {
|
|||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
padding: 0px 40px 15px 50px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1em;
|
||||
margin-right: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.grid-form {
|
||||
|
@ -66,4 +64,4 @@ h1 {
|
|||
align-items: center;
|
||||
grid-column: span 2;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<div class="create-ou-container">
|
||||
<mat-dialog-content class="create-ou-container">
|
||||
<h1 mat-dialog-title>{{ isEditMode ? ('edit' | translate) : ('createButton' | translate) }} {{
|
||||
'labelOrganizationalUnit' | translate }}</h1>
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
<div [ngClass]="{'loading': loading}">
|
||||
<!-- Paso 1: General -->
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<span *ngIf="!loading" class="step-title">{{ 'generalTabLabel' | translate }}</span>
|
||||
|
@ -134,8 +134,8 @@
|
|||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'p2pTimeLabel' | translate }}</mat-label>
|
||||
<mat-form-field class="form-field" *ngIf="networkSettingsFormGroup.get('p2pMode')?.value === 'seeder'">
|
||||
<mat-label>{{ 'p2pTimeLabel' | translate }} (minutos)</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
|
@ -185,10 +185,10 @@
|
|||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mat-dialog-actions">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()"
|
||||
[disabled]="!generalFormGroup.valid || !additionalInfoFormGroup.valid || !networkSettingsFormGroup.valid">{{
|
||||
isEditMode ? ('edit' | translate) : ('createButton' | translate) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()"
|
||||
[disabled]="!generalFormGroup.valid || !additionalInfoFormGroup.valid || !networkSettingsFormGroup.valid">{{
|
||||
isEditMode ? ('edit' | translate) : ('createButton' | translate) }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,6 +1,6 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ManageOrganizationalUnitComponent } from './manage-organizational-unit.component';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
|
@ -35,6 +35,7 @@ describe('ManageOrganizationalUnitComponent', () => {
|
|||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
MatDialogModule,
|
||||
MatCheckboxModule,
|
||||
TranslateModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
|
|
|
@ -271,6 +271,13 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
onSubmit() {
|
||||
if (this.generalFormGroup.valid && this.additionalInfoFormGroup.valid && this.networkSettingsFormGroup.valid) {
|
||||
const parentValue = this.generalFormGroup.value.parent;
|
||||
|
||||
// Preparar networkSettings con lógica condicional para p2pTime
|
||||
const networkSettings = { ...this.networkSettingsFormGroup.value };
|
||||
if (networkSettings.p2pMode !== 'seeder') {
|
||||
networkSettings.p2pTime = null;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
name: this.generalFormGroup.value.name,
|
||||
excludeParentChanges: this.generalFormGroup.value.excludeParentChanges,
|
||||
|
@ -279,7 +286,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
comments: this.additionalInfoFormGroup.value.comments,
|
||||
remoteCalendar: this.generalFormGroup.value.remoteCalendar,
|
||||
type: this.generalFormGroup.value.type,
|
||||
networkSettings: this.networkSettingsFormGroup.value,
|
||||
networkSettings: networkSettings,
|
||||
location: this.classroomInfoFormGroup.value.location,
|
||||
projector: this.classroomInfoFormGroup.value.projector,
|
||||
board: this.classroomInfoFormGroup.value.board,
|
||||
|
|
|
@ -92,7 +92,7 @@ export class ShowOrganizationalUnitComponent implements OnInit {
|
|||
{ property: 'Router', value: this.ou.networkSettings.router },
|
||||
{ property: 'NTP', value: this.ou.networkSettings.ntp },
|
||||
{ property: 'Modo P2P', value: this.ou.networkSettings.p2pMode },
|
||||
{ property: 'Tiempo P2P', value: this.ou.networkSettings.p2pTime },
|
||||
...(this.ou.networkSettings.p2pMode === 'seeder' ? [{ property: 'Tiempo P2P (minutos)', value: this.ou.networkSettings.p2pTime }] : []),
|
||||
{ property: 'Mcast IP', value: this.ou.networkSettings.mcastIp },
|
||||
{ property: 'Mcast Speed', value: this.ou.networkSettings.mcastSpeed },
|
||||
{ property: 'Mcast Port', value: this.ou.networkSettings.mcastPort },
|
||||
|
|
|
@ -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,16 +61,11 @@ 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',
|
||||
width: '65vw',
|
||||
height: '80vh',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -60,7 +60,8 @@ export class OgbootStatusComponent implements OnInit {
|
|||
this.loading = false;
|
||||
|
||||
}, error => {
|
||||
this.toastService.error('Error al sincronizar con el el servicio de og-boot');
|
||||
console.log(error)
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -43,10 +43,12 @@ export class CreatePXEImageComponent implements OnInit {
|
|||
next: (response: any) => {
|
||||
this.loading = false;
|
||||
this.downloads = response.message;
|
||||
this.loading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error fetching downloads:', error);
|
||||
this.toastService.error('Error fetching iso files');
|
||||
this.loading = false;
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error fetching downloads');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openSubnetInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button class="action-button" (click)="addImage()" joyrideStep="addImageStep"
|
||||
[text]="'addOgLiveButtonDescription' | translate">
|
||||
{{ 'addOgLiveButton' | translate }}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { PXEimagesComponent } from './pxe-images.component';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { of } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
import { MatAccordion, MatExpansionPanel, MatExpansionPanelHeader, MatExpansionPanelTitle, MatExpansionPanelDescription } from '@angular/material/expansion';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { JoyrideModule } from 'ngx-joyride';
|
||||
import { LoadingComponent } from '../../../shared/loading/loading.component';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {MatProgressSpinner, MatProgressSpinnerModule, MatSpinner} from "@angular/material/progress-spinner";
|
||||
|
||||
describe('PXEimagesComponent', () => {
|
||||
let component: PXEimagesComponent;
|
||||
|
@ -42,13 +43,14 @@ describe('PXEimagesComponent', () => {
|
|||
MatExpansionPanelDescription,
|
||||
MatIcon,
|
||||
MatDivider,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatPaginatorModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatPaginatorModule,
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
MatTableModule,
|
||||
MatProgressSpinnerModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
|
|
|
@ -76,10 +76,8 @@ export class PXEimagesComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
this.search();
|
||||
this.loadAlert();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
addImage(): void {
|
||||
|
@ -204,7 +202,8 @@ export class PXEimagesComponent implements OnInit {
|
|||
return this.http.get<any>(`${this.apiUrl}/server/get-collection`);
|
||||
}
|
||||
|
||||
openSubnetInfoDialog() {
|
||||
openInfoDialog() {
|
||||
this.loading = true;
|
||||
this.loadAlert().subscribe(
|
||||
response => {
|
||||
this.alertMessage = response.message;
|
||||
|
@ -215,10 +214,12 @@ export class PXEimagesComponent implements OnInit {
|
|||
message: this.alertMessage
|
||||
}
|
||||
});
|
||||
this.loading = false
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
console.error('Error al cargar la información del alert', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||
import { JoyrideModule } from 'ngx-joyride';
|
||||
import { LoadingComponent } from '../../../shared/loading/loading.component';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
|
||||
describe('PxeComponent', () => {
|
||||
let component: PxeComponent;
|
||||
|
@ -45,7 +46,7 @@ describe('PxeComponent', () => {
|
|||
BrowserAnimationsModule,
|
||||
MatDialogModule,
|
||||
MatTableModule,
|
||||
MatAccordion, MatExpansionPanel, MatExpansionPanelHeader, MatExpansionPanelTitle,
|
||||
MatAccordion, MatExpansionPanel, MatExpansionPanelHeader, MatExpansionPanelTitle,
|
||||
MatExpansionPanelDescription,
|
||||
MatIcon,
|
||||
MatDivider,
|
||||
|
@ -55,6 +56,7 @@ describe('PxeComponent', () => {
|
|||
MatSelect,
|
||||
MatOption,
|
||||
MatPaginator,
|
||||
MatProgressSpinnerModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
|
@ -63,8 +65,8 @@ describe('PxeComponent', () => {
|
|||
DatePipe,
|
||||
DataService,
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting()
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting()
|
||||
]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
@ -138,4 +140,4 @@ describe('PxeComponent', () => {
|
|||
it('should have a defined selectedItem', () => {
|
||||
expect(component.selectedItem).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -77,10 +77,8 @@ export class PxeComponent implements OnInit{
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
this.search();
|
||||
this.loadAlert()
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
search(): void {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
|
|
|
@ -183,6 +183,7 @@ export class OgDhcpSubnetsComponent implements OnInit {
|
|||
}
|
||||
|
||||
openSubnetInfoDialog() {
|
||||
this.loading = true;
|
||||
this.loadAlert().subscribe(
|
||||
response => {
|
||||
this.alertMessage = response.message;
|
||||
|
@ -193,10 +194,12 @@ export class OgDhcpSubnetsComponent implements OnInit {
|
|||
message: this.alertMessage
|
||||
}
|
||||
});
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
console.error('Error al cargar la información del alert', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -37,3 +37,8 @@ mat-dialog-actions {
|
|||
.selected-item button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<h2 mat-dialog-title>Convertir imagen en virtual </h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-spinner *ngIf="loading" class="loading-spinner"></mat-spinner>
|
||||
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
|
||||
<mat-label>Extension</mat-label>
|
||||
<input matInput [(ngModel)]="extension" placeholder="Introduzca la extensión de la imagen a convertir."
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
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";
|
||||
|
@ -10,9 +10,9 @@ import {ConfigService} from "@services/config.service";
|
|||
templateUrl: './convert-image-to-virtual.component.html',
|
||||
styleUrl: './convert-image-to-virtual.component.css'
|
||||
})
|
||||
export class ConvertImageToVirtualComponent {
|
||||
export class ConvertImageToVirtualComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
loading: boolean = true;
|
||||
loading: boolean = false;
|
||||
extension: string = '';
|
||||
|
||||
constructor(
|
||||
|
@ -27,25 +27,27 @@ export class ConvertImageToVirtualComponent {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
save() {
|
||||
this.loading = true;
|
||||
this.http.post<any>(`${this.baseUrl}${this.data.imageImageRepository['@id']}/convert-image-to-virtual`, {
|
||||
extension: this.extension
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Peticion de conversion de imagen enviada correctamente');
|
||||
this.dialogRef.close();
|
||||
this.dialogRef.close(true);
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: error => {
|
||||
this.loading = false;
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,3 +37,9 @@ mat-dialog-actions {
|
|||
.selected-item button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<h2 mat-dialog-title>Convertir imagen virtual </h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<p >Repositorio destino: {{ data.name }}</p>
|
||||
<mat-spinner *ngIf="loading" class="loading-spinner"></mat-spinner>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<p *ngIf="!loading" >Repositorio destino: {{ data.name }}</p>
|
||||
|
||||
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
|
||||
<mat-label>Imagen</mat-label>
|
||||
<input matInput [(ngModel)]="imageName" placeholder="Introduzca el nombre de la imagen a importar."
|
||||
/>
|
||||
<mat-hint>El nombre de la imagen tiene que ir con la extensión. </mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
|
||||
<mat-label>Sistema de archivos</mat-label>
|
||||
<input matInput [(ngModel)]="filesystem" placeholder="Introduzca el sistema de archivos."/>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, Inject } from '@angular/core';
|
||||
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";
|
||||
|
@ -10,9 +10,9 @@ import { ConfigService } from "@services/config.service";
|
|||
templateUrl: './convert-image.component.html',
|
||||
styleUrl: './convert-image.component.css'
|
||||
})
|
||||
export class ConvertImageComponent {
|
||||
export class ConvertImageComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
loading: boolean = true;
|
||||
loading: boolean = false;
|
||||
imageName: string = '';
|
||||
filesystem: string = '';
|
||||
|
||||
|
@ -28,27 +28,28 @@ export class ConvertImageComponent {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
save() {
|
||||
console.log(this.data?.repositoryUuid)
|
||||
this.loading = true;
|
||||
this.http.post<any>(`${this.baseUrl}/image-repositories/${this.data?.repositoryUuid}/convert-image`, {
|
||||
name: this.imageName,
|
||||
filesystem: this.filesystem
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Peticion de conversion de imagen enviada correctamente');
|
||||
this.dialogRef.close();
|
||||
this.dialogRef.close(true);
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: error => {
|
||||
this.loading = false;
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ mat-dialog-actions {
|
|||
|
||||
.selected-item {
|
||||
display: flex;
|
||||
justify-content: space-between; /* Alinea texto a la izquierda y botón a la derecha */
|
||||
align-items: center; /* Centra verticalmente */
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
@ -37,3 +37,8 @@ mat-dialog-actions {
|
|||
.selected-item button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<h2 mat-dialog-title>Importar imagenes a {{ data.name }}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-spinner *ngIf="loading" class="loading-spinner"></mat-spinner>
|
||||
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
|
||||
<mat-label>Imagen</mat-label>
|
||||
<input matInput [(ngModel)]="imageName" placeholder="Introduzca el nombre de la imagen a importar."
|
||||
/>
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ConfigService } from '@services/config.service';
|
|||
})
|
||||
export class ImportImageComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
loading: boolean = true;
|
||||
loading: boolean = false;
|
||||
imageName: string = '';
|
||||
|
||||
constructor(
|
||||
|
@ -27,26 +27,28 @@ export class ImportImageComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
|
||||
}
|
||||
|
||||
save() {
|
||||
console.log(this.data?.repositoryUuid)
|
||||
this.loading = true;
|
||||
this.http.post<any>(`${this.baseUrl}/image-repositories/${this.data?.repositoryUuid}/import-image`, {
|
||||
name: this.imageName
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Peticion de importacion de imagen enviada correctamente');
|
||||
this.dialogRef.close();
|
||||
this.dialogRef.close(true);
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: error => {
|
||||
this.loading = false;
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
(click)="openShowMonoliticImagesDialog(repository)">
|
||||
{{ 'monolithicImage' | translate }}
|
||||
</button>
|
||||
<!--
|
||||
<button
|
||||
class="action-button"
|
||||
joyrideStep="gitImageStep"
|
||||
|
@ -60,8 +59,7 @@
|
|||
[disabled]="!isGitModuleInstalled"
|
||||
(click)="openShowGitImagesDialog(repository)">
|
||||
{{ 'gitImage' | translate }}
|
||||
</button
|
||||
-->
|
||||
</button>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Router } from '@angular/router';
|
|||
import { ConfigService } from '@services/config.service';
|
||||
import {Subnet} from "../ogdhcp/og-dhcp-subnets.component";
|
||||
import {ShowMonoliticImagesComponent} from "./show-monolitic-images/show-monolitic-images.component";
|
||||
import {ShowGitImagesComponent} from "./show-git-images/show-git-images.component";
|
||||
import {ShowGitCommitsComponent} from "./show-git-images/show-git-images.component";
|
||||
import {ManageRepositoryComponent} from "./manage-repository/manage-repository.component";
|
||||
|
||||
@Component({
|
||||
|
@ -146,7 +146,7 @@ export class RepositoriesComponent implements OnInit {
|
|||
}
|
||||
|
||||
openShowGitImagesDialog(repository: Subnet) {
|
||||
const dialogRef = this.dialog.open(ShowGitImagesComponent, {
|
||||
const dialogRef = this.dialog.open(ShowGitCommitsComponent, {
|
||||
width: '85vw',
|
||||
height: '85vh',
|
||||
maxWidth: '85vw',
|
||||
|
|
|
@ -98,3 +98,94 @@ table {
|
|||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
/* Estilos específicos para commits */
|
||||
.repository-selector-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.branch-selector-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.commit-id {
|
||||
font-family: 'Courier New', monospace;
|
||||
background-color: #f5f5f5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.commit-message {
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.commit-stats {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.commit-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.commit-tags .mat-chip {
|
||||
font-size: 0.8em;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.no-tags {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.mat-mdc-table {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mat-mdc-header-cell {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.mat-mdc-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-buttons .mat-mdc-icon-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.filters-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.repository-selector-container,
|
||||
.branch-selector-container {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
|
|
@ -6,96 +6,96 @@
|
|||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2>Gestionar imágenes git en {{data.repositoryName}}</h2>
|
||||
<h2>Commits de Git en {{data.repositoryName}}</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="openImageInfoDialog()">Ver Información</button>
|
||||
<button class="action-button" disabled (click)="syncRepository()">Sincronizar base de datos</button>
|
||||
<button class="action-button" disabled (click)="importImage()">
|
||||
{{ 'importImageButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchImagesField"
|
||||
text="Busca una imagen por nombre. Pulsa 'enter' para iniciar la búsqueda.">
|
||||
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="loadData()"
|
||||
i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="search-boolean">
|
||||
<mat-label i18n="@@searchLabel">Estado</mat-label>
|
||||
<mat-select [(ngModel)]="filters['status']" (selectionChange)="loadData()" placeholder="Seleccionar opción">
|
||||
<mat-option [value]="'failed'">Fallido</mat-option>
|
||||
<mat-option [value]="'pending'">Pendiente</mat-option>
|
||||
<mat-option [value]="'in-progress'">Transfiriendo</mat-option>
|
||||
<mat-option [value]="'success'">Creado con éxito</mat-option>
|
||||
<mat-option [value]="'transferring'">En progreso</mat-option>
|
||||
<mat-option [value]="'trash'">Papelera</mat-option>
|
||||
<mat-option [value]="'aux-files-pending'">Creando archivos auxiliares</mat-option>
|
||||
<div class="filters-row">
|
||||
<mat-form-field appearance="fill" style="width: 300px;">
|
||||
<mat-label>Seleccionar Repositorio</mat-label>
|
||||
<mat-select [(ngModel)]="selectedRepository" (selectionChange)="onRepositoryChange()" [disabled]="loadingRepositories">
|
||||
<mat-option *ngFor="let repo of repositories" [value]="repo">
|
||||
{{ repo }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingRepositories">hourglass_empty</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" style="width: 300px;">
|
||||
<mat-label>Seleccionar Rama</mat-label>
|
||||
<mat-select [(ngModel)]="selectedBranch" (selectionChange)="onBranchChange()" [disabled]="loadingBranches || !selectedRepository">
|
||||
<mat-option *ngFor="let branch of branches" [value]="branch">
|
||||
{{ branch }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingBranches">hourglass_empty</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="imagesTable"
|
||||
text="Esta tabla muestra las imágenes disponibles.">
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchCommitsField"
|
||||
text="Busca un commit por mensaje. Pulsa 'enter' para iniciar la búsqueda.">
|
||||
<mat-label>Buscar commits</mat-label>
|
||||
<input matInput placeholder="Búsqueda por mensaje" [(ngModel)]="filters['message']" (keyup.enter)="loadData()"
|
||||
i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>Buscar por mensaje del commit</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="commitsTable"
|
||||
text="Esta tabla muestra los commits disponibles.">
|
||||
<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 === 'isGlobal'">
|
||||
<mat-chip>
|
||||
{{ image.isGlobal ? 'Sí' : 'No' }}
|
||||
</mat-chip>
|
||||
<td mat-cell *matCellDef="let commit">
|
||||
<ng-container *ngIf="column.columnDef === 'hexsha'">
|
||||
<code class="commit-id">
|
||||
{{ column.cell(commit) }}
|
||||
</code>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'status'">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': image.status === 'failed',
|
||||
'chip-success': image.status === 'success',
|
||||
'chip-pending': image.status === 'pending',
|
||||
'chip-in-progress': image.status === 'in-progress',
|
||||
'chip-transferring': image.status === 'transferring',
|
||||
}">
|
||||
{{ getStatusLabel(image[column.columnDef]) }}
|
||||
</mat-chip>
|
||||
<ng-container *ngIf="column.columnDef === 'message'">
|
||||
<div class="commit-message">
|
||||
{{ column.cell(commit) }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal'">
|
||||
{{ column.cell(image) }}
|
||||
<ng-container *ngIf="column.columnDef === 'stats_total'">
|
||||
<div class="commit-stats">
|
||||
{{ column.cell(commit) }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'tags'">
|
||||
<div class="commit-tags">
|
||||
<mat-chip *ngFor="let tag of commit.tags" color="primary" selected>
|
||||
{{ tag }}
|
||||
</mat-chip>
|
||||
<span *ngIf="!commit.tags || commit.tags.length === 0" class="no-tags">
|
||||
Sin tags
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef !== 'hexsha' && column.columnDef !== 'message' && column.columnDef !== 'stats_total' && column.columnDef !== 'tags'">
|
||||
{{ column.cell(commit) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<td mat-cell *matCellDef="let image" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="goToPage(image)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<td mat-cell *matCellDef="let commit" style="text-align: center;">
|
||||
<div class="action-buttons">
|
||||
<a [href]="'http://localhost:3100/oggit/' + selectedRepository + '/commit/' + commit.hexsha" target="_blank" matTooltip="Ver commit en Git">
|
||||
<button mat-icon-button color="primary">
|
||||
<mat-icon>open_in_new</mat-icon>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<button mat-icon-button color="primary" (click)="toggleAction(image, 'edit')">
|
||||
<mat-icon i18n="@@deleteElementTooltip">edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="toggleAction(image, 'delete-trash')">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item
|
||||
(click)="toggleAction(image, 'show-tags')">Ver tags</button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'show-branches')">Ver ramas</button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'transfer')">Transferir imagen</button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'transfer-global')">Transferir imagen globalmente </button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'backup')">Realizar backup </button>
|
||||
</mat-menu>
|
||||
<button mat-icon-button color="primary" (click)="toggleAction(commit, 'view-details')" matTooltip="Ver detalles">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ShowGitImagesComponent } from './show-git-images.component';
|
||||
import { ShowGitCommitsComponent } from './show-git-images.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
@ -17,9 +17,9 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
|
||||
describe('ShowGitImagesComponent', () => {
|
||||
let component: ShowGitImagesComponent;
|
||||
let fixture: ComponentFixture<ShowGitImagesComponent>;
|
||||
describe('ShowGitCommitsComponent', () => {
|
||||
let component: ShowGitCommitsComponent;
|
||||
let fixture: ComponentFixture<ShowGitCommitsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
|
@ -27,7 +27,7 @@ describe('ShowGitImagesComponent', () => {
|
|||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ShowGitImagesComponent, LoadingComponent],
|
||||
declarations: [ShowGitCommitsComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
|
@ -52,7 +52,7 @@ describe('ShowGitImagesComponent', () => {
|
|||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ShowGitImagesComponent);
|
||||
fixture = TestBed.createComponent(ShowGitCommitsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
|
|
@ -16,12 +16,12 @@ import {BackupImageComponent} from "../backup-image/backup-image.component";
|
|||
import {EditImageComponent} from "../edit-image/edit-image.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-git-images',
|
||||
selector: 'app-show-git-commits',
|
||||
templateUrl: './show-git-images.component.html',
|
||||
styleUrl: './show-git-images.component.css'
|
||||
})
|
||||
export class ShowGitImagesComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
export class ShowGitCommitsComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
length: number = 0;
|
||||
|
@ -32,46 +32,50 @@ baseUrl: string;
|
|||
alertMessage: string | null = null;
|
||||
repository: any = {};
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
branches: string[] = [];
|
||||
selectedBranch: string = '';
|
||||
loadingBranches: boolean = false;
|
||||
repositories: string[] = [];
|
||||
selectedRepository: string = '';
|
||||
loadingRepositories: boolean = false;
|
||||
|
||||
private initialLoad = true;
|
||||
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'Id',
|
||||
cell: (image: any) => `${image.id}`
|
||||
columnDef: 'hexsha',
|
||||
header: 'Commit ID',
|
||||
cell: (commit: any) => commit.hexsha
|
||||
},
|
||||
{
|
||||
columnDef: 'repositoryName',
|
||||
header: 'Nombre del repositorio',
|
||||
cell: (image: any) => image.image?.name
|
||||
columnDef: 'message',
|
||||
header: 'Mensaje del commit',
|
||||
cell: (commit: any) => commit.message
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'Nombre de imagen',
|
||||
cell: (image: any) => image.name
|
||||
columnDef: 'committed_date',
|
||||
header: 'Fecha del commit',
|
||||
cell: (commit: any) => `${this.datePipe.transform(commit.committed_date * 1000, 'dd/MM/yyyy hh:mm:ss')}`
|
||||
},
|
||||
{
|
||||
columnDef: 'tag',
|
||||
header: 'Tag',
|
||||
cell: (image: any) => image.tag
|
||||
columnDef: 'size',
|
||||
header: 'Tamaño',
|
||||
cell: (commit: any) => `${commit.size} bytes`
|
||||
},
|
||||
{
|
||||
columnDef: 'isGlobal',
|
||||
header: 'Imagen global',
|
||||
cell: (image: any) => image.image?.isGlobal
|
||||
columnDef: 'stats_total',
|
||||
header: 'Estadísticas',
|
||||
cell: (commit: any) => {
|
||||
if (commit.stats_total) {
|
||||
return `+${commit.stats_total.insertions} -${commit.stats_total.deletions} (${commit.stats_total.files} archivos)`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
{
|
||||
columnDef: 'status',
|
||||
header: 'Estado',
|
||||
cell: (image: any) => image.status
|
||||
},
|
||||
{
|
||||
columnDef: 'description',
|
||||
header: 'Descripción',
|
||||
cell: (image: any) => image.description
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
header: 'Fecha de creación',
|
||||
cell: (image: any) => `${this.datePipe.transform(image.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
|
||||
columnDef: 'tags',
|
||||
header: 'Tags',
|
||||
cell: (commit: any) => commit.tags?.length > 0 ? commit.tags.join(', ') : 'Sin tags'
|
||||
}
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
|
@ -83,214 +87,129 @@ baseUrl: string;
|
|||
private joyrideService: JoyrideService,
|
||||
private configService: ConfigService,
|
||||
private router: Router,
|
||||
public dialogRef: MatDialogRef<ShowGitImagesComponent>,
|
||||
public dialogRef: MatDialogRef<ShowGitCommitsComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/git-image-repositories`;
|
||||
this.apiUrl = `${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.loadData();
|
||||
this.loadRepositories();
|
||||
}
|
||||
}
|
||||
|
||||
loadRepositories(): void {
|
||||
this.loadingRepositories = true;
|
||||
this.http.get<any>(`${this.apiUrl}/get-collection`).subscribe(
|
||||
data => {
|
||||
this.repositories = data.repositories || [];
|
||||
this.loadingRepositories = false;
|
||||
if (this.repositories.length > 0) {
|
||||
this.selectedRepository = this.repositories[0];
|
||||
this.loadBranches();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching repositories', error);
|
||||
this.toastService.error('Error al cargar los repositorios');
|
||||
this.loadingRepositories = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onRepositoryChange(): void {
|
||||
this.selectedBranch = '';
|
||||
this.branches = [];
|
||||
this.page = 0;
|
||||
this.loadBranches();
|
||||
}
|
||||
|
||||
loadBranches(): void {
|
||||
if (!this.selectedRepository) {
|
||||
return;
|
||||
}
|
||||
this.loadingBranches = true;
|
||||
this.http.post<any>(`${this.apiUrl}/branches`, { repositoryName: this.selectedRepository }).subscribe(
|
||||
data => {
|
||||
this.branches = data.branches || [];
|
||||
this.loadingBranches = false;
|
||||
if (this.branches.length > 0) {
|
||||
this.selectedBranch = this.branches[0];
|
||||
this.loadData();
|
||||
if (this.initialLoad) {
|
||||
this.initialLoad = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching branches', error);
|
||||
this.toastService.error('Error al cargar las ramas del repositorio');
|
||||
this.loadingBranches = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onBranchChange(): void {
|
||||
this.page = 0;
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
loadData(): void {
|
||||
if (!this.selectedBranch || !this.selectedRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&repository.id=${this.data.repositoryId}`, { params: this.filters }).subscribe(
|
||||
const payload = {
|
||||
repositoryName: this.selectedRepository,
|
||||
branch: this.selectedBranch
|
||||
};
|
||||
|
||||
this.http.post<any>(`${this.apiUrl}/commits`, payload).subscribe(
|
||||
data => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.dataSource.data = data.commits || [];
|
||||
this.length = data.commits?.length || 0;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching image repositories', error);
|
||||
console.error('Error fetching commits', error);
|
||||
this.toastService.error('Error al cargar los commits');
|
||||
this.loading = false;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getStatusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'Pendiente';
|
||||
case 'in-progress':
|
||||
return 'En progreso';
|
||||
case 'aux-files-pending':
|
||||
return 'Archivos auxiliares pendientes';
|
||||
case 'success':
|
||||
return 'Creado con éxito';
|
||||
case 'trash':
|
||||
return 'Papelera temporal';
|
||||
case 'failed':
|
||||
return 'Fallido';
|
||||
case 'transferring':
|
||||
return 'Transfiriendo';
|
||||
default:
|
||||
return 'Estado desconocido';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.length = event.length;
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
loadImageAlert(image: any): Observable<any> {
|
||||
return this.http.get<any>(`${this.apiUrl}/server/${image.uuid}/get`, {});
|
||||
}
|
||||
|
||||
importImage(): void {
|
||||
this.dialog.open(ImportImageComponent, {
|
||||
width: '600px',
|
||||
data: {
|
||||
repositoryUuid: this.data.repositoryUuid,
|
||||
name: this.data.repositoryName
|
||||
}
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.loadData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleAction(image: any, action:string): void {
|
||||
toggleAction(commit: any, action: string): void {
|
||||
switch (action) {
|
||||
case 'delete-trash':
|
||||
if (!image.imageFullsum) {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '400px',
|
||||
data: { name: image.name },
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this.http.delete(`${this.baseUrl}${image['@id']}`).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Image deleted successfully');
|
||||
this.loadData()
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error('Error deleting image');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/delete-trash`,
|
||||
{ repository: `/image-repositories/${this.data.repositoryUuid}` })
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Petición de eliminación de la papelera temporal enviada');
|
||||
this.loadData()
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
case 'delete-permanent':
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: image.name },
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/delete-permanent`, {}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Petición de eliminación de la papelera temporal enviada');
|
||||
this.loadData()
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'edit':
|
||||
this.dialog.open(EditImageComponent, {
|
||||
width: '600px',
|
||||
case 'view-details':
|
||||
this.dialog.open(ServerInfoDialogComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
image: image,
|
||||
}
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.loadData();
|
||||
title: 'Detalles del Commit',
|
||||
content: {
|
||||
'Commit ID': commit.hexsha,
|
||||
'Mensaje': commit.message,
|
||||
'Fecha': this.datePipe.transform(commit.committed_date * 1000, 'dd/MM/yyyy hh:mm:ss'),
|
||||
'Tamaño': `${commit.size} bytes`,
|
||||
'Archivos modificados': commit.stats_total?.files || 0,
|
||||
'Líneas añadidas': commit.stats_total?.insertions || 0,
|
||||
'Líneas eliminadas': commit.stats_total?.deletions || 0,
|
||||
'Tags': commit.tags?.join(', ') || 'Sin tags'
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'recover':
|
||||
this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/recover`, {}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Petición de recuperación de la imagen enviada');
|
||||
this.loadData()
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'transfer':
|
||||
this.http.get(`${this.baseUrl}${image.image['@id']}`).subscribe({
|
||||
next: (response) => {
|
||||
this.dialog.open(ExportImageComponent, {
|
||||
width: '600px',
|
||||
data: {
|
||||
image: response,
|
||||
imageImageRepository: image
|
||||
}
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'transfer-global':
|
||||
this.http.post<any>(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/transfer-global`, {
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Petición de exportación de imagen realizada correctamente');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: error => {
|
||||
this.loading = false;
|
||||
this.toastService.error('Error en la petición de exportación de imagen');
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'backup':
|
||||
this.http.get(`${this.baseUrl}${image.image['@id']}`).subscribe({
|
||||
next: (response) => {
|
||||
this.dialog.open(BackupImageComponent, {
|
||||
width: '600px',
|
||||
data: {
|
||||
image: response,
|
||||
imageImageRepository: image
|
||||
}
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'show-tags':
|
||||
this.http.get(`${this.baseUrl}/git-image-repositories/server/${image.uuid}/get-tags`, {}).subscribe({
|
||||
next: (response) => {
|
||||
this.dialog.open(ServerInfoDialogComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
repositories: response
|
||||
}
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
case 'copy-commit-id':
|
||||
navigator.clipboard.writeText(commit.hexsha).then(() => {
|
||||
this.toastService.success('Commit ID copiado al portapapeles');
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
@ -302,55 +221,40 @@ baseUrl: string;
|
|||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'imagesTitleStep',
|
||||
'addImageButton',
|
||||
'searchImagesField',
|
||||
'imagesTable',
|
||||
'commitsTitleStep',
|
||||
'repositorySelector',
|
||||
'branchSelector',
|
||||
'searchCommitsField',
|
||||
'commitsTable',
|
||||
'actionsHeader',
|
||||
'editImageButton',
|
||||
'deleteImageButton',
|
||||
'imagesPagination'
|
||||
'viewCommitButton',
|
||||
'copyCommitButton',
|
||||
'commitsPagination'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
loadAlert(): Observable<any> {
|
||||
return this.http.post<any>(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/get-collection`, {});
|
||||
}
|
||||
|
||||
syncRepository() {
|
||||
this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/sync`, {})
|
||||
.subscribe(response => {
|
||||
this.toastService.success('Sincronización completada');
|
||||
this.loadData()
|
||||
}, error => {
|
||||
console.error('Error al sincronizar', error);
|
||||
this.toastService.error('Error al sincronizar');
|
||||
});
|
||||
}
|
||||
|
||||
openImageInfoDialog() {
|
||||
this.loadAlert().subscribe(
|
||||
response => {
|
||||
this.alertMessage = response.repositories;
|
||||
|
||||
this.dialog.open(ServerInfoDialogComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
repositories: this.alertMessage
|
||||
}
|
||||
});
|
||||
},
|
||||
error => {
|
||||
console.error('Error al cargar la información del alert', error);
|
||||
this.dialog.open(ServerInfoDialogComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
title: 'Información del Repositorio',
|
||||
content: {
|
||||
'Nombre del repositorio': this.selectedRepository || 'No seleccionado',
|
||||
'UUID del repositorio': this.data.repositoryUuid,
|
||||
'Rama seleccionada': this.selectedBranch || 'No seleccionada',
|
||||
'Total de repositorios': this.repositories.length,
|
||||
'Total de ramas': this.branches.length,
|
||||
'Total de commits': this.length
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
goToPage( image: any) {
|
||||
window.location.href = `http://192.168.68.20:3000/oggit/${image.image.name}`;
|
||||
goToPage(commit: any) {
|
||||
window.open(`http://localhost:3100/oggit/${this.selectedRepository}/commit/${commit.hexsha}`, '_blank');
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
|
|
|
@ -98,3 +98,8 @@ table {
|
|||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue