Merge pull request 'develop' (#13) from develop into main
Reviewed-on: #13pull/16/head opengnsys_devel-1.4.0
commit
67ebc5b926
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -1,10 +1,24 @@
|
|||
# Changelog
|
||||
## [0.9.0] - 2025-3-4
|
||||
### 🔹 Added
|
||||
- Integracion con Mercure. Subscriber tanto en "Trazas" con en "Clientes".
|
||||
- Nueva funcionalidad para checkear la integridad de una imagen. Boton en apartado "imagenes" dentro del repositorio.
|
||||
- Centralizacion de estilos.
|
||||
- Nueva funcionalidad para realizar backup de imágenes.
|
||||
|
||||
### ⚡ Changed
|
||||
- Nueva interfaz en "Grupos". Se ha aprovechado mejor el espacio y acortado el tamaño de las filas, para poder tener mas elementos por pantalla.
|
||||
- Cambios en filtros de "Grupos". Ahora se pueden filtrar por "Centro" y "Unidad Organizativa" y estado. Ahora se busca en base de datos, y no en una lista de clientes dados.
|
||||
- Refactorizados compontentes de crear/editar clientes en uno solo.
|
||||
- Cambios en DHCP. Nueva UX en "ver clientes". Ahora tenemos un buscador detallado.
|
||||
- Para gestionar/añadir clientes a subredes ahora tenemos un botón para "añadir todos" y tan solo nos aparecn los equipos que no estén previamente asignados en una subred.
|
||||
|
||||
## [0.7.0] - 2024-12-10
|
||||
|
||||
### Refactored
|
||||
- Refactored the group screen, removing the separate tabs for clients, advanced search, and organizational units.
|
||||
- Added support for partitioning functionality in the client detail view.
|
||||
- Boton para cancelar despliegues de imagenes. Aparece en "trazas" tan solo para los comendos "deploy" y para el estado "en progreos".
|
||||
|
||||
## [0.6.1] - 2024-11-19
|
||||
|
||||
|
@ -15,7 +29,7 @@
|
|||
|
||||
## [0.6.0] - 2024-11-19
|
||||
|
||||
### Added
|
||||
### 🔹 Added
|
||||
- Added functionality to execute actions from the menu in the general groups screen.
|
||||
- Displayed the selected center on the general screen for better context.
|
||||
- Implemented the option to collapse the sidebar for improved usability.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
oggui (1.0) unstable; urgency=low
|
||||
|
||||
* Initial release.
|
||||
|
||||
-- Your Name <nicolas.arenas@qindel.com> Thu, 01 Jan 1970 00:00:00 +0000
|
|
@ -0,0 +1,9 @@
|
|||
Package: oggui
|
||||
Version: %%VERSION%%
|
||||
Section: base
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Depends: nginx , npm , nodejs
|
||||
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
|
||||
Description: Description of the ogcore package
|
||||
This is a longer description of the ogcore package.
|
|
@ -0,0 +1,21 @@
|
|||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: ogcore
|
||||
Source: <source URL>
|
||||
|
||||
Files: *
|
||||
Copyright: 2023 Your Name <your.email@example.com>
|
||||
License: GPL-3+
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this package; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
MA 02110-1301 USA.
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Asegurarse de que el usuario exista
|
||||
USER="opengnsys"
|
||||
HASH_FILE="/opt/opengnsys/oggui/var/lib/oggui/oggui.config.hash"
|
||||
CONFIG_FILE="/opt/opengnsys/oggui/src/.env"
|
||||
|
||||
# Provisionar base de datos si es necesario en caso de instalación.
|
||||
|
||||
|
||||
# Detectar si es una instalación nueva o una actualización
|
||||
if [ "$1" = "configure" ] && [ -z "$2" ]; then
|
||||
cd /opt/opengnsys/oggui/src/
|
||||
npm install -g @angular/cli
|
||||
npm install
|
||||
/usr/local/bin/ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production --localize=false
|
||||
cp -pr /opt/opengnsys/oggui/src/dist/oggui/browser/* /opt/opengnsys/oggui/browser/
|
||||
md5sum "$CONFIG_FILE" > "$HASH_FILE"
|
||||
ln -s /opt/opengnsys/oggui/etc/systemd/system/oggui.service /etc/systemd/system/oggui.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable oggui
|
||||
elif [ "$1" = "configure" ] && [ -n "$2" ]; then
|
||||
cd /opt/opengnsys/oggui
|
||||
echo "Actualización desde la versión $2"
|
||||
fi
|
||||
|
||||
# Cambiar la propiedad de los archivos al usuario especificado
|
||||
chown opengnsys:www-data /opt/opengnsys/
|
||||
chown -R opengnsys:www-data /opt/opengnsys/oggui
|
||||
# Install http server stuff
|
||||
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf
|
||||
# Reiniciar servicios si es necesario
|
||||
# systemctl restart nombre_del_servicio
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl restart nginx
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
NGINX_FILE="/etc/nginx/sites-enabled/oggui.conf"
|
||||
UNIT_FILE="/etc/systemd/system/oggui.service"
|
||||
|
||||
|
||||
case "$1" in
|
||||
remove)
|
||||
echo "El paquete se está desinstalando..."
|
||||
# Aquí puedes hacer limpieza de archivos o servicios
|
||||
if [ -L "$NGINX_FILE" ]; then
|
||||
rm -f "$NGINX_FILE"
|
||||
systemctl restart nginx
|
||||
fi
|
||||
if [ -L "$UNIT_FILE" ]; then
|
||||
rm -f "$UNIT_FILE"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
;;
|
||||
purge)
|
||||
echo "Eliminando configuración residual..."
|
||||
;;
|
||||
|
||||
upgrade)
|
||||
echo "Actualizando paquete..."
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Asegurarse de que el usuario exista
|
||||
USER="opengnsys"
|
||||
HOME_DIR="/opt/opengnsys"
|
||||
if id "$USER" &>/dev/null; then
|
||||
echo "El usuario $USER ya existe."
|
||||
else
|
||||
echo "Creando el usuario $USER con home en $HOME_DIR."
|
||||
useradd -m -d "$HOME_DIR" -s /bin/bash "$USER"
|
||||
fi
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,34 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
CONFIG_FILE="/opt/opengnsys/oggui/src/.env"
|
||||
HASH_FILE="/opt/opengnsys/oggui/var/lib/oggui/oggui.config.hash"
|
||||
APP_DIR="/opt/opengnsys/oggui/browser"
|
||||
SRC_DIR="/opt/opengnsys/oggui/src"
|
||||
COMPILED_DIR=$SRC_DIR/dist
|
||||
NGINX_SERVICE="nginx"
|
||||
|
||||
# Verificar si el archivo de configuración cambió
|
||||
if [ -f "$CONFIG_FILE" ] && [ -f "$HASH_FILE" ]; then
|
||||
OLD_HASH=$(cat "$HASH_FILE")
|
||||
NEW_HASH=$(md5sum "$CONFIG_FILE")
|
||||
|
||||
if [ "$OLD_HASH" != "$NEW_HASH" ]; then
|
||||
echo "🔄 Cambios detectados en $CONFIG_FILE, recompilando Angular..."
|
||||
cd "$SRC_DIR"
|
||||
npm install -g @angular/cli
|
||||
npm install
|
||||
/usr/local/bin/ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production --localize=false
|
||||
md5sum "$CONFIG_FILE" > "$HASH_FILE"
|
||||
exit 0
|
||||
else
|
||||
echo "No hay cambios en $CONFIG_FILE, no es necesario recompilar."
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "Archivo de configuración no encontrado o sin hash previo. No se recompilará."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Iniciar Nginx
|
||||
systemctl restart "$NGINX_SERVICE"
|
|
@ -0,0 +1,19 @@
|
|||
server {
|
||||
listen 4200;
|
||||
server_name localhost;
|
||||
|
||||
root /opt/opengnsys/oggui/browser;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Manejo de archivos estáticos
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Configuración para evitar problemas con rutas de Angular
|
||||
error_page 404 /index.html;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=Aplicación Angular con Nginx
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/opt/opengnsys/oggui/bin/start-oggui.sh
|
||||
Restart=always
|
||||
User=www-data
|
||||
WorkingDirectory=/var/www/mi-aplicacion
|
||||
Environment=NODE_ENV=production
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -7,7 +7,7 @@
|
|||
"i18n": {
|
||||
"sourceLocale": "es",
|
||||
"locales": {
|
||||
"en-US": "src/locale/en.json"
|
||||
"en": "src/locale/en.json"
|
||||
}
|
||||
},
|
||||
"projectType": "application",
|
||||
|
@ -66,8 +66,8 @@
|
|||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
"maximumWarning": "4kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
|
|
@ -13,14 +13,11 @@ import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.co
|
|||
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 { OgdhcpComponent } from './components/ogdhcp/ogdhcp.component';
|
||||
import { OgDhcpSubnetsComponent } from './components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.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/commands/commands-task/task-logs/task-logs.component';
|
||||
import { StatusComponent } from "./components/ogdhcp/og-dhcp-subnets/status/status.component";
|
||||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import {SoftwareComponent} from "./components/software/software.component";
|
||||
|
@ -41,6 +38,8 @@ import {
|
|||
} 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";
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||
{ path: '', component: MainLayoutComponent,
|
||||
|
@ -55,7 +54,6 @@ const routes: Routes = [
|
|||
{ path: 'pxe', component: PxeComponent },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent },
|
||||
{ path: 'dhcp', component: OgdhcpComponent },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent },
|
||||
{ path: 'ogdhcp-status', component: StatusComponent },
|
||||
{ path: 'commands', component: CommandsComponent },
|
||||
|
|
|
@ -25,6 +25,7 @@ import { MatListModule } from '@angular/material/list';
|
|||
import { UsersComponent } from './components/admin/users/users/users.component';
|
||||
import { RolesComponent } from './components/admin/roles/roles/roles.component';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { AddUserModalComponent } from './components/admin/users/users/add-user-modal/add-user-modal.component';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
|
@ -34,14 +35,12 @@ import { GroupsComponent } from './components/groups/groups.component';
|
|||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { CreateClientComponent } from './components/groups/shared/clients/create-client/create-client.component';
|
||||
import { DeleteModalComponent } from './shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { EditClientComponent } from './components/groups/shared/clients/edit-client/edit-client.component';
|
||||
import { ClassroomViewComponent } from './components/groups/shared/classroom-view/classroom-view.component';
|
||||
import { MatProgressSpinner } from "@angular/material/progress-spinner";
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu";
|
||||
import {MatAutocomplete, MatAutocompleteTrigger} from "@angular/material/autocomplete";
|
||||
import { MatAutocomplete, MatAutocompleteTrigger } from "@angular/material/autocomplete";
|
||||
import { MatChip, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule } from "@angular/material/chips";
|
||||
import { ClientViewComponent } from './components/groups/shared/client-view/client-view.component';
|
||||
import { MatTab, MatTabGroup } from "@angular/material/tabs";
|
||||
|
@ -63,7 +62,6 @@ import { LegendComponent } from './components/groups/shared/legend/legend.compon
|
|||
import { ClassroomViewDialogComponent } from './components/groups/shared/classroom-view/classroom-view-modal';
|
||||
import { MatPaginator } from "@angular/material/paginator";
|
||||
import { SaveFiltersDialogComponent } from './components/groups/shared/save-filters-dialog/save-filters-dialog.component';
|
||||
import { AcctionsModalComponent } from './components/groups/shared/acctions-modal/acctions-modal.component';
|
||||
import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.component';
|
||||
import { CreatePXEImageComponent } from './components/ogboot/pxe-images/create-image/create-image/create-image.component';
|
||||
import { InfoImageComponent } from './components/ogboot/pxe-images/info-image/info-image/info-image.component';
|
||||
|
@ -72,12 +70,7 @@ import { CreatePxeTemplateComponent } from './components/ogboot/pxe/create-pxeTe
|
|||
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
|
||||
import { MatExpansionPanel, MatExpansionPanelDescription, MatExpansionPanelTitle } from "@angular/material/expansion";
|
||||
import { OgbootStatusComponent } from './components/ogboot/ogboot-status/ogboot-status.component';
|
||||
import { CreatePxeBootFileComponent } from './components/ogboot/pxe-boot-files/create-pxeBootFile/create-pxe-boot-file/create-pxe-boot-file.component';
|
||||
import { NgxChartsModule } from '@swimlane/ngx-charts';
|
||||
import { OgdhcpComponent } from './components/ogdhcp/ogdhcp.component';
|
||||
import { OgDhcpSubnetsComponent } from './components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component';
|
||||
import { CreateSubnetComponent } from './components/ogdhcp/og-dhcp-subnets/create-subnet/create-subnet.component';
|
||||
import { AddClientsToSubnetComponent } from './components/ogdhcp/og-dhcp-subnets/add-clients-to-subnet/add-clients-to-subnet.component';
|
||||
import { CommandsComponent } from './components/commands/main-commands/commands.component';
|
||||
import { CommandDetailComponent } from './components/commands/main-commands/detail-command/command-detail.component';
|
||||
import { CreateCommandComponent } from './components/commands/main-commands/create-command/create-command.component';
|
||||
|
@ -85,7 +78,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
|
|||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { CalendarComponent } from './components/calendar/calendar.component';
|
||||
import { CreateCalendarComponent } from './components/calendar/create-calendar/create-calendar.component';
|
||||
import {MatRadioButton, MatRadioGroup} from "@angular/material/radio";
|
||||
import { MatRadioButton, MatRadioGroup } from "@angular/material/radio";
|
||||
import { CreateCalendarRuleComponent } from './components/calendar/create-calendar-rule/create-calendar-rule.component';
|
||||
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
|
||||
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
|
||||
|
@ -94,13 +87,11 @@ import { DetailCommandGroupComponent } from './components/commands/commands-grou
|
|||
import { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component';
|
||||
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-task.component';
|
||||
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
|
||||
import { ServerInfoDialogComponent } from './components/ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component';
|
||||
import { StatusComponent } from './components/ogdhcp/og-dhcp-subnets/status/status.component';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import { CreateImageComponent } from './components/images/create-image/create-image.component';
|
||||
import { CreateClientImageComponent} from './components/groups/components/client-main-view/create-image/create-image.component';
|
||||
import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.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';
|
||||
|
@ -124,11 +115,20 @@ import { MenusComponent } from './components/menus/menus.component';
|
|||
import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component';
|
||||
import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component';
|
||||
import { ExportImageComponent } from './components/images/export-image/export-image.component';
|
||||
import {ImportImageComponent} from "./components/repositories/import-image/import-image.component";
|
||||
import { ImportImageComponent } from "./components/repositories/import-image/import-image.component";
|
||||
import { LoadingComponent } from './shared/loading/loading.component';
|
||||
import { RepositoryImagesComponent } from './components/repositories/repository-images/repository-images.component';
|
||||
import { InputDialogComponent } from './components/commands/commands-task/task-logs/input-dialog/input-dialog.component';
|
||||
import { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
|
||||
import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
|
||||
import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||
import { StatusComponent } from "./components/ogdhcp/status/status.component";
|
||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import { CreateSubnetComponent } from "./components/ogdhcp/create-subnet/create-subnet.component";
|
||||
import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component";
|
||||
import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component';
|
||||
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component';
|
||||
import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component';
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
}
|
||||
|
@ -149,16 +149,14 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
AddRoleModalComponent,
|
||||
ChangePasswordModalComponent,
|
||||
GroupsComponent,
|
||||
CreateClientComponent,
|
||||
ManageClientComponent,
|
||||
DeleteModalComponent,
|
||||
EditClientComponent,
|
||||
ClassroomViewComponent,
|
||||
ClientViewComponent,
|
||||
ShowOrganizationalUnitComponent,
|
||||
LegendComponent,
|
||||
ClassroomViewDialogComponent,
|
||||
SaveFiltersDialogComponent,
|
||||
AcctionsModalComponent,
|
||||
PXEimagesComponent,
|
||||
CreatePXEImageComponent,
|
||||
InfoImageComponent,
|
||||
|
@ -166,8 +164,6 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
CreatePxeTemplateComponent,
|
||||
PxeBootFilesComponent,
|
||||
OgbootStatusComponent,
|
||||
CreatePxeBootFileComponent,
|
||||
OgdhcpComponent,
|
||||
OgDhcpSubnetsComponent,
|
||||
CreateSubnetComponent,
|
||||
AddClientsToSubnetComponent,
|
||||
|
@ -215,55 +211,59 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
RepositoryImagesComponent,
|
||||
InputDialogComponent,
|
||||
ManageOrganizationalUnitComponent,
|
||||
BackupImageComponent,
|
||||
ShowClientsComponent,
|
||||
OperationResultDialogComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
AppRoutingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatToolbarModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatSidenavModule,
|
||||
NoopAnimationsModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatTableModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatDividerModule,
|
||||
MatProgressBarModule,
|
||||
MatStepperModule,
|
||||
DragDropModule,
|
||||
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip,
|
||||
MatExpansionModule,
|
||||
NgxChartsModule,
|
||||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
MatSliderModule,
|
||||
MatSortModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
JoyrideModule.forRoot(),
|
||||
ToastrModule.forRoot(
|
||||
{
|
||||
timeOut: 5000,
|
||||
positionClass: 'toast-bottom-right',
|
||||
preventDuplicates: true,
|
||||
progressBar: true,
|
||||
progressAnimation: 'increasing',
|
||||
closeButton: true
|
||||
}
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger
|
||||
],
|
||||
imports: [BrowserModule,
|
||||
AppRoutingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatToolbarModule,
|
||||
MatIconModule,
|
||||
MatButtonToggleModule,
|
||||
MatButtonModule,
|
||||
MatSidenavModule,
|
||||
NoopAnimationsModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatTableModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatDividerModule,
|
||||
MatProgressBarModule,
|
||||
MatStepperModule,
|
||||
DragDropModule,
|
||||
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip,
|
||||
MatExpansionModule,
|
||||
NgxChartsModule,
|
||||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
MatSliderModule,
|
||||
MatSortModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
JoyrideModule.forRoot(),
|
||||
ToastrModule.forRoot(
|
||||
{
|
||||
timeOut: 5000,
|
||||
positionClass: 'toast-bottom-right',
|
||||
preventDuplicates: true,
|
||||
progressBar: true,
|
||||
progressAnimation: 'increasing',
|
||||
closeButton: true
|
||||
}
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
],
|
||||
|
|
|
@ -14,16 +14,6 @@
|
|||
margin: 0 10px;
|
||||
}
|
||||
|
||||
/* Estilos de los botones */
|
||||
button {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
/* Estilos del texto debajo de los botones */
|
||||
span{
|
||||
margin: 0;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<div class="container">
|
||||
<button mat-fab color="primary" class="fab-button" routerLink="/users">
|
||||
<button class="action-button" routerLink="/users">
|
||||
<mat-icon>group</mat-icon>
|
||||
<span>{{ 'labelUsers' | translate }}</span>
|
||||
</button>
|
||||
<button mat-fab color="primary" class="fab-button" routerLink="/user-groups">
|
||||
<button class="action-button" routerLink="/user-groups">
|
||||
<mat-icon>admin_panel_settings</mat-icon>
|
||||
<span>{{ 'labelRoles' | translate }}</span>
|
||||
</button>
|
||||
|
|
|
@ -45,13 +45,4 @@ describe('AdminComponent', () => {
|
|||
expect(button).toBeTruthy();
|
||||
expect(button.querySelector('mat-icon').textContent.trim()).toBe('group');
|
||||
});
|
||||
|
||||
|
||||
it('debería aplicar la clase "fab-button" a ambos botones', () => {
|
||||
const buttons = fixture.nativeElement.querySelectorAll('button');
|
||||
buttons.forEach((button: HTMLElement) => {
|
||||
expect(button.classList).toContain('fab-button');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -15,10 +15,6 @@
|
|||
gap: 16px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
|
||||
button {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
</mat-table>
|
||||
|
||||
<div class="actions">
|
||||
<button mat-raised-button color="primary" (click)="saveEnvVars()">Guardar Cambios</button>
|
||||
<button mat-raised-button color="accent" (click)="loadEnvVars()">Recargar</button>
|
||||
<button class="action-button" (click)="loadEnvVars()">Recargar</button>
|
||||
<button class="submit-button" (click)="saveEnvVars()">Guardar Cambios</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,9 @@
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin-top: 2em;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
|
@ -15,16 +17,24 @@
|
|||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
.checkbox-group {
|
||||
margin: 15px 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.time-fields {
|
||||
display: flex;
|
||||
gap: 15px; /* Espacio entre los campos */
|
||||
gap: 15px;
|
||||
/* Espacio entre los campos */
|
||||
}
|
||||
|
||||
.time-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
</section>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()">{{ 'buttonAdd' | translate }}</button>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()">{{ 'buttonAdd' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<h2>{{ 'adminRolesTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="addUser()">
|
||||
<button class="action-button" (click)="addUser()">
|
||||
{{ 'addRole' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -1,30 +1,12 @@
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.form-container {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
margin: 15px 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.time-fields {
|
||||
.user-form {
|
||||
display: flex;
|
||||
gap: 15px; /* Espacio entre los campos */
|
||||
flex-direction: column;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.time-field {
|
||||
flex: 1;
|
||||
}
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
<h1 mat-dialog-title>{{ 'dialogTitleAddUser' | translate }}</h1>
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h1 mat-dialog-title>{{ isEditMode ? ('dialogTitleEditUser' | translate) : ('dialogTitleAddUser' | translate) }}</h1>
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="userForm" class="user-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
|
@ -27,9 +29,18 @@
|
|||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Vista tarjetas</mat-label>
|
||||
<mat-select formControlName="groupsView" required>
|
||||
<mat-option *ngFor="let option of views" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()">{{ 'buttonAdd' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" [disabled]="userForm.invalid">{{ isEditMode ? ('buttonEdit' | translate) : ('buttonAdd' | translate) }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {DataService} from "../data.service";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { DataService } from "../data.service";
|
||||
|
||||
interface UserGroup {
|
||||
'@id': string;
|
||||
|
@ -19,10 +19,18 @@ interface UserGroup {
|
|||
export class AddUserModalComponent implements OnInit {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
@Output() userAdded = new EventEmitter<void>();
|
||||
@Output() userEdited = new EventEmitter<void>();
|
||||
userForm: FormGroup<any>;
|
||||
userGroups: UserGroup[] = [];
|
||||
organizationalUnits: any[] = [];
|
||||
userId: string | null = null;
|
||||
loading: boolean = false;
|
||||
isEditMode: boolean = false;
|
||||
|
||||
protected views = [
|
||||
{ value: 'card', name: 'Tarjetas' },
|
||||
{ value: 'list', name: 'Listado' },
|
||||
];
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<AddUserModalComponent>,
|
||||
|
@ -36,40 +44,52 @@ export class AddUserModalComponent implements OnInit {
|
|||
username: ['', Validators.required],
|
||||
password: ['', Validators.required],
|
||||
role: ['', Validators.required],
|
||||
groupsView: ['card', Validators.required],
|
||||
organizationalUnits: [[]]
|
||||
});
|
||||
|
||||
if (data) {
|
||||
this.isEditMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.dataService.getUserGroups().subscribe((data) => {
|
||||
this.userGroups = data['hydra:member'];
|
||||
});
|
||||
|
||||
this.dataService.getOrganizationalUnits().subscribe((data) => {
|
||||
this.organizationalUnits = data['hydra:member'].filter((item: any) => item.type === 'organizational-unit');
|
||||
});
|
||||
|
||||
if (this.data) {
|
||||
this.load()
|
||||
this.load();
|
||||
} else {
|
||||
this.userForm.get('password')?.setValidators([Validators.required]);
|
||||
}
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.dataService.getUser(this.data).subscribe({
|
||||
next: (response) => {
|
||||
console.log(response);
|
||||
|
||||
const organizationalUnitIds = response.allowedOrganizationalUnits.map((unit: any) => unit['@id']);
|
||||
|
||||
// Patch the values to the form
|
||||
this.userForm.patchValue({
|
||||
username: response.username,
|
||||
role: response.userGroups[0]['@id'],
|
||||
organizationalUnits: organizationalUnitIds
|
||||
role: response.userGroups.length > 0 ? response.userGroups[0]['@id'] : null,
|
||||
organizationalUnits: organizationalUnitIds,
|
||||
groupsView: response.groupsView
|
||||
});
|
||||
|
||||
this.userId = response['@id'];
|
||||
this.userForm.get('password')?.clearValidators();
|
||||
this.userForm.get('password')?.updateValueAndValidity();
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error fetching remote calendar:', err);
|
||||
this.loading = false;
|
||||
console.error('Error fetching user:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -80,37 +100,49 @@ export class AddUserModalComponent implements OnInit {
|
|||
|
||||
onSubmit(): void {
|
||||
if (this.userForm.valid) {
|
||||
const payload = {
|
||||
const payload: any = {
|
||||
username: this.userForm.value.username,
|
||||
allowedOrganizationalUnits: this.userForm.value.organizationalUnit,
|
||||
password: this.userForm.value.password,
|
||||
allowedOrganizationalUnits: this.userForm.value.organizationalUnits,
|
||||
enabled: true,
|
||||
userGroups: [this.userForm.value.role ]
|
||||
userGroups: [this.userForm.value.role],
|
||||
groupsView: this.userForm.value.groupsView
|
||||
};
|
||||
|
||||
if (!this.userId && this.userForm.value.password) {
|
||||
payload.password = this.userForm.value.password;
|
||||
} else if (this.userId && this.userForm.value.password.trim() !== '') {
|
||||
payload.password = this.userForm.value.password;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
if (this.userId) {
|
||||
this.http.put(`${this.baseUrl}${this.userId}`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Usuario editado correctamente');
|
||||
this.userEdited.emit();
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error['error']['hydra:description']);
|
||||
console.error('Error al editar el rol', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/users`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Usuario añadido correctamente');
|
||||
this.userAdded.emit();
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error['error']['hydra:description']);
|
||||
console.error('Error al añadir añadido', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
.user-form .form-field {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
|
@ -17,3 +16,14 @@ mat-spinner {
|
|||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin-top: 2em;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<h1 mat-dialog-title>{{ 'dialogTitleEditUser' | translate }}</h1>
|
||||
<h1 mat-dialog-title>{{ 'dialogTitleChangePassword' | translate }}</h1>
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="userForm" class="user-form">
|
||||
<mat-form-field class="form-field">
|
||||
|
@ -23,7 +23,7 @@
|
|||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="loading">{{ 'buttonEdit' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" [disabled]="loading">{{ 'buttonEdit' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -3,13 +3,12 @@
|
|||
<h2>{{ 'adminUsersTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="addUser()">
|
||||
<button class="action-button" (click)="addUser()">
|
||||
{{ 'addUser' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
|
||||
|
@ -26,7 +25,16 @@
|
|||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let user"> {{ column.cell(user) }} </td>
|
||||
<td mat-cell *matCellDef="let user">
|
||||
<ng-container *ngIf="column.columnDef === 'groupsView'">
|
||||
<mat-chip>
|
||||
{{ user[column.columnDef] === 'card' ? 'Vista tarjetas' : 'Listado' }}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef !== 'groupsView'">
|
||||
{{ column.cell(user) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
|
|
|
@ -32,6 +32,11 @@ export class UsersComponent implements OnInit {
|
|||
header: 'Nombre de Usuario',
|
||||
cell: (user: any) => `${user.username}`
|
||||
},
|
||||
{
|
||||
columnDef: 'groupsView',
|
||||
header: 'Vista de Grupos',
|
||||
cell: (user: any) => `${user.groupsView}`
|
||||
},
|
||||
{
|
||||
columnDef: 'allowedOrganizationalUnits',
|
||||
header: 'Unidades Organizacionales Permitidas',
|
||||
|
@ -87,6 +92,10 @@ export class UsersComponent implements OnInit {
|
|||
data: user['@id']
|
||||
});
|
||||
|
||||
dialogRef.componentInstance.userEdited.subscribe(() => {
|
||||
this.search();
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.search();
|
||||
|
@ -124,4 +133,4 @@ export class UsersComponent implements OnInit {
|
|||
this.length = event.length;
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@
|
|||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCalendarsTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="calendar-button-row">
|
||||
<button joyrideStep="addButtonStep" text="{{ 'addButtonStepText' | translate }}" mat-flat-button color="primary"
|
||||
(click)="addImage()">
|
||||
<button joyrideStep="addButtonStep" text="{{ 'addButtonStepText' | translate }}" class="action-button"
|
||||
(click)="addCalendar()">
|
||||
{{ 'addCalendar' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -66,7 +66,7 @@ export class CalendarComponent implements OnInit {
|
|||
this.search();
|
||||
}
|
||||
|
||||
addImage(): void {
|
||||
addCalendar(): void {
|
||||
const dialogRef = this.dialog.open(CreateCalendarComponent, {
|
||||
width: '400px'
|
||||
});
|
||||
|
|
|
@ -28,3 +28,10 @@
|
|||
.time-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -56,10 +56,10 @@
|
|||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button
|
||||
mat-button
|
||||
class="submit-button"
|
||||
(click)="submitRule()"
|
||||
cdkFocusInitial
|
||||
[disabled]="(!isRemoteAvailable && (!busyFromHour || !busyToHour)) || (isRemoteAvailable && (!availableReason || !availableFromDate || !availableToDate))">
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.form-container {
|
||||
padding: 40px;
|
||||
}
|
||||
|
@ -12,7 +9,7 @@
|
|||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.additional-form {
|
||||
|
@ -58,3 +55,9 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div *ngIf="isEditMode" mat-subheader>{{ 'rulesHeader' | translate }}</div>
|
||||
<button mat-flat-button color="primary" *ngIf="isEditMode" (click)="createRule()" style="padding: 10px;">
|
||||
<button class="action-button" *ngIf="isEditMode" (click)="createRule()" style="padding: 10px;">
|
||||
{{ 'addRule' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -41,9 +41,9 @@
|
|||
</mat-list>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="submitForm()" [disabled]="!name || name === ''" cdkFocusInitial>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="submitForm()" [disabled]="!name || name === ''" cdkFocusInitial>
|
||||
{{ 'buttonSave' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandGroupsTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="command-groups-button-row">
|
||||
<button mat-flat-button color="primary" (click)="openCreateCommandGroupModal()" joyrideStep="addCommandGroupStep" text="{{ 'addCommandGroupStepText' | translate }}">
|
||||
<button class="action-button" (click)="openCreateCommandGroupModal()" joyrideStep="addCommandGroupStep" text="{{ 'addCommandGroupStepText' | translate }}">
|
||||
{{ 'addCommandGroup' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -35,7 +35,7 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'commands'" joyrideStep="viewCommandsStep" text="{{ 'viewCommandsStepText' | translate }}">
|
||||
<button mat-button [matMenuTriggerFor]="menu">{{ 'viewCommands' | translate }}</button>
|
||||
<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 }}
|
||||
|
|
|
@ -98,3 +98,10 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -58,19 +58,6 @@
|
|||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #3f51b5; /* Color primario */
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2c387e; /* Color primario oscuro */
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.mat-card {
|
||||
margin: 10px 0;
|
||||
|
@ -86,15 +73,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.cancel-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.create-command-group-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
@ -146,8 +124,9 @@
|
|||
color: #666;
|
||||
}
|
||||
|
||||
.command-group-actions {
|
||||
margin-top: 20px;
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
|
|
@ -49,10 +49,10 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="command-group-actions" *ngIf="!loading">
|
||||
<button mat-flat-button color="primary" (click)="toggleClientSelect()">
|
||||
<div class="action-container" *ngIf="!loading">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button [ngClass]="showClientSelect ? 'submit-button' : 'action-button'" (click)="toggleClientSelect()">
|
||||
{{ showClientSelect ? ('execute' | translate) : ('scheduleExecution' | translate) }}
|
||||
</button>
|
||||
<button mat-flat-button color="warn" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -6,7 +6,7 @@
|
|||
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'manageTasksTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="task-button-row">
|
||||
<button mat-flat-button color="primary" (click)="openCreateTaskModal()" joyrideStep="addTaskStep" text="{{ 'addTaskStepText' | translate }}">
|
||||
<button class="action-button" (click)="openCreateTaskModal()" joyrideStep="addTaskStep" text="{{ 'addTaskStepText' | translate }}">
|
||||
{{ 'addTask' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
|
|
|
@ -87,10 +87,6 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="button-container">
|
||||
<button mat-raised-button color="primary" (click)="saveTask()">{{ 'buttonSave' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Clientes</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
|
@ -102,10 +98,9 @@
|
|||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="button-container">
|
||||
<button mat-raised-button color="primary" (click)="saveTask()">Guardar</button>
|
||||
</div>
|
||||
|
||||
</mat-dialog-content>
|
||||
</form>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="submit-button" (click)="saveTask()">{{ 'buttonSave' | translate }}</button>
|
||||
</div>
|
||||
|
|
|
@ -57,26 +57,4 @@
|
|||
justify-content: flex-end;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2c387e;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cancel-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
|
@ -50,6 +50,6 @@
|
|||
</mat-card>
|
||||
|
||||
<div class="task-actions">
|
||||
<button mat-flat-button class="cancel-button" (click)="closeDialog()">{{ 'buttonClose' | translate }}</button>
|
||||
<button class="ordinary-button" (click)="closeDialog()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
<pre>{{ data.input | json }}</pre>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">{{ 'closeButton' | translate }}</button>
|
||||
<button class="ordinary-button" (click)="close()">{{ 'closeButton' | translate }}</button>
|
||||
</div>
|
||||
|
|
|
@ -91,4 +91,28 @@ table {
|
|||
.chip-in-progress {
|
||||
background-color: #f5a623 !important;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.status-progress-flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button.cancel-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
color: red;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cancel-button mat-icon {
|
||||
color: red;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep"
|
||||
<button class="action-button" (click)="resetFilters()" joyrideStep="resetFiltersStep"
|
||||
text="{{ 'resetFiltersStepText' | translate }}">
|
||||
{{ 'resetFilters' | translate }}
|
||||
</button>
|
||||
|
@ -20,7 +20,7 @@
|
|||
<mat-form-field appearance="fill" class="search-select" joyrideStep="clientSelectStep"
|
||||
text="{{ 'clientSelectStepText' | translate }}">
|
||||
<input type="text" matInput [formControl]="clientControl" [matAutocomplete]="clientAuto"
|
||||
placeholder="{{ 'selectClientPlaceholder' | translate }}">
|
||||
placeholder="{{ 'filterClientPlaceholder' | translate }}">
|
||||
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient"
|
||||
(optionSelected)="onOptionClientSelected($event.option.value)">
|
||||
<mat-option *ngFor="let client of filteredClients | async" [value]="client">
|
||||
|
@ -32,7 +32,7 @@
|
|||
<mat-form-field appearance="fill" class="search-select" joyrideStep="commandSelectStep"
|
||||
text="{{ 'commandSelectStepText' | translate }}">
|
||||
<input type="text" matInput [formControl]="commandControl" [matAutocomplete]="commandAuto"
|
||||
placeholder="{{ 'selectCommandPlaceholder' | translate }}">
|
||||
placeholder="{{ 'filterCommandPlaceholder' | translate }}">
|
||||
<mat-autocomplete #commandAuto="matAutocomplete" [displayWith]="displayFnCommand"
|
||||
(optionSelected)="onOptionCommandSelected($event.option.value)">
|
||||
<mat-option *ngFor="let command of filteredCommands | async" [value]="command">
|
||||
|
@ -44,10 +44,12 @@
|
|||
<mat-form-field appearance="fill" class="search-boolean">
|
||||
<mat-label i18n="@@searchLabel">Estado</mat-label>
|
||||
<mat-select [(ngModel)]="filters['status']" (selectionChange)="loadTraces()" placeholder="Seleccionar opción">
|
||||
<mat-option [value]="undefined">Todos</mat-option>
|
||||
<mat-option [value]="'failed'">Fallido</mat-option>
|
||||
<mat-option [value]="'pending'">Pendiente de ejecutar</mat-option>
|
||||
<mat-option [value]="'in-progress'">Ejecutando</mat-option>
|
||||
<mat-option [value]="'success'">Completado con éxito</mat-option>
|
||||
<mat-option [value]="'cancelled'">Cancelado</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
@ -62,41 +64,49 @@
|
|||
<td mat-cell *matCellDef="let trace">
|
||||
|
||||
<ng-container [ngSwitch]="column.columnDef">
|
||||
<!-- Caso para "status" -->
|
||||
<ng-container *ngSwitchCase="'status'">
|
||||
<ng-container *ngIf="trace.status === 'in-progress' && trace.progress; else statusChip">
|
||||
<div class="progress-container">
|
||||
<mat-progress-bar class="example-margin" [mode]="mode" [value]="progress" [bufferValue]="bufferValue">
|
||||
<mat-progress-bar class="example-margin" [mode]="mode" [value]="trace.progress" [bufferValue]="bufferValue">
|
||||
</mat-progress-bar>
|
||||
<span>{{progress}}%</span>
|
||||
<span>{{trace.progress}}%</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #statusChip>
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': trace.status === 'failed',
|
||||
'chip-success': trace.status === 'success',
|
||||
'chip-pending': trace.status === 'pending',
|
||||
'chip-in-progress': trace.status === 'in-progress'
|
||||
}">
|
||||
{{
|
||||
trace.status === 'failed' ? 'Fallido' :
|
||||
trace.status === 'in-progress' ? 'En ejecución' :
|
||||
trace.status === 'success' ? 'Finalizado con éxito' :
|
||||
trace.status === 'pending' ? 'Pendiente de ejecutar' :
|
||||
trace.status
|
||||
}}
|
||||
</mat-chip>
|
||||
<div class="status-progress-flex">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': trace.status === 'failed',
|
||||
'chip-success': trace.status === 'success',
|
||||
'chip-pending': trace.status === 'pending',
|
||||
'chip-in-progress': trace.status === 'in-progress',
|
||||
'chip-cancelled': trace.status === 'cancelled'
|
||||
}">
|
||||
{{
|
||||
trace.status === 'failed' ? 'Fallido' :
|
||||
trace.status === 'in-progress' ? 'En ejecución' :
|
||||
trace.status === 'success' ? 'Finalizado con éxito' :
|
||||
trace.status === 'pending' ? 'Pendiente de ejecutar' :
|
||||
trace.status === 'cancelled' ? 'Cancelado' :
|
||||
trace.status
|
||||
}}
|
||||
</mat-chip>
|
||||
<button *ngIf="trace.status === 'in-progress' && trace.command === 'deploy-image'"
|
||||
mat-icon-button
|
||||
(click)="cancelTrace(trace)"
|
||||
class="cancel-button"
|
||||
matTooltip="Cancelar transmisión de imagen">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<!-- Caso para "input" con modal -->
|
||||
<ng-container *ngSwitchCase="'input'">
|
||||
<button mat-icon-button (click)="openInputModal(trace.input)">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<!-- Para cualquier otro caso (default) -->
|
||||
<ng-container *ngSwitchDefault>
|
||||
{{ column.cell(trace) }}
|
||||
</ng-container>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import {ChangeDetectorRef, Component, OnInit} from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, forkJoin } from 'rxjs';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
@ -8,6 +8,8 @@ import { JoyrideService } from 'ngx-joyride';
|
|||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { InputDialogComponent } from "./input-dialog/input-dialog.component";
|
||||
import { ProgressBarMode, MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-task-logs',
|
||||
|
@ -27,7 +29,7 @@ export class TaskLogsComponent implements OnInit {
|
|||
pageSizeOptions: number[] = [10, 20, 30, 50];
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
mode: ProgressBarMode = 'buffer';
|
||||
progress = 65;
|
||||
progress = 0;
|
||||
bufferValue = 0;
|
||||
|
||||
columns = [
|
||||
|
@ -86,14 +88,16 @@ export class TaskLogsComponent implements OnInit {
|
|||
commandControl = new FormControl();
|
||||
|
||||
constructor(private http: HttpClient,
|
||||
private joyrideService: JoyrideService,
|
||||
private dialog: MatDialog
|
||||
private joyrideService: JoyrideService,
|
||||
private dialog: MatDialog,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private toastService: ToastrService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTraces();
|
||||
this.loadCommands();
|
||||
this.loadClients();
|
||||
//this.loadClients();
|
||||
this.filteredCommands = this.commandControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => (typeof value === 'string' ? value : value?.name)),
|
||||
|
@ -104,8 +108,39 @@ export class TaskLogsComponent implements OnInit {
|
|||
map(value => (typeof value === 'string' ? value : value?.name)),
|
||||
map(name => (name ? this._filterClients(name) : this.clients.slice()))
|
||||
);
|
||||
|
||||
const eventSource = new EventSource('http://localhost:3000/.well-known/mercure?topic='
|
||||
+ encodeURIComponent(`traces`));
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data && data['@id']) {
|
||||
this.updateTracesStatus(data['@id'], data.status, data.progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
|
||||
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
|
||||
if (traceIndex !== -1) {
|
||||
const updatedTraces = [...this.traces];
|
||||
|
||||
updatedTraces[traceIndex] = {
|
||||
...updatedTraces[traceIndex],
|
||||
status: newStatus,
|
||||
progress: progress
|
||||
};
|
||||
|
||||
this.traces = updatedTraces;
|
||||
this.cdr.detectChanges();
|
||||
|
||||
console.log(`Estado actualizado para la traza ${clientUuid}: ${newStatus}`);
|
||||
} else {
|
||||
console.warn(`Traza con UUID ${clientUuid} no encontrado en la lista.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _filterClients(name: string): any[] {
|
||||
const filterValue = name.toLowerCase();
|
||||
return this.clients.filter(client => client.name.toLowerCase().includes(filterValue));
|
||||
|
@ -141,10 +176,34 @@ export class TaskLogsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
cancelTrace(trace: any): void {
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: trace.jobId },
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Transmision de imagen cancelada');
|
||||
this.loadTraces();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
console.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadTraces(): void {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/traces?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`;
|
||||
this.http.get<any>(url, { params: this.filters }).subscribe(
|
||||
const params = { ...this.filters };
|
||||
if (params['status'] === undefined) {
|
||||
delete params['status'];
|
||||
}
|
||||
this.http.get<any>(url, { params }).subscribe(
|
||||
(data) => {
|
||||
this.traces = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
|
@ -163,6 +222,7 @@ export class TaskLogsComponent implements OnInit {
|
|||
this.http.get<any>(`${this.baseUrl}/commands?&page=1&itemsPerPage=10000`).subscribe(
|
||||
response => {
|
||||
this.commands = response['hydra:member'];
|
||||
console.log(this.commands);
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
|
@ -176,10 +236,20 @@ export class TaskLogsComponent implements OnInit {
|
|||
this.loading = true;
|
||||
this.http.get<any>(`${this.baseUrl}/clients?&page=1&itemsPerPage=10000`).subscribe(
|
||||
response => {
|
||||
this.clients = response['hydra:member'];
|
||||
this.loading = false;
|
||||
const clientIds = response['hydra:member'].map((client: any) => client['@id']);
|
||||
const clientDetailsRequests: Observable<any>[] = clientIds.map((id: string) => this.http.get<any>(`${this.baseUrl}${id}`));
|
||||
forkJoin(clientDetailsRequests).subscribe(
|
||||
(clients: any[]) => {
|
||||
this.clients = clients;
|
||||
this.loading = false;
|
||||
},
|
||||
(error: any) => {
|
||||
console.error('Error fetching client details:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
},
|
||||
error => {
|
||||
(error: any) => {
|
||||
console.error('Error fetching clients:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'CommandsTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="command-button-row" joyrideStep="addCommandStep" text="{{ 'addCommandStepText' | translate }}">
|
||||
<button mat-flat-button color="primary" (click)="openCreateCommandModal()">{{ 'addCommand' | translate }}</button>
|
||||
<button class="action-button" (click)="openCreateCommandModal()">{{ 'addCommand' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -7,12 +7,6 @@
|
|||
|
||||
.form-group {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.additional-form {
|
||||
|
@ -58,3 +52,9 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -23,7 +23,7 @@
|
|||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onCancel($event)">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onCancel($event)">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
<ng-container [ngSwitch]="buttonType">
|
||||
<button *ngSwitchCase="'icon'" mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu">
|
||||
<button *ngSwitchCase="'icon'" mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu"
|
||||
[disabled]="disabled">
|
||||
<mat-icon>{{ icon }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-flat-button [disabled]="clientData.length === 0" *ngSwitchCase="'text'" mat-button color="primary" [matMenuTriggerFor]="commandMenu">
|
||||
<button class="action-button" [disabled]="clientData.length === 0 || disabled" *ngSwitchCase="'text'"
|
||||
[matMenuTriggerFor]="commandMenu">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<mat-menu #commandMenu="matMenu" >
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="command.disabled || (command.slug === 'create-image' && clientData.length > 1)"
|
||||
*ngFor="let command of arrayCommands"
|
||||
(click)="onCommandSelect(command.slug)"
|
||||
>
|
||||
<mat-menu #commandMenu="matMenu">
|
||||
<button mat-menu-item [disabled]="command.disabled || (command.slug === 'create-image' && clientData.length > 1)"
|
||||
*ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
|
||||
{{ command.name }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</mat-menu>
|
|
@ -15,6 +15,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
@Input() buttonType: 'icon' | 'text' = 'icon';
|
||||
@Input() buttonText: string = 'Ejecutar Comandos';
|
||||
@Input() icon: string = 'terminal';
|
||||
@Input() disabled: boolean = false;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
loading: boolean = true;
|
||||
|
||||
|
@ -47,12 +48,6 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
this.clientData = this.clientData || [];
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['clientData']) {
|
||||
console.log(this.clientData.length)
|
||||
}
|
||||
}
|
||||
|
||||
onCommandSelect(action: any): void {
|
||||
if (action === 'partition') {
|
||||
this.openPartitionAssistant();
|
||||
|
|
|
@ -32,20 +32,24 @@
|
|||
.charts-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between; /* Distribuye el espacio entre los gráficos */
|
||||
gap: 20px; /* Añade espacio entre los gráficos */
|
||||
justify-content: space-between;
|
||||
/* Distribuye el espacio entre los gráficos */
|
||||
gap: 20px;
|
||||
/* Añade espacio entre los gráficos */
|
||||
}
|
||||
|
||||
.disk-usage {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
min-width: 200px; /* Ajusta este valor según el tamaño mínimo deseado para cada gráfico */
|
||||
min-width: 200px;
|
||||
/* Ajusta este valor según el tamaño mínimo deseado para cada gráfico */
|
||||
}
|
||||
|
||||
.circular-chart {
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
margin: 0 auto; /* Centra el gráfico dentro del contenedor */
|
||||
margin: 0 auto;
|
||||
/* Centra el gráfico dentro del contenedor */
|
||||
}
|
||||
|
||||
.chart {
|
||||
|
@ -86,7 +90,7 @@
|
|||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
|
@ -134,8 +138,10 @@
|
|||
.client-button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between; /* Distribuye el espacio entre los gráficos */
|
||||
gap: 20px; /* Añade espacio entre los gráficos */
|
||||
justify-content: space-between;
|
||||
/* Distribuye el espacio entre los gráficos */
|
||||
gap: 20px;
|
||||
/* Añade espacio entre los gráficos */
|
||||
}
|
||||
|
||||
.buttons-row {
|
||||
|
@ -221,10 +227,26 @@
|
|||
}
|
||||
|
||||
/* Define colores distintos para cada partición */
|
||||
.partition-0 { stroke: #00bfa5; } /* Ejemplo: verde */
|
||||
.partition-1 { stroke: #ff6f61; } /* Ejemplo: rojo */
|
||||
.partition-2 { stroke: #ffb400; } /* Ejemplo: amarillo */
|
||||
.partition-3 { stroke: #3498db; } /* Ejemplo: azul */
|
||||
.partition-0 {
|
||||
stroke: #00bfa5;
|
||||
}
|
||||
|
||||
/* Ejemplo: verde */
|
||||
.partition-1 {
|
||||
stroke: #ff6f61;
|
||||
}
|
||||
|
||||
/* Ejemplo: rojo */
|
||||
.partition-2 {
|
||||
stroke: #ffb400;
|
||||
}
|
||||
|
||||
/* Ejemplo: amarillo */
|
||||
.partition-3 {
|
||||
stroke: #3498db;
|
||||
}
|
||||
|
||||
/* Ejemplo: azul */
|
||||
|
||||
/* Texto en el centro del gráfico */
|
||||
.percentage {
|
||||
|
@ -258,6 +280,26 @@
|
|||
}
|
||||
|
||||
.back-button {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-left: 0.5em;
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
padding: 8px 18px 8px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.back-button:hover:not(:disabled) {
|
||||
background-color: #485ac0d7;
|
||||
}
|
||||
|
||||
.back-button:disabled {
|
||||
background-color: #ced0df;
|
||||
cursor: not-allowed;
|
||||
}
|
|
@ -1,24 +1,13 @@
|
|||
<div class="header-container">
|
||||
|
||||
<h2 class="title">{{ 'clientDetailsTitle' | translate }}</h2>
|
||||
<div class="client-button-row">
|
||||
<button mat-flat-button color="primary" (click)="onEditClick($event, clientData.uuid)"
|
||||
i18n="@@editImage">Editar</button>
|
||||
<button mat-flat-button color="primary" [matMenuTriggerFor]="commandMenu">{{ 'commandsButton' | translate
|
||||
}}</button>
|
||||
</div>
|
||||
<mat-menu #commandMenu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
|
||||
{{ command.name }}
|
||||
<button class="back-button" (click)="navigateToGroups()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
{{ 'Back' | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="navigateToGroups()" class="back-button">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
{{ 'Back' | translate }}
|
||||
</button>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading" class="client-info">
|
||||
|
|
|
@ -5,8 +5,8 @@ import {MatTableDataSource} from "@angular/material/table";
|
|||
import {PartitionAssistantComponent} from "./partition-assistant/partition-assistant.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {Router} from "@angular/router";
|
||||
import {EditClientComponent} from "../../shared/clients/edit-client/edit-client.component";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import { ManageClientComponent } from "../../shared/clients/manage-client/manage-client.component";
|
||||
|
||||
interface ClientInfo {
|
||||
property: string;
|
||||
|
@ -198,7 +198,7 @@ export class ClientMainViewComponent implements OnInit {
|
|||
|
||||
onEditClick(event: MouseEvent, uuid: string): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' } );
|
||||
const dialogRef = this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' } );
|
||||
dialogRef.afterClosed().subscribe();
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" (click)="save()">Ejecutar</button>
|
||||
<button class="action-button" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
|
|
@ -31,6 +31,10 @@ table {
|
|||
gap: 10px;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" (click)="save()">Ejecutar</button>
|
||||
<button class="action-button" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
@ -89,56 +89,59 @@
|
|||
</table>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
<h3 *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">Opciones multicast</h3>
|
||||
<h3 *ngIf="isMethod('p2p')" class="input-group">Opciones torrent</h3>
|
||||
<div *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Puerto</mat-label>
|
||||
<input matInput [(ngModel)]="mcastPort" name="mcastPort">
|
||||
</mat-form-field>
|
||||
<div class="options-container">
|
||||
<h3 *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">Opciones multicast</h3>
|
||||
<h3 *ngIf="isMethod('p2p')" class="input-group">Opciones torrent</h3>
|
||||
<div *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Puerto</mat-label>
|
||||
<input matInput [(ngModel)]="mcastPort" name="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Dirección</mat-label>
|
||||
<input matInput [(ngModel)]="mcastIp" name="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Dirección</mat-label>
|
||||
<input matInput [(ngModel)]="mcastIp" name="mcastIp">
|
||||
</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">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</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">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Velocidad</mat-label>
|
||||
<input matInput [(ngModel)]="mcastSpeed" name="mcastSpeed">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Velocidad</mat-label>
|
||||
<input matInput [(ngModel)]="mcastSpeed" name="mcastSpeed" type="number">
|
||||
</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">
|
||||
</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">
|
||||
</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">
|
||||
</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">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isMethod('p2p')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
||||
<mat-select [(ngModel)]="p2pMode" name="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isMethod('p2p')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
||||
<mat-select [(ngModel)]="p2pMode" name="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ 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>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,7 @@ export class DeployImageComponent {
|
|||
selectedMethod: string | null = null;
|
||||
selectedPartition: any = null;
|
||||
mcastIp: string = '';
|
||||
mcastPort: string = '';
|
||||
mcastPort: Number = 0;
|
||||
mcastMode: string = '';
|
||||
mcastSpeed: Number = 0;
|
||||
mcastMaxClients: Number = 0;
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.disk-size {
|
||||
|
|
|
@ -7,10 +7,9 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" [disabled]="data.status === 'busy'" (click)="save()">Ejecutar</button>
|
||||
<button class="action-button" [disabled]="data.status === 'busy'" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<mat-expansion-panel hideToggle>
|
||||
|
@ -19,13 +18,10 @@
|
|||
<mat-panel-description> Listado de clientes donde se realizará el particionado </mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="clients-grid" >
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of clientData" class="client-item">
|
||||
<div class="client-card">
|
||||
<img
|
||||
[src]="'assets/images/ordenador_' + client.status + '.png'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
<img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon" class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name }}</span>
|
||||
|
@ -54,86 +50,74 @@
|
|||
|
||||
<div class="partition-assistant" *ngIf="selectedDisk">
|
||||
<div class="partition-bar">
|
||||
<div
|
||||
*ngFor="let partition of activePartitions(selectedDisk.diskNumber)"
|
||||
<div *ngFor="let partition of activePartitions(selectedDisk.diskNumber)"
|
||||
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
|
||||
class="partition-segment"
|
||||
>
|
||||
class="partition-segment">
|
||||
{{ partition.partitionCode }} ({{ (partition.size / 1024).toFixed(2) }} GB)
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button mat-flat-button color="primary" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
|
||||
<button class="action-button" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-container">
|
||||
<table class="partition-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Partición</th>
|
||||
<th>Tipo partición</th>
|
||||
<th>S. ficheros</th>
|
||||
<th>Tamaño (MB)</th>
|
||||
<th>Tamaño (%)</th>
|
||||
<th>Formatear</th>
|
||||
<th>Eliminar</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Partición</th>
|
||||
<th>Tipo partición</th>
|
||||
<th>S. ficheros</th>
|
||||
<th>Tamaño (MB)</th>
|
||||
<th>Tamaño (%)</th>
|
||||
<th>Formatear</th>
|
||||
<th>Eliminar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let partition of selectedDisk.partitions; let j = index">
|
||||
<tr *ngIf="!partition.removed">
|
||||
<td>{{ partition.partitionNumber }}</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.partitionCode" required>
|
||||
<option *ngFor="let type of partitionTypes" [value]="type.name">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.filesystem" required>
|
||||
<option *ngFor="let type of filesystemTypes" [value]="type.name">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.size" required
|
||||
(input)="updatePartitionSize(selectedDisk.diskNumber, j, partition.size)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.percentage"
|
||||
(input)="updatePartitionSizeFromPercentage(selectedDisk.diskNumber, j, partition.percentage)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" [(ngModel)]="partition.format" />
|
||||
</td>
|
||||
<td>
|
||||
<button (click)="removePartition(selectedDisk.diskNumber, partition)" class="remove-btn">X</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let partition of selectedDisk.partitions; let j = index">
|
||||
<tr *ngIf="!partition.removed">
|
||||
<td>{{ partition.partitionNumber }}</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.partitionCode" required>
|
||||
<option *ngFor="let type of partitionTypes" [value]="type.name">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.filesystem" required>
|
||||
<option *ngFor="let type of filesystemTypes" [value]="type.name">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" [(ngModel)]="partition.size" required
|
||||
(input)="updatePartitionSize(selectedDisk.diskNumber, j, partition.size)" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" [(ngModel)]="partition.percentage"
|
||||
(input)="updatePartitionSizeFromPercentage(selectedDisk.diskNumber, j, partition.percentage)" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" [(ngModel)]="partition.format" />
|
||||
</td>
|
||||
<td>
|
||||
<button (click)="removePartition(selectedDisk.diskNumber, partition)" class="remove-btn">X</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<ngx-charts-pie-chart
|
||||
[view]="view"
|
||||
[results]="selectedDisk.chartData"
|
||||
[doughnut]="true"
|
||||
>
|
||||
<ngx-charts-pie-chart [view]="view" [results]="selectedDisk.chartData" [doughnut]="true">
|
||||
</ngx-charts-pie-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
|
||||
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
|
|
@ -1,3 +1,10 @@
|
|||
.groups-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
|
@ -7,6 +14,14 @@
|
|||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
|
@ -16,20 +31,77 @@
|
|||
.groups-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
.clients-container {
|
||||
flex-grow: 1;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
flex-direction: column;
|
||||
padding: 0rem 1rem 0rem 0.5rem;
|
||||
}
|
||||
|
||||
button[mat-raised-button] {
|
||||
.clients-view-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.clients-view {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.cards-view {
|
||||
max-height: calc(100vh - 330px);
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cards-select-all {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
padding-top: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.clients-table {
|
||||
max-height: calc(100vh - 330px);
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clients-table table {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.cards-paginator {
|
||||
width: 100%;
|
||||
margin-left: 1rem;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.list-paginator {
|
||||
width: 100%;
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
.actions mat-icon {
|
||||
|
@ -42,15 +114,6 @@ button[mat-raised-button] {
|
|||
color: #1976d2;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
font-size: 16px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
@ -61,42 +124,7 @@ button[mat-raised-button] {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.details-wrapper {
|
||||
width: 95%;
|
||||
padding: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.details-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button[mat-raised-button] {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.card-container {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
@ -108,29 +136,6 @@ button[mat-raised-button] {
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
mat-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.unidad-card {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 30px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
mat-tree {
|
||||
background-color: #f9f9f9;
|
||||
padding: 0px 10px 10px 10px;
|
||||
|
@ -243,47 +248,6 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.filters-container {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
gap: 1rem;
|
||||
margin: 2rem 0px 0.7rem 10px;
|
||||
}
|
||||
|
||||
.filters-container mat-form-field {
|
||||
flex: 1 1 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.chip-busy {
|
||||
background-color: indianred !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.chip-og-live {
|
||||
background-color: yellow !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.chip-windows,
|
||||
.chip-windows-session,
|
||||
.chip-macos {
|
||||
background-color: cornflowerblue !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-linux,
|
||||
.chip-linux-session {
|
||||
background-color: mediumpurple !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-off {
|
||||
background-color: darkgrey !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.clients-card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -292,59 +256,41 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.classroom-item {
|
||||
flex: 1 1 calc(25% - 16px);
|
||||
max-width: calc(25% - 16px);
|
||||
box-sizing: border-box;
|
||||
.client-name {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.classroom-pc {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
.filters-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
justify-content: start;
|
||||
padding: 1em 1em 0em 1em;
|
||||
}
|
||||
|
||||
.classroom-pc .pc-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 8px;
|
||||
.filter-form-field {
|
||||
min-width: 21rem;
|
||||
}
|
||||
|
||||
.pc-details {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pc-details .client-name,
|
||||
.pc-details .client-ip,
|
||||
.pc-details .client-mac {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pc-actions button {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
.filters-and-tree-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.filters-panel {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
width: 25%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.clients-container {
|
||||
width: 75%;
|
||||
box-sizing: border-box;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100% - var(--filters-panel-height));
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
|
@ -354,6 +300,7 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
|
||||
.client-card {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
|
@ -364,6 +311,7 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
align-items: center;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.client-card:hover {
|
||||
|
@ -372,17 +320,10 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
}
|
||||
|
||||
.client-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
display: block;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
|
@ -391,50 +332,12 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
color: #666;
|
||||
}
|
||||
|
||||
button[mat-raised-button] {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 4px; /* Espaciado reducido entre cards */
|
||||
}
|
||||
|
||||
.clients-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.client-item-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.client-details-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr); /* 6 columnas por fila */
|
||||
gap: 16px; /* Espaciado entre tarjetas */
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.clients-list .list-item-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-icons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -442,23 +345,8 @@ button[mat-raised-button] {
|
|||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.client-card, .list-item-content {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.client-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.client-info {
|
||||
|
@ -468,46 +356,21 @@ button[mat-raised-button] {
|
|||
margin: 5px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-self: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.client-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.clients-view-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
@media (max-width: 1560px) {
|
||||
.clients-view-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.clients-title-name {
|
||||
|
@ -524,3 +387,64 @@ button[mat-raised-button] {
|
|||
margin-top: 1.5rem;
|
||||
margin-left: 1.6rem;
|
||||
}
|
||||
|
||||
.view-type-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
mat-button-toggle-group {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mat-button-toggle-group .mat-button-toggle {
|
||||
height: 36px;
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.mat-button-toggle-group .mat-button-toggle:first-child {
|
||||
border-top-left-radius: 15px;
|
||||
border-bottom-left-radius: 15px;
|
||||
}
|
||||
|
||||
.mat-button-toggle-group .mat-button-toggle:last-child {
|
||||
border-top-right-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
|
||||
.mat-button-toggle-group .mat-button-toggle:not(:first-child):not(:last-child) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.mat-button-toggle-group .mat-button-toggle:hover {
|
||||
background-color: #303f9f;
|
||||
}
|
||||
|
||||
.mat-button-toggle-group .mat-button-toggle.mat-button-toggle-checked {
|
||||
background-color: #303f9f;
|
||||
}
|
||||
|
||||
.mat-button-toggle-group .mat-button-toggle.mat-button-toggle-disabled {
|
||||
background-color: #c7c7c7;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 0.5rem 1rem 1rem 1rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
|
@ -1,350 +1,398 @@
|
|||
<!-- Header -->
|
||||
<div class="header-container" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||
{{ 'adminGroupsTitle' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="groups-button-row" joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}">
|
||||
<button mat-flat-button color="primary" (click)="addOU($event)"
|
||||
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'newOrganizationalUnitButton' | translate }}
|
||||
<div class="groups-container">
|
||||
<!-- HEADER -->
|
||||
<div class="header-container" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<button mat-flat-button color="primary" [matMenuTriggerFor]="menuClients">{{ '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 }}</button>
|
||||
</mat-menu>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||
{{ 'adminGroupsTitle' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="groups-button-row" joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}">
|
||||
<button class="action-button" (click)="addOU($event)"
|
||||
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'newOrganizationalUnitButton' | translate }}
|
||||
</button>
|
||||
<button class="action-button" [matMenuTriggerFor]="menuClients">{{ '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 }}</button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-flat-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
{{ 'legendButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="initialLoading; else contentTemplate">
|
||||
<app-loading [isLoading]="initialLoading"></app-loading>
|
||||
</div>
|
||||
|
||||
<ng-template #contentTemplate>
|
||||
<!-- Filters Panel -->
|
||||
<div class="filters-panel" joyrideStep="filtersPanelStep" text="{{ 'filtersPanelStepText' | translate }}">
|
||||
<div class="filters-container">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'searchTree' | translate }}</mat-label>
|
||||
<input matInput #treeSearchInput (input)="onTreeFilterInput($event)" placeholder="Centro, aula, grupos ..." />
|
||||
<button *ngIf="treeSearchInput.value" mat-icon-button matSuffix aria-label="Clear tree search"
|
||||
(click)="clearTreeSearch(treeSearchInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'searchClient' | translate }}</mat-label>
|
||||
<input matInput #clientSearchInput (input)="onClientFilterInput($event)"
|
||||
placeholder="Nombre, IP, estado o MAC" />
|
||||
<button *ngIf="clientSearchInput.value" mat-icon-button matSuffix aria-label="Clear client search"
|
||||
(click)="clearClientSearch(clientSearchInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Funcionalidad actualmente deshabilitada-->
|
||||
<!-- <mat-form-field appearance="outline">
|
||||
<mat-select (selectionChange)="loadSelectedFilter($event.value)" placeholder="Cargar filtro" disabled>
|
||||
<mat-option *ngFor="let savedFilter of savedFilterNames" [value]="savedFilter">
|
||||
{{ savedFilter[0] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'filterByType' | translate }}</mat-label>
|
||||
<mat-select [(value)]="selectedTreeFilter" (selectionChange)="filterTree(searchTerm, $event.value)" disabled>
|
||||
<mat-option [value]=""> {{ 'all' | translate }} </mat-option>
|
||||
<mat-option value="classrooms-group">{{ 'classroomsGroup' | translate }}</mat-option>
|
||||
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
|
||||
<mat-option value="group">{{ 'computerGroups' | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field> -->
|
||||
<button class="ordinary-button" (click)="openBottomSheet()" joyrideStep="keyStep"
|
||||
text="{{ 'keyStepText' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'legendButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unit details view-->
|
||||
<div class="main-container">
|
||||
<!-- Tree view -->
|
||||
<div class="tree-container">
|
||||
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
|
||||
*matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(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)="onNodeClick(node)">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-tree-node>
|
||||
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
|
||||
*matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick(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)="onNodeClick(node)">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-tree-node>
|
||||
</mat-tree>
|
||||
</div>
|
||||
<div *ngIf="initialLoading; else contentTemplate">
|
||||
<app-loading [isLoading]="initialLoading"></app-loading>
|
||||
</div>
|
||||
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
<!-- MAIN VIEW -->
|
||||
<ng-template #contentTemplate>
|
||||
<div class="main-container">
|
||||
|
||||
<!-- Tree node actions -->
|
||||
<mat-menu restoreFocus=false #commandMenu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of commands" (click)="executeCommand(command, selectedNode)">
|
||||
<span>{{ command.name }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-menu #menuNode="matMenu">
|
||||
<button mat-menu-item (click)="onShowDetailsClick($event, selectedNode)">
|
||||
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
|
||||
<span>{{ 'viewUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item (click)="onRoomMap(selectedNode)">
|
||||
<mat-icon>map</mat-icon>
|
||||
<span>{{ 'roomMap' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addClient($event, selectedNode)">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'newSingleClientButton' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event, selectedNode)">
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
<span>{{ 'newMultipleClientButton' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addOU($event, selectedNode)">
|
||||
<mat-icon>account_tree</mat-icon>
|
||||
<span>{{ 'addOrganizationalUnit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<!-- FILTERS AND TREE VIEW -->
|
||||
<div class="filters-and-tree-container">
|
||||
|
||||
<!-- Clients view -->
|
||||
<div class="clients-container">
|
||||
<div class="clients-view-header">
|
||||
<span [ngStyle]="{ visibility: isLoadingClients ? 'hidden' : 'visible' }" class="clients-title-name">
|
||||
{{ 'clients' | translate }}
|
||||
<strong>{{ selectedNode?.name }}</strong>
|
||||
</span>
|
||||
<div class="view-type-container">
|
||||
<app-execute-command [clientData]="arrayClients" [buttonType]="'text'"
|
||||
[buttonText]="'Ejecutar comandos'"></app-execute-command>
|
||||
<button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'">
|
||||
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
|
||||
</button>
|
||||
<button mat-button color="primary" (click)="toggleView('list')" [disabled]="currentView === 'list'">
|
||||
<mat-icon>list</mat-icon> {{ 'Vista Lista' | translate }}
|
||||
</button>
|
||||
<!-- Filters -->
|
||||
<div class="filters-panel" joyrideStep="filtersPanelStep" text="{{ 'filtersPanelStepText' | translate }}">
|
||||
<div class="filters-container">
|
||||
<mat-form-field class="filter-form-field" appearance="outline">
|
||||
<mat-label>{{ 'searchTree' | translate }}</mat-label>
|
||||
<input matInput #treeSearchInput (input)="onTreeFilterInput($event)"
|
||||
placeholder="Centro, aula, grupos ..." />
|
||||
<button *ngIf="treeSearchInput.value" mat-icon-button matSuffix aria-label="Clear tree search"
|
||||
(click)="clearTreeSearch(treeSearchInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="filter-form-field" appearance="outline">
|
||||
<mat-label>{{ 'searchClient' | translate }}</mat-label>
|
||||
<input matInput #clientSearchInput (input)="onClientFilterInput($event)"
|
||||
placeholder="Nombre, IP, estado o MAC" />
|
||||
<button *ngIf="clientSearchInput.value" mat-icon-button matSuffix aria-label="Clear client search"
|
||||
(click)="clearClientSearch(clientSearchInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field search-select" appearance="outline">
|
||||
<mat-select placeholder="Buscar por estado..." #clientSearchStatusInput (selectionChange)="onClientFilterStatusInput($event.value)">
|
||||
<mat-option *ngFor="let option of status" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<button *ngIf="clientSearchStatusInput.value" mat-icon-button matSuffix aria-label="Clear tree search" (click)="clearStatusFilter($event, clientSearchStatusInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
|
||||
<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>
|
||||
<mat-option *ngFor="let savedFilter of savedFilterNames" [value]="savedFilter">
|
||||
{{ savedFilter[0] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'filterByType' | translate }}</mat-label>
|
||||
<mat-select [(value)]="selectedTreeFilter" (selectionChange)="filterTree(searchTerm, $event.value)" disabled>
|
||||
<mat-option [value]=""> {{ 'all' | translate }} </mat-option>
|
||||
<mat-option value="classrooms-group">{{ 'classroomsGroup' | translate }}</mat-option>
|
||||
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
|
||||
<mat-option value="group">{{ 'computerGroups' | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tree -->
|
||||
<div class="tree-container">
|
||||
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
|
||||
*matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(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)="onNodeClick(node)">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-tree-node>
|
||||
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
|
||||
*matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick(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)="onNodeClick(node)">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-tree-node>
|
||||
</mat-tree>
|
||||
</div>
|
||||
|
||||
<!-- Tree node actions -->
|
||||
<mat-menu restoreFocus=false #commandMenu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of commands" (click)="executeCommand(command, selectedNode)">
|
||||
<span>{{ command.name }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-menu #menuNode="matMenu">
|
||||
<button mat-menu-item (click)="onShowDetailsClick($event, selectedNode)">
|
||||
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
|
||||
<span>{{ 'viewUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item (click)="onRoomMap(selectedNode)">
|
||||
<mat-icon>map</mat-icon>
|
||||
<span>{{ 'roomMap' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addClient($event, selectedNode)">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'newSingleClientButton' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event, selectedNode)">
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
<span>{{ 'newMultipleClientButton' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addOU($event, selectedNode)">
|
||||
<mat-icon>account_tree</mat-icon>
|
||||
<span>{{ 'addOrganizationalUnit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="isLoadingClients"></app-loading>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
|
||||
<div *ngIf="!isLoadingClients">
|
||||
<div *ngIf="hasClients; else noClientsTemplate">
|
||||
<!-- Cards view -->
|
||||
<div class="clients-grid" *ngIf="currentView === 'card'">
|
||||
<div *ngFor="let client of arrayClients" class="client-item">
|
||||
<div class="client-card">
|
||||
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(client)"
|
||||
[checked]="selection.isSelected(client)" [disabled]="client.status === 'busy'">
|
||||
<!-- CLIENTS -->
|
||||
<div class="clients-container">
|
||||
|
||||
<!-- CLIENTS HEADER -->
|
||||
<div class="clients-view-header">
|
||||
<div>
|
||||
<span [ngStyle]="{ visibility: isLoadingClients ? 'hidden' : 'visible' }" class="clients-title-name">
|
||||
{{ 'clients' | translate }}
|
||||
<strong>{{ selectedNode?.name }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="view-type-container">
|
||||
<app-execute-command [clientData]="selection.selected" [buttonType]="'text'"
|
||||
[buttonText]="'Ejecutar comandos'" [disabled]="selection.selected.length === 0"></app-execute-command>
|
||||
<mat-button-toggle-group name="viewType" aria-label="View Type" [hideSingleSelectionIndicator]="true"
|
||||
(change)="toggleView($event.value)">
|
||||
<mat-button-toggle value="list" [disabled]="currentView === 'list'">
|
||||
<mat-icon>list</mat-icon> {{ 'Vista Lista' | translate }}
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="card" [disabled]="currentView === 'card'">
|
||||
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="isLoadingClients"></app-loading>
|
||||
|
||||
<!-- CLIENTS VIEWS-->
|
||||
<div class="clients-view" *ngIf="!isLoadingClients">
|
||||
<div *ngIf="hasClients; else noClientsTemplate">
|
||||
|
||||
<!-- Cards view -->
|
||||
<div *ngIf="currentView === 'card'">
|
||||
<section class="cards-view">
|
||||
<mat-checkbox class="cards-select-all" (change)="toggleAllCards()"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
</mat-checkbox>
|
||||
<img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon"
|
||||
class="client-image" />
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of arrayClients" class="client-item">
|
||||
<div class="client-card">
|
||||
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(client)"
|
||||
[checked]="selection.isSelected(client)" [disabled]="client.status === 'busy'">
|
||||
</mat-checkbox>
|
||||
<img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon"
|
||||
class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
<div class="action-icons">
|
||||
<button *ngIf="(!syncStatus || syncingClientId !== client.uuid)" mat-icon-button color="primary"
|
||||
(click)="getStatus(client, selectedNode)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
<div class="action-icons">
|
||||
<button *ngIf="(!syncStatus || syncingClientId !== client.uuid)" mat-icon-button
|
||||
color="primary" (click)="getStatus(client, selectedNode)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
|
||||
<button *ngIf="syncStatus && syncingClientId === client.uuid" mat-icon-button color="primary">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
</button>
|
||||
<button *ngIf="syncStatus && syncingClientId === client.uuid" mat-icon-button color="primary">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
</button>
|
||||
|
||||
<app-execute-command [clientData]="[client]" [buttonType]="'icon'"
|
||||
[icon]="'terminal'"></app-execute-command>
|
||||
<app-execute-command [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'"
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"></app-execute-command>
|
||||
|
||||
<button mat-icon-button [matMenuTriggerFor]="clientMenu" color="primary">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<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>
|
||||
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onShowClientDetail($event, client)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
<span>{{ 'viewDetails' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onShowClientDetail($event, client)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
<span>{{ 'viewDetails' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="paginator-container">
|
||||
<mat-paginator class="cards-paginator" [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page"
|
||||
[pageSizeOptions]="[5, 10, 20, 50, 100]" (page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List view -->
|
||||
<div *ngIf="currentView === 'list'" class="list-view">
|
||||
<section class="clients-table" tabindex="0">
|
||||
<table mat-table matSort [dataSource]="selectedClients">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox (change)="$event ? toggleAllRows() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
</mat-checkbox>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(row)"
|
||||
[checked]="selection.isSelected(row)" [disabled]="row.status === 'busy'">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
|
||||
matTooltipPosition="left" matTooltipShowDelay="500">
|
||||
<img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon"
|
||||
class="client-image" />
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
|
||||
matTooltipPosition="left" matTooltipShowDelay="500">
|
||||
<p style="font-weight: 500;">{{ client.name }}</p>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="ip">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>IP </th>
|
||||
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
|
||||
matTooltipPosition="left" matTooltipShowDelay="500">
|
||||
{{ client.ip }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="oglive">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.ogLive?.date | date }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="maintenace">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ '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>
|
||||
<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.template?.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>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ '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 [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'"
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"></app-execute-command>
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onShowClientDetail($event, client)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
<span>{{ 'viewDetails' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="getStatus(client, selectedNode)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
<span>{{ 'sync' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<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>
|
||||
</table>
|
||||
</section>
|
||||
<mat-paginator class="list-paginator" [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page"
|
||||
[pageSizeOptions]="pageSizeOptions" (page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- List view -->
|
||||
<div class="clients-table" *ngIf="currentView === 'list'">
|
||||
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox (change)="$event ? toggleAllRows() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
</mat-checkbox>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(row)"
|
||||
[checked]="selection.isSelected(row)" [disabled]="row.status === 'busy'">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" matTooltipPosition="left"
|
||||
matTooltipShowDelay="500">
|
||||
<img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon"
|
||||
class="client-image" />
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="sync">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'sync' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<button *ngIf="(!syncStatus || syncingClientId !== client.uuid)" mat-icon-button color="primary"
|
||||
(click)="getStatus(client, selectedNode)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
|
||||
<button *ngIf="syncStatus && syncingClientId === client.uuid" mat-icon-button color="primary">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" matTooltipPosition="left"
|
||||
matTooltipShowDelay="500">
|
||||
<div class="client-info">
|
||||
<div class="client-name">{{ client.name }}</div>
|
||||
<div class="client-ip">{{ client.ip }}</div>
|
||||
<div class="client-ip">{{ client.mac }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="oglive">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.ogLive?.date | date }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="maintenace">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ '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>
|
||||
<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.template?.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>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'actions' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<button mat-icon-button [matMenuTriggerFor]="clientMenu" color="primary">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<app-execute-command [clientData]="[client]" [buttonType]="'icon'"
|
||||
[icon]="'terminal'"></app-execute-command>
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onShowClientDetail($event, client)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
<span>{{ 'viewDetails' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5, 10, 20, 50]" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
<!-- No clients view -->
|
||||
<ng-template #noClientsTemplate>
|
||||
<div *ngIf="!initialLoading" class="no-clients-info">
|
||||
<span>{{ 'noClients' | translate }}</span>
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<!-- No clients view -->
|
||||
<ng-template #noClientsTemplate>
|
||||
<div *ngIf="!initialLoading" class="no-clients-info">
|
||||
<span>{{ 'noClients' | translate }}</span>
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
|
@ -123,6 +123,9 @@ describe('GroupsComponent', () => {
|
|||
spyOn(component['http'], 'get').and.callThrough();
|
||||
component.fetchClientsForNode(node);
|
||||
expect(component.isLoadingClients).toBeTrue();
|
||||
expect(component['http'].get).toHaveBeenCalledWith(`${component.baseUrl}/clients?organizationalUnit.id=${node.id}`);
|
||||
expect(component['http'].get).toHaveBeenCalledWith(
|
||||
`${component.baseUrl}/clients?organizationalUnit.id=${node.id}&page=1&itemsPerPage=20`,
|
||||
{ params: jasmine.any(Object) }
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatBottomSheet } from '@angular/material/bottom-sheet';
|
||||
|
@ -10,18 +10,19 @@ import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'
|
|||
import { Subscription } from 'rxjs';
|
||||
import { DataService } from './services/data.service';
|
||||
import { UnidadOrganizativa, Client, TreeNode, FlatNode, Command } from './model/model';
|
||||
import { CreateClientComponent } from './shared/clients/create-client/create-client.component';
|
||||
import { ManageOrganizationalUnitComponent } from './shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
|
||||
import { EditClientComponent } from './shared/clients/edit-client/edit-client.component';
|
||||
import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
||||
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 { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { CreateMultipleClientComponent } from "./shared/clients/create-multiple-client/create-multiple-client.component";
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { ManageClientComponent } from "./shared/clients/manage-client/manage-client.component";
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
enum NodeType {
|
||||
OrganizationalUnit = 'organizational-unit',
|
||||
|
@ -41,6 +42,10 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
organizationalUnits: UnidadOrganizativa[] = [];
|
||||
selectedUnidad: UnidadOrganizativa | null = null;
|
||||
selectedDetail: UnidadOrganizativa | null = null;
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 20;
|
||||
page: number = 0;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 50, 100];
|
||||
initialLoading: boolean = true;
|
||||
isLoadingClients: boolean = false;
|
||||
searchTerm = '';
|
||||
|
@ -54,18 +59,31 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
selectedClients = new MatTableDataSource<Client>([]);
|
||||
selection = new SelectionModel<any>(true, []);
|
||||
cols = 4;
|
||||
currentView: 'card' | 'list' = 'list';
|
||||
currentView: string = 'list';
|
||||
savedFilterNames: [string, string][] = [];
|
||||
selectedTreeFilter = '';
|
||||
syncStatus = false;
|
||||
syncingClientId: string | null = null;
|
||||
private originalTreeData: TreeNode[] = [];
|
||||
arrayClients: any[] = [];
|
||||
filters: { [key: string]: string } = {};
|
||||
private clientFilterSubject = new Subject<string>();
|
||||
|
||||
displayedColumns: string[] = ['select', 'status', 'sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
|
||||
protected status = [
|
||||
{ value: 'off', name: 'Apagado' },
|
||||
{ value: 'initializing', name: 'Inicializando' },
|
||||
{ value: 'og-live', name: 'Og Live' },
|
||||
{ value: 'linux', name: 'Linux' },
|
||||
{ value: 'linux-session', name: 'Linux Session' },
|
||||
{ value: 'windows', name: 'Windows' },
|
||||
{ value: 'windows-session', name: 'Windows Session' },
|
||||
{ value: 'busy', name: 'Ocupado' },
|
||||
{ value: 'mac', name: 'Mac' },
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['select', 'status', 'ip', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
|
||||
|
||||
private _sort!: MatSort;
|
||||
private _paginator!: MatPaginator;
|
||||
|
||||
@ViewChild(MatSort)
|
||||
set matSort(ms: MatSort) {
|
||||
|
@ -75,14 +93,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewChild(MatPaginator)
|
||||
set matPaginator(mp: MatPaginator) {
|
||||
this._paginator = mp;
|
||||
if (this.selectedClients) {
|
||||
this.selectedClients.paginator = this._paginator;
|
||||
}
|
||||
}
|
||||
|
||||
private subscriptions: Subscription = new Subscription();
|
||||
|
||||
constructor(
|
||||
|
@ -107,6 +117,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
|
||||
this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
|
||||
this.currentView = localStorage.getItem('groupsView') || 'list';
|
||||
}
|
||||
|
||||
|
||||
|
@ -126,6 +137,40 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
this.arrayClients = this.selectedClients.data;
|
||||
|
||||
const eventSource = new EventSource('http://localhost:3000/.well-known/mercure?topic='
|
||||
+ encodeURIComponent(`clients`));
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data && data['@id']) {
|
||||
this.updateClientStatus(data['@id'], data.status);
|
||||
}
|
||||
}
|
||||
|
||||
this.clientFilterSubject.pipe(debounceTime(500)).subscribe(searchTerm => {
|
||||
this.filters['query'] = searchTerm;
|
||||
this.filterClients(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
private updateClientStatus(clientUuid: string, newStatus: string): void {
|
||||
const clientIndex = this.selectedClients.data.findIndex(client => client['@id'] === clientUuid);
|
||||
|
||||
if (clientIndex !== -1) {
|
||||
const updatedClients = [...this.selectedClients.data];
|
||||
|
||||
updatedClients[clientIndex] = {
|
||||
...updatedClients[clientIndex],
|
||||
status: newStatus
|
||||
};
|
||||
|
||||
this.selectedClients.data = updatedClients;
|
||||
|
||||
console.log(`Estado actualizado para el cliente ${clientUuid}: ${newStatus}`);
|
||||
} else {
|
||||
console.warn(`Cliente con UUID ${clientUuid} no encontrado en la lista.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -215,7 +260,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
|
||||
private refreshData(selectedNodeIdOrUuid?: string): void {
|
||||
private refreshData(selectedNodeIdOrUuid?: string, selectedClientsBeforeEdit: string[] = []): void {
|
||||
this.dataService.getOrganizationalUnits().subscribe({
|
||||
next: (data) => {
|
||||
this.originalTreeData = data.map((unidad) => this.convertToTreeData(unidad));
|
||||
|
@ -226,13 +271,13 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
if (this.selectedNode) {
|
||||
this.treeControl.collapseAll();
|
||||
this.expandPathToNode(this.selectedNode);
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
this.fetchClientsForNode(this.selectedNode, selectedClientsBeforeEdit);
|
||||
}
|
||||
} else {
|
||||
this.treeControl.collapseAll();
|
||||
if (this.treeDataSource.data.length > 0) {
|
||||
this.selectedNode = this.treeDataSource.data[0];
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
this.fetchClientsForNode(this.selectedNode, selectedClientsBeforeEdit);
|
||||
} else {
|
||||
this.selectedNode = null;
|
||||
this.selectedClients.data = [];
|
||||
|
@ -304,15 +349,25 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
|
||||
public fetchClientsForNode(node: TreeNode): void {
|
||||
public fetchClientsForNode(node: any, selectedClientsBeforeEdit: string[] = []): void {
|
||||
const params = new HttpParams({ fromObject: this.filters });
|
||||
|
||||
this.isLoadingClients = true;
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({
|
||||
next: (response) => {
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params }).subscribe({
|
||||
next: (response: any) => {
|
||||
this.selectedClients.data = response['hydra:member'];
|
||||
this.length = response['hydra:totalItems'];
|
||||
this.arrayClients = this.selectedClients.data;
|
||||
this.hasClients = node.hasClients ?? false;
|
||||
this.isLoadingClients = false;
|
||||
this.initialLoading = false;
|
||||
this.selection.clear();
|
||||
selectedClientsBeforeEdit.forEach(uuid => {
|
||||
const client = this.selectedClients.data.find(client => client.uuid === uuid);
|
||||
if (client) {
|
||||
this.selection.select(client);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.isLoadingClients = false;
|
||||
|
@ -321,6 +376,11 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
}
|
||||
|
||||
addOU(event: MouseEvent, parent: TreeNode | null = null): void {
|
||||
event.stopPropagation();
|
||||
|
@ -339,7 +399,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
||||
event.stopPropagation();
|
||||
const targetNode = organizationalUnit || this.selectedNode;
|
||||
const dialogRef = this.dialog.open(CreateClientComponent, {
|
||||
const dialogRef = this.dialog.open(ManageClientComponent, {
|
||||
data: { organizationalUnit: targetNode },
|
||||
width: '900px',
|
||||
});
|
||||
|
@ -390,7 +450,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
const dialogRef = node?.type !== NodeType.Client
|
||||
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
|
||||
: this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
|
||||
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
if (node) {
|
||||
|
@ -454,12 +514,13 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
onEditClick(event: MouseEvent, type: string, uuid: string): void {
|
||||
event.stopPropagation();
|
||||
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(EditClientComponent, { data: { uuid }, width: '900px' });
|
||||
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.refreshData(this.selectedNode?.id);
|
||||
this.refreshData(this.selectedNode?.id, selectedClientsBeforeEdit);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -585,13 +646,21 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
onClientFilterInput(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const searchTerm = input?.value || '';
|
||||
this.filterClients(searchTerm);
|
||||
this.clientFilterSubject.next(searchTerm);
|
||||
}
|
||||
|
||||
onClientFilterStatusInput(event: Event): void {
|
||||
// @ts-ignore
|
||||
this.filters['status'] = event;
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
}
|
||||
|
||||
|
||||
filterClients(searchTerm: string): void {
|
||||
this.searchTerm = searchTerm.trim().toLowerCase();
|
||||
this.selectedClients.filter = this.searchTerm;
|
||||
//this.selectedClients.filter = this.searchTerm;
|
||||
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
this.arrayClients = this.selectedClients.filteredData;
|
||||
}
|
||||
|
||||
|
@ -639,12 +708,19 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
toggleAllRows() {
|
||||
if (this.isAllSelected()) {
|
||||
this.selection.clear();
|
||||
this.arrayClients = []
|
||||
return;
|
||||
} else {
|
||||
this.selection.select(...this.selectedClients.data);
|
||||
}
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
this.selection.select(...this.selectedClients.data);
|
||||
this.arrayClients = [...this.selection.selected];
|
||||
toggleAllCards() {
|
||||
if (this.isAllSelected()) {
|
||||
this.selection.clear();
|
||||
} else {
|
||||
this.selection.select(...this.selectedClients.data);
|
||||
}
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
|
||||
|
@ -655,7 +731,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
|
||||
updateSelectedClients() {
|
||||
this.arrayClients = [...this.selection.selected];
|
||||
this.arrayClients = this.selectedClients.data;
|
||||
}
|
||||
|
||||
|
||||
|
@ -685,6 +761,14 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
clearClientSearch(inputElement: HTMLInputElement): void {
|
||||
inputElement.value = '';
|
||||
delete this.filters['query'];
|
||||
this.filterClients('');
|
||||
}
|
||||
|
||||
clearStatusFilter(event: Event, clientSearchStatusInput: any): void {
|
||||
event.stopPropagation();
|
||||
delete this.filters['status'];
|
||||
clientSearchStatusInput.value = null;
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ export interface Client {
|
|||
"@type": string;
|
||||
id: number;
|
||||
name: string;
|
||||
ogLive: any;
|
||||
pxtTemplate: any;
|
||||
type: string;
|
||||
serialNumber: string;
|
||||
netiface: string;
|
||||
|
|
|
@ -12,7 +12,7 @@ export class DataService {
|
|||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
|
||||
private apiUrl = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
||||
private clientsUrl = `${this.baseUrl}/clients?page=1&itemsPerPage=1000`;
|
||||
private clientsUrl = `${this.baseUrl}/clients`;
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
/* Contenedor de los botones, organizados en 2 columnas */
|
||||
.button-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr; /* 2 columnas iguales */
|
||||
gap: 15px; /* Espacio entre los botones */
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* Opcional: ancho 100% para los botones */
|
||||
.button-action {
|
||||
width: 100%;
|
||||
justify-self: stretch; /* Asegura que los botones se extiendan por toda la columna */
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
<h1 mat-dialog-title>{{ 'actionsModalTitle' | translate }}</h1>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="button-container">
|
||||
<button
|
||||
*ngFor="let command of arrayCommands"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
class="button-action"
|
||||
(click)="onCommandClick(command)">
|
||||
{{ command.name }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-dialog-content>
|
|
@ -1,58 +0,0 @@
|
|||
// componente
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { CreatePxeBootFileComponent } from '../../../ogboot/pxe-boot-files/create-pxeBootFile/create-pxe-boot-file/create-pxe-boot-file.component';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { CommandDetailComponent } from '../../../commands/main-commands/detail-command/command-detail.component';
|
||||
import { RouterLink } from '@angular/router';
|
||||
@Component({
|
||||
selector: 'app-acctions-modal',
|
||||
templateUrl: './acctions-modal.component.html',
|
||||
styleUrls: ['./acctions-modal.component.css']
|
||||
})
|
||||
export class AcctionsModalComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
selectedElements: any;
|
||||
displayedColumns: string[] = ['name', 'createdBy', 'createdAt'];
|
||||
|
||||
arrayCommands: any[] = [
|
||||
{name: 'Enceder', slug: 'power-on'},
|
||||
{name: 'Apagar', slug: 'power-off'},
|
||||
{name: 'Reiniciar', slug: 'reboot'},
|
||||
{name: 'Iniciar Sesión', slug: 'login'},
|
||||
{name: 'Inventario Software', slug: 'software-inventory'},
|
||||
{name: 'Inventario Hardware', slug: 'hardware-inventory'},
|
||||
{name: 'Ejecutar script', slug: 'run-script'},
|
||||
];
|
||||
|
||||
private apiUrl = `${this.baseUrl}/commands?page=1&itemsPerPage=40`;
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
constructor(
|
||||
private toastService: ToastrService,
|
||||
public dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.selectedElements = data?.selectedElements || [];
|
||||
}
|
||||
|
||||
onSend(): void {
|
||||
this.toastService.success('Acción enviada a: ' + this.selectedElements);
|
||||
}
|
||||
|
||||
onPxeBootFile(): void {
|
||||
const dialog = this.dialog.open(CreatePxeBootFileComponent, { data: this.selectedElements, width: '400px' });
|
||||
dialog.afterClosed().subscribe(() => {
|
||||
this.dialog.closeAll();
|
||||
});
|
||||
}
|
||||
|
||||
onCommandClick(command: any): void {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -129,6 +129,6 @@ mat-dialog-content {
|
|||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.saveDisposition-btn{
|
||||
margin-top: 10px;
|
||||
}
|
||||
.submit-button {
|
||||
margin: 1rem;
|
||||
}
|
|
@ -21,5 +21,5 @@
|
|||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-raised-button color="primary" class="saveDisposition-btn" (click)="saveDisposition()">{{ 'saveDispositionButton' | translate }}</button>
|
||||
<button class="submit-button" (click)="saveDisposition()">{{ 'saveDispositionButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -21,8 +21,7 @@ export class ClassroomViewComponent implements OnInit, OnChanges {
|
|||
@Input() pcInTable: number = 5;
|
||||
groupedClients: GroupedClients[] = [];
|
||||
|
||||
constructor(public dialog: MatDialog, private http: HttpClient, private toastService: ToastrService) {}
|
||||
|
||||
constructor(public dialog: MatDialog, private http: HttpClient, private toastService: ToastrService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.groupClientsByOrganizationalUnit();
|
||||
|
@ -61,7 +60,6 @@ export class ClassroomViewComponent implements OnInit, OnChanges {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
chunkArray(arr: any[], chunkSize: number): any[][] {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||
|
@ -71,7 +69,8 @@ export class ClassroomViewComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
handleClientClick(client: any): void {
|
||||
const dialogRef = this.dialog.open(ClientViewComponent, { data: { client }, width: '800px', height:'700px' });
|
||||
console.log('Client clicked:', client);
|
||||
this.dialog.open(ClientViewComponent, { data: { client }, width: '800px', height: '700px' });
|
||||
}
|
||||
|
||||
onDragMoved(event: CdkDragMove<any>, client: any): void {
|
||||
|
@ -106,4 +105,4 @@ export class ClassroomViewComponent implements OnInit, OnChanges {
|
|||
} else
|
||||
this.toastService.success('Cliente actualizado!', 'Éxito');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
|
||||
@Component({
|
||||
selector: 'app-client-view',
|
||||
|
@ -11,54 +11,61 @@ export class ClientViewComponent {
|
|||
displayedColumns: string[] = ['property', 'value'];
|
||||
|
||||
generalData = [
|
||||
{property: 'Nombre', value: this.data.client.name},
|
||||
{property: 'Uuid', value: this.data.client.uuid},
|
||||
{property: 'IP', value: this.data.client.ip},
|
||||
{property: 'MAC', value: this.data.client.mac},
|
||||
{property: 'Nº de serie', value: this.data.client.serialNumber},
|
||||
{property: 'Netiface', value: this.data.client.netiface},
|
||||
{property: 'Perfil hardware', value: this.data.client.hardwareProfile ? this.data.client.hardwareProfile.description : ''},
|
||||
{property: 'Menú', value: this.data.client.menu ? this.data.client.menu.description : ''},
|
||||
{property: 'Fecha de creación', value: this.data.client.createdAt},
|
||||
{property: 'Creado por', value: this.data.client.createdBy}
|
||||
{ property: 'Nombre', value: this.data.client.name },
|
||||
{ property: 'Uuid', value: this.data.client.uuid },
|
||||
{ property: 'IP', value: this.data.client.ip },
|
||||
{ property: 'MAC', value: this.data.client.mac },
|
||||
{ property: 'Nº de serie', value: this.data.client.serialNumber },
|
||||
{ property: 'Netiface', value: this.data.client.netiface },
|
||||
{ property: 'Perfil hardware', value: this.data.client.hardwareProfile ? this.data.client.hardwareProfile.description : '' },
|
||||
{ property: 'Menú', value: this.data.client.menu ? this.data.client.menu.description : '' },
|
||||
{ property: 'Fecha de creación', value: this.data.client.createdAt },
|
||||
{ property: 'Creado por', value: this.data.client.createdBy }
|
||||
];
|
||||
|
||||
networkData = [
|
||||
{property: 'Menú', value: this.data.client.menu ? this.data.client.menu.description : ''},
|
||||
{property: 'Perfil hardware', value: this.data.client.hardwareProfile ? this.data.client.hardwareProfile.description : ''},
|
||||
{property: 'Subred', value: this.data.client.subnet},
|
||||
{property: 'OGlive', value: ''},
|
||||
{property: 'Autoexec', value: ''},
|
||||
{property: 'Repositorio', value: ''},
|
||||
{property: 'Validacion', value: ''},
|
||||
{property: 'Página login', value: ''},
|
||||
{property: 'Fecha de creación', value: this.data.client.createdAt},
|
||||
{property: 'Creado por', value: this.data.client.createdBy}
|
||||
{ property: 'Menú', value: this.data.client.menu ? this.data.client.menu.description : '' },
|
||||
{ property: 'Perfil hardware', value: this.data.client.hardwareProfile ? this.data.client.hardwareProfile.description : '' },
|
||||
{ property: 'Subred', value: this.data.client.subnet },
|
||||
{ property: 'OGlive', value: '' },
|
||||
{ property: 'Autoexec', value: '' },
|
||||
{ property: 'Repositorio', value: '' },
|
||||
{ property: 'Validacion', value: '' },
|
||||
{ property: 'Página login', value: '' },
|
||||
{ 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 || '' },
|
||||
{ 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 || '' },
|
||||
{ property: 'Velocidad multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastSpeed || '' },
|
||||
{ property: 'Perfil hardware', value: this.data.client.organizationalUnit?.networkSettings?.hardwareProfile?.description || '' },
|
||||
{ property: 'Menú', value: this.data.client.organizationalUnit?.networkSettings?.menu?.description || '' }
|
||||
];
|
||||
|
||||
classroomData = [
|
||||
{property: 'Url servidor proxy', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.proxy : ''},
|
||||
{property: 'IP DNS', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.dns : ''},
|
||||
{property: 'Máscara de red', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.netmask : ''},
|
||||
{property: 'Router', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.router : ''},
|
||||
{property: 'NTP', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.ntp : ''},
|
||||
{property: 'Modo p2p', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.p2pMode : ''},
|
||||
{property: 'Tiempo p2p', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.p2pTime : ''},
|
||||
{property: 'IP multicast', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.mcastIp : ''},
|
||||
{property: 'Modo multicast', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.mcastMode : ''},
|
||||
{property: 'Puerto multicast', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.mcastPort : ''},
|
||||
{property: 'Velocidad multicast', value: this.data.client.organizationalUnit.networkSettings ? this.data.client.organizationalUnit.networkSettings.mcastSpeed : ''},
|
||||
{property: 'Perfil hardware', value: this.data.client.organizationalUnit.networkSettings && this.data.client.organizationalUnit.networkSettings.hardwareProfile ? this.data.client.organizationalUnit.networkSettings.hardwareProfile.description : ''},
|
||||
{property: 'Menú', value: this.data.client.organizationalUnit.networkSettings && this.data.client.organizationalUnit.networkSettings.menu ? this.data.client.organizationalUnit.networkSettings.menu.description : ''}
|
||||
{ property: 'Url servidor proxy', value: this.data.client.organizationalUnit?.networkSettings?.proxy || '' },
|
||||
{ property: 'IP DNS', value: this.data.client.organizationalUnit?.networkSettings?.dns || '' },
|
||||
{ property: 'Máscara de red', value: this.data.client.organizationalUnit?.networkSettings?.netmask || '' },
|
||||
{ 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 || '' },
|
||||
{ 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 || '' },
|
||||
{ property: 'Velocidad multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastSpeed || '' },
|
||||
{ property: 'Perfil hardware', value: this.data.client.organizationalUnit?.networkSettings?.hardwareProfile?.description || '' },
|
||||
{ property: 'Menú', value: this.data.client.organizationalUnit?.networkSettings?.menu?.description || '' }
|
||||
];
|
||||
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<ClientViewComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
h1 {
|
||||
text-align: center;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
color: #3f51b5;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
padding: 15px 50px 15px 50px;
|
||||
}
|
||||
|
||||
button {
|
||||
text-transform: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
mat-option .unit-name {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-option .unit-path {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.create-client-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
column-gap: 20px;
|
||||
row-gap: 20px;
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
<div class="create-client-container">
|
||||
<h1 mat-dialog-title i18n="@@add-client-dialog-title">Añadir Cliente</h1>
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@organizational-unit-label">Padre</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onParentChange($event)">
|
||||
<mat-select-trigger>
|
||||
{{ getSelectedParentName() }}
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id" >
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@name-label">Nombre</mat-label>
|
||||
<input matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@oglive-label">OgLive</mat-label>
|
||||
<mat-select formControlName="ogLive">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@serial-number-label">Número de Serie</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@netiface-label">Interfaz de red</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@net-driver-label">Controlador de red</mat-label>
|
||||
<mat-select formControlName="netDriver">
|
||||
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mac-label">MAC</mat-label>
|
||||
<mat-hint i18n="@@mac-hint">Ejemplo: 00:11:22:33:44:55</mat-hint>
|
||||
<input matInput formControlName="mac">
|
||||
<mat-error i18n="@@mac-error">Formato de MAC inválido. Ejemplo válido: 00:11:22:33:44:55</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@ip-label">Dirección IP</mat-label>
|
||||
<mat-hint i18n="@@ip-hint">Ejemplo: 127.0.0.1</mat-hint>
|
||||
<input matInput formControlName="ip">
|
||||
<mat-error i18n="@@ip-error">Formato de dirección IP inválido. Ejemplo válido: 127.0.0.1</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@oglive-label">Plantilla PXE</mat-label>
|
||||
<mat-select formControlName="template">
|
||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardware-profile-label">Perfil de Hardware</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
|
||||
{{ unit.description }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error i18n="@@hardware-profile-error">Formato de URL inválido.</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardware-profile-label">Repositorio</mat-label>
|
||||
<mat-select formControlName="repository">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="menu">
|
||||
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
|
||||
{{ menu.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'menuError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()" i18n="@@cancel-button">Cancelar</button>
|
||||
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()" i18n="@@add-button">Añadir</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,217 +0,0 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-client',
|
||||
templateUrl: './create-client.component.html',
|
||||
styleUrls: ['./create-client.component.css']
|
||||
})
|
||||
export class CreateClientComponent implements OnInit {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
clientForm!: FormGroup;
|
||||
parentUnits: any[] = [];
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
menus: any[] = [];
|
||||
templates: any[] = [];
|
||||
repositories: any[] = [];
|
||||
loading: boolean = false;
|
||||
displayedColumns: string[] = ['name', 'ip'];
|
||||
protected netifaceTypes = [
|
||||
{ name: 'Eth0', value: 'eth0' },
|
||||
{ name: 'Eth1', value: 'eth1' },
|
||||
{ name: 'Eth2', value: 'eth2' }
|
||||
];
|
||||
protected netDriverTypes = [
|
||||
{ name: 'Generic', value: 'generic' }
|
||||
];
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<CreateClientComponent>,
|
||||
private http: HttpClient,
|
||||
private snackBar: MatSnackBar,
|
||||
private toastService: ToastrService,
|
||||
private dataService: DataService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadParentUnits();
|
||||
this.loadHardwareProfiles();
|
||||
this.loadOgLives();
|
||||
this.loadPxeTemplates();
|
||||
this.loadRepositories();
|
||||
this.loadMenus()
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
initForm(): void {
|
||||
this.clientForm = this.fb.group({
|
||||
organizationalUnit: [
|
||||
this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null,
|
||||
Validators.required
|
||||
],
|
||||
name: ['', Validators.required],
|
||||
serialNumber: [''],
|
||||
netiface: null,
|
||||
netDriver: null,
|
||||
mac: ['', Validators.required],
|
||||
ip: ['', Validators.required],
|
||||
template: [null],
|
||||
hardwareProfile: [null],
|
||||
ogLive: [null],
|
||||
repository: [null],
|
||||
menu: [null]
|
||||
});
|
||||
}
|
||||
|
||||
loadParentUnits(): void {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.parentUnits = response['hydra:member'];
|
||||
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
|
||||
id: unit['@id'],
|
||||
name: unit.name,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits),
|
||||
repository: unit.networkSettings?.repository?.['@id'],
|
||||
hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'],
|
||||
ogLive: unit.networkSettings?.ogLive?.['@id'],
|
||||
menu: unit.networkSettings?.menu?.['@id']
|
||||
}));
|
||||
|
||||
// 🚀 Ahora que los datos están listos, aplicamos la configuración inicial
|
||||
const initialUnitId = this.clientForm.get('organizationalUnit')?.value;
|
||||
if (initialUnitId) {
|
||||
this.setOrganizationalUnitDefaults(initialUnitId);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching parent units:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getSelectedParentName(): string | undefined {
|
||||
const parentId = this.clientForm.get('organizationalUnit')?.value;
|
||||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): void {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => {
|
||||
this.hardwareProfiles = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching hardware profiles:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadOgLives(): void {
|
||||
const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.ogLives = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadPxeTemplates(): void {
|
||||
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.templates = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching PXE templates:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadMenus(): void {
|
||||
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.menus = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching menus:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadRepositories(): void {
|
||||
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.repositories = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onParentChange(event: any): void {
|
||||
this.setOrganizationalUnitDefaults(event.value);
|
||||
}
|
||||
|
||||
setOrganizationalUnitDefaults(unitId: string): void {
|
||||
const selectedUnit: any = this.parentUnitsWithPaths.find(unit => unit.id === unitId);
|
||||
if (selectedUnit) {
|
||||
this.clientForm.patchValue({
|
||||
repository: selectedUnit.repository || null,
|
||||
hardwareProfile: selectedUnit.hardwareProfile || null,
|
||||
ogLive: selectedUnit.ogLive || null,
|
||||
menu: selectedUnit.menu || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.clientForm.valid) {
|
||||
const formData = this.clientForm.value;
|
||||
|
||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
||||
this.dialogRef.close({
|
||||
client: response,
|
||||
organizationalUnit: formData.organizationalUnit,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error.error['hydra:description'], 'Error al crear el cliente');
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.toastService.error('Formulario inválido. Por favor, revise los campos obligatorios.', 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
.create-client-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
padding: 16px 16px 0px 16px;
|
||||
}
|
||||
|
||||
h1, h3, h4 {
|
||||
h1,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0 0 16px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
|
@ -75,7 +77,8 @@ table {
|
|||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
@ -90,14 +93,6 @@ tr:hover {
|
|||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
|
@ -148,7 +143,8 @@ input[type="file"] {
|
|||
gap: 16px;
|
||||
}
|
||||
|
||||
.mat-dialog-content, .create-multiple-client-container {
|
||||
.mat-dialog-content,
|
||||
.create-multiple-client-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
@ -156,3 +152,10 @@ input[type="file"] {
|
|||
max-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -20,12 +20,15 @@
|
|||
|
||||
<div>
|
||||
<div class="upload-container">
|
||||
<button mat-raised-button color="primary" (click)="fileInput.click()">Subir fichero</button>
|
||||
<button class="action-button" (click)="fileInput.click()">Subir fichero</button>
|
||||
<input #fileInput type="file" (change)="onFileUpload($event)" accept="*" hidden>
|
||||
<p>o añadelos manualmente:</p>
|
||||
<div *ngIf="showTextarea">
|
||||
<textarea #textarea matInput placeholder="Ejemplo: host bbaa-it1-11 { hardware ethernet a0:48:1c:8a:f1:5b; fixed-address 172.17.69.11; };" rows="20" cols="100"></textarea>
|
||||
<button mat-raised-button color="primary" (click)="onTextarea(textarea.value)">Previsualizar</button>
|
||||
<textarea #textarea matInput
|
||||
placeholder="Ejemplo: host bbaa-it1-11 { hardware ethernet a0:48:1c:8a:f1:5b; fixed-address 172.17.69.11; };"
|
||||
rows="20" cols="100"></textarea>
|
||||
<button class="action-button" style="margin-top: 0.5em;"
|
||||
(click)="onTextarea(textarea.value)">Previsualizar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -55,8 +58,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button color="warn" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button color="primary" [disabled]="!organizationalUnit" (click)="onSubmit()">{{ 'saveButton' | translate }}</button>
|
||||
<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>
|
||||
</div>
|
|
@ -1,64 +0,0 @@
|
|||
h1 {
|
||||
text-align: center;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
color: #3f51b5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.network-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
padding: 50px;
|
||||
}
|
||||
|
||||
button {
|
||||
text-transform: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mat-slide-toggle {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mat-dialog-content.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.client-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
<h1 mat-dialog-title>{{ 'editClientDialogTitle' | translate }}</h1>
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit">
|
||||
<mat-select-trigger>
|
||||
{{ getSelectedParentName() }}
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive">
|
||||
<mat-option *ngFor="let ogLive of ogLives" [value]="ogLive['@id']">
|
||||
{{ ogLive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netDriverLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netDriver">
|
||||
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'macLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mac">
|
||||
<mat-error>{{ 'macError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ipLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="ip">
|
||||
<mat-error>{{ 'ipError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="template">
|
||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
|
||||
{{ unit.description }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardware-profile-label">Repositorio</mat-label>
|
||||
<mat-select formControlName="repository">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="menu">
|
||||
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
|
||||
{{ menu.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'menuError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()">
|
||||
{{ !isEditMode ? ('addButton' | translate) : ('saveButton' | translate) }}
|
||||
</button>
|
||||
</div>
|
|
@ -1,239 +0,0 @@
|
|||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { CreateClientComponent } from '../create-client/create-client.component';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-client',
|
||||
templateUrl: './edit-client.component.html',
|
||||
styleUrls: ['./edit-client.component.css']
|
||||
})
|
||||
export class EditClientComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
clientForm!: FormGroup;
|
||||
parentUnits: any[] = [];
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
repositories: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
templates: any[] = [];
|
||||
menus: any[] = [];
|
||||
isEditMode: boolean;
|
||||
protected netifaceTypes = [
|
||||
{ "name": 'Eth0', "value": "eth0" },
|
||||
{ "name": 'Eth1', "value": "eth1" },
|
||||
{ "name": 'Eth2', "value": "eth2" },
|
||||
];
|
||||
protected netDriverTypes = [
|
||||
{ "name": 'Generic', "value": "generic" },
|
||||
];
|
||||
loading: boolean = false;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<CreateClientComponent>,
|
||||
private http: HttpClient,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.isEditMode = !!data?.uuid;
|
||||
if (this.isEditMode) {
|
||||
this.loadData(data.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadParentUnits();
|
||||
this.loadHardwareProfiles();
|
||||
this.loadOgLives();
|
||||
this.loadPxeTemplates()
|
||||
this.loadRepositories();
|
||||
this.loadMenus()
|
||||
this.clientForm = this.fb.group({
|
||||
organizationalUnit: [null, Validators.required],
|
||||
name: ['', Validators.required],
|
||||
serialNumber: null,
|
||||
netiface: null,
|
||||
netDriver: null,
|
||||
mac: null,
|
||||
ip: null,
|
||||
template: null,
|
||||
hardwareProfile: null,
|
||||
ogLive: null,
|
||||
repository: null,
|
||||
menu: null,
|
||||
});
|
||||
}
|
||||
|
||||
loadParentUnits(): void {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.parentUnits = response['hydra:member'];
|
||||
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
|
||||
id: unit['@id'],
|
||||
name: unit.name,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits)
|
||||
}));
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching parent units:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getSelectedParentName(): string | undefined {
|
||||
const parentId = this.clientForm.get('organizationalUnit')?.value;
|
||||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): void {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => {
|
||||
this.hardwareProfiles = data;
|
||||
},
|
||||
(error: any) => {
|
||||
console.error('Error fetching hardware profiles', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadOgLives(): void {
|
||||
const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.ogLives = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadMenus(): void {
|
||||
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.menus = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching menus:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadRepositories(): void {
|
||||
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.repositories = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadPxeTemplates(): void {
|
||||
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.templates = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadData(uuid: string) {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/clients/${uuid}`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
data => {
|
||||
this.clientForm.patchValue({
|
||||
name: data.name,
|
||||
ip: data.ip,
|
||||
mac: data.mac,
|
||||
netiface: data.netiface,
|
||||
netDriver: data.netDriver,
|
||||
serialNumber: data.serialNumber,
|
||||
hardwareProfile: data.hardwareProfile ? data.hardwareProfile['@id'] : null,
|
||||
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
|
||||
repository: data.repository ? data.repository['@id'] : null,
|
||||
ogLive: data.ogLive ? data.ogLive['@id'] : null,
|
||||
template: data.template ? data.template['@id'] : null,
|
||||
menu: data.menu ? data.menu['@id'] : null,
|
||||
});
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error al cargar datos del cliente:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.clientForm.valid) {
|
||||
const formData = this.clientForm.value;
|
||||
|
||||
if (this.isEditMode) {
|
||||
// Edit mode: Send PUT request to update the unit
|
||||
const putUrl = `${this.baseUrl}/clients/${this.data.uuid}`;
|
||||
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
|
||||
|
||||
this.http.patch<any>(putUrl, formData, { headers }).subscribe(
|
||||
response => {
|
||||
this.dialogRef.close();
|
||||
this.openSnackBar(false, 'Cliente actualizado exitosamente');
|
||||
|
||||
},
|
||||
error => {
|
||||
console.error('Error al realizar PUT:', error);
|
||||
this.openSnackBar(true, 'Error al actualizar el cliente: ' + error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Create mode: Send POST request to create a new unit
|
||||
const postUrl = `${this.baseUrl}/clients`;
|
||||
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
|
||||
|
||||
this.http.post<any>(postUrl, formData, { headers }).subscribe(
|
||||
response => {
|
||||
this.dialogRef.close();
|
||||
this.openSnackBar(false, 'Cliente creado exitosamente');
|
||||
},
|
||||
error => {
|
||||
console.error('Error al realizar POST:', error);
|
||||
this.openSnackBar(true, 'Error al crear el cliente: ' + error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
openSnackBar(isError: boolean, message: string) {
|
||||
if (isError) {
|
||||
this.toastService.error(' Error al crear el cliente: ' + message, 'Error');
|
||||
} else
|
||||
this.toastService.success(message, 'Éxito');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
h1 {
|
||||
text-align: center;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
color: #3f51b5;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
padding: 15px 50px 15px 50px;
|
||||
}
|
||||
|
||||
mat-option .unit-name {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-option .unit-path {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.create-client-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
column-gap: 20px;
|
||||
row-gap: 20px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
<div class="create-client-container">
|
||||
<h1 mat-dialog-title>{{ dialogTitle | translate }}</h1>
|
||||
<div class="mat-dialog-content" [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">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onParentChange($event)">
|
||||
<mat-select-trigger>
|
||||
{{ getSelectedParentName() }}
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive">
|
||||
<mat-option *ngFor="let ogLive of ogLives" [value]="ogLive['@id']">
|
||||
{{ ogLive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netDriverLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netDriver">
|
||||
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'macLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mac" required>
|
||||
<mat-error>{{ 'macError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ipLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="ip" required>
|
||||
<mat-error>{{ 'ipError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="template">
|
||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let profile of hardwareProfiles" [value]="profile['@id']">
|
||||
{{ profile.description }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="menu">
|
||||
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
|
||||
{{ menu.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'menuError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</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>
|
|
@ -0,0 +1,69 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { ManageClientComponent } from './manage-client.component';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
|
||||
describe('ManageClientComponent', () => {
|
||||
let component: ManageClientComponent;
|
||||
let fixture: ComponentFixture<ManageClientComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ManageClientComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatSnackBarModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot(),
|
||||
MatProgressSpinnerModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { uuid: '123', organizationalUnit: { '@id': '/units/1' } } },
|
||||
DataService
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ManageClientComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should initialize form', () => {
|
||||
expect(component.clientForm).toBeDefined();
|
||||
expect(component.clientForm.controls['name']).toBeDefined();
|
||||
expect(component.clientForm.controls['mac']).toBeDefined();
|
||||
expect(component.clientForm.controls['ip']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set dialog title for edit mode', () => {
|
||||
component.isEditMode = true;
|
||||
component.data = { uuid: '123', organizationalUnit: { '@id': '/units/1' } };
|
||||
component.ngOnInit();
|
||||
expect(component.dialogTitle).toBe('editClientDialogTitle');
|
||||
});
|
||||
|
||||
it('should call onSubmit when form is valid', () => {
|
||||
spyOn(component, 'onSubmit');
|
||||
component.clientForm.controls['name'].setValue('Test Client');
|
||||
component.clientForm.controls['mac'].setValue('00:11:22:33:44:55');
|
||||
component.clientForm.controls['ip'].setValue('192.168.1.1');
|
||||
fixture.detectChanges();
|
||||
const button = fixture.debugElement.nativeElement.querySelector('.submit-button');
|
||||
button.click();
|
||||
expect(component.onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,302 @@
|
|||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-client',
|
||||
templateUrl: './manage-client.component.html',
|
||||
styleUrls: ['./manage-client.component.css']
|
||||
})
|
||||
export class ManageClientComponent implements OnInit {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
clientForm!: FormGroup;
|
||||
parentUnits: any[] = [];
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
menus: any[] = [];
|
||||
templates: any[] = [];
|
||||
repositories: any[] = [];
|
||||
loading: boolean = false;
|
||||
displayedColumns: string[] = ['name', 'ip'];
|
||||
dialogTitle: string;
|
||||
protected netifaceTypes = [
|
||||
{ name: 'Eth0', value: 'eth0' },
|
||||
{ name: 'Eth1', value: 'eth1' },
|
||||
{ name: 'Eth2', value: 'eth2' }
|
||||
];
|
||||
protected netDriverTypes = [
|
||||
{ name: 'Generic', value: 'generic' }
|
||||
];
|
||||
isEditMode: boolean;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<ManageClientComponent>,
|
||||
private http: HttpClient,
|
||||
private snackBar: MatSnackBar,
|
||||
private toastService: ToastrService,
|
||||
private dataService: DataService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.isEditMode = !!data?.uuid;
|
||||
this.dialogTitle = this.isEditMode ? 'editClientDialogTitle' : 'addClientDialogTitle';
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
this.initForm();
|
||||
const observables = [
|
||||
this.loadParentUnits(),
|
||||
this.loadHardwareProfiles(),
|
||||
this.loadOgLives(),
|
||||
this.loadPxeTemplates(),
|
||||
this.loadRepositories(),
|
||||
this.loadMenus()
|
||||
];
|
||||
|
||||
Promise.all(observables).then(() => {
|
||||
if (this.isEditMode) {
|
||||
this.loadData(this.data.uuid).then(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
} else {
|
||||
this.loading = false;
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error loading data:', error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
initForm(): void {
|
||||
this.clientForm = this.fb.group({
|
||||
organizationalUnit: [
|
||||
this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null,
|
||||
Validators.required
|
||||
],
|
||||
name: ['', Validators.required],
|
||||
serialNumber: [''],
|
||||
netiface: null,
|
||||
netDriver: null,
|
||||
mac: ['', Validators.required],
|
||||
ip: ['', Validators.required],
|
||||
template: [null],
|
||||
hardwareProfile: [null],
|
||||
ogLive: [null],
|
||||
repository: [null],
|
||||
menu: [null]
|
||||
});
|
||||
}
|
||||
|
||||
loadParentUnits(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.parentUnits = response['hydra:member'];
|
||||
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
|
||||
id: unit['@id'],
|
||||
name: unit.name,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits),
|
||||
repository: unit.networkSettings?.repository?.['@id'],
|
||||
hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'],
|
||||
ogLive: unit.networkSettings?.ogLive?.['@id'],
|
||||
menu: unit.networkSettings?.menu?.['@id']
|
||||
}));
|
||||
|
||||
const initialUnitId = this.clientForm.get('organizationalUnit')?.value;
|
||||
if (initialUnitId) {
|
||||
this.setOrganizationalUnitDefaults(initialUnitId);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching parent units:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedParentName(): string | undefined {
|
||||
const parentId = this.clientForm.get('organizationalUnit')?.value;
|
||||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => {
|
||||
this.hardwareProfiles = data;
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching hardware profiles:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadOgLives(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.ogLives = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadPxeTemplates(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.templates = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching PXE templates:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadMenus(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.menus = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching menus:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadRepositories(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.repositories = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onParentChange(event: any): void {
|
||||
this.setOrganizationalUnitDefaults(event.value);
|
||||
}
|
||||
|
||||
setOrganizationalUnitDefaults(unitId: string): void {
|
||||
const selectedUnit: any = this.parentUnitsWithPaths.find(unit => unit.id === unitId);
|
||||
if (selectedUnit) {
|
||||
this.clientForm.patchValue({
|
||||
repository: selectedUnit.repository || null,
|
||||
hardwareProfile: selectedUnit.hardwareProfile || null,
|
||||
ogLive: selectedUnit.ogLive || null,
|
||||
menu: selectedUnit.menu || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadData(uuid: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/clients/${uuid}`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
data => {
|
||||
this.clientForm.patchValue({
|
||||
name: data.name,
|
||||
ip: data.ip,
|
||||
mac: data.mac,
|
||||
netiface: data.netiface,
|
||||
netDriver: data.netDriver,
|
||||
serialNumber: data.serialNumber,
|
||||
hardwareProfile: data.hardwareProfile ? data.hardwareProfile['@id'] : null,
|
||||
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
|
||||
repository: data.repository ? data.repository['@id'] : null,
|
||||
ogLive: data.ogLive ? data.ogLive['@id'] : null,
|
||||
template: data.template ? data.template['@id'] : null,
|
||||
menu: data.menu ? data.menu['@id'] : null,
|
||||
});
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error al cargar datos del cliente:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.clientForm.valid) {
|
||||
const formData = this.clientForm.value;
|
||||
|
||||
if (this.isEditMode) {
|
||||
const putUrl = `${this.baseUrl}/clients/${this.data.uuid}`;
|
||||
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
|
||||
|
||||
this.http.patch<any>(putUrl, formData, { headers }).subscribe(
|
||||
response => {
|
||||
this.dialogRef.close();
|
||||
this.toastService.success('Cliente actualizado exitosamente', 'Éxito');
|
||||
},
|
||||
error => {
|
||||
console.error('Error al realizar PUT:', error);
|
||||
this.toastService.error('Error al actualizar el cliente: ' + error.error['hydra:description'], 'Error');
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
||||
this.dialogRef.close({
|
||||
client: response,
|
||||
organizationalUnit: formData.organizationalUnit,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error.error['hydra:description'], 'Error al crear el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.toastService.error('Formulario inválido. Por favor, revise los campos obligatorios.', 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
|
@ -1,57 +1,55 @@
|
|||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.command-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mat-checkbox {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mat-dialog-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
button[mat-button] {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button[mat-button]:disabled {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.command-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mat-checkbox {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mat-dialog-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -20,9 +20,8 @@
|
|||
<div class="checkbox-group">
|
||||
<label>{{ 'clientsLabel' | translate }}</label>
|
||||
<div *ngIf="clients.length > 0">
|
||||
<mat-checkbox *ngFor="let client of clients"
|
||||
(change)="toggleClientSelection(client.uuid)"
|
||||
[checked]="form.get('clientSelection')?.value.includes(client.uuid)">
|
||||
<mat-checkbox *ngFor="let client of clients" (change)="toggleClientSelection(client.uuid)"
|
||||
[checked]="form.get('clientSelection')?.value.includes(client.uuid)">
|
||||
{{ client.name }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
@ -34,12 +33,10 @@
|
|||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="closeModal()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button
|
||||
(click)="executeCommand()"
|
||||
[disabled]="!form.get('clientSelection')?.value.length ||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="closeModal()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" (click)="executeCommand()" [disabled]="!form.get('clientSelection')?.value.length ||
|
||||
(!form.get('selectedCommand')?.value && !form.get('selectedCommandGroup')?.value)">
|
||||
{{ 'executeButton' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</mat-dialog-actions>
|
|
@ -28,12 +28,6 @@ h1 {
|
|||
margin-right: 1em;
|
||||
}
|
||||
|
||||
button {
|
||||
text-transform: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grid-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
|
|
@ -1,172 +1,185 @@
|
|||
<h1 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Crear' }} Unidad Organizativa</h1>
|
||||
<div class="mat-dialog-content">
|
||||
<!-- Paso 1: General -->
|
||||
<span class="step-title">General</span>
|
||||
<form [formGroup]="generalFormGroup" class="grid-form">
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Tipo</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">
|
||||
{{ typeTranslations[type] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Nombre</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Padre</mat-label>
|
||||
<mat-select formControlName="parent">
|
||||
<mat-select-trigger>
|
||||
{{ getSelectedParentName() }}
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||
<div>{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<mat-form-field class="form-field description-form-field" appearance="fill">
|
||||
<mat-label>Descripción</mat-label>
|
||||
<textarea matInput formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
<div *ngIf="!loading">
|
||||
<h1 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Crear' }} Unidad Organizativa</h1>
|
||||
<div class="mat-dialog-content">
|
||||
<!-- Paso 1: General -->
|
||||
<span class="step-title">General</span>
|
||||
<form [formGroup]="generalFormGroup" class="grid-form">
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Tipo</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">
|
||||
{{ typeTranslations[type] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Nombre</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Padre</mat-label>
|
||||
<mat-select formControlName="parent">
|
||||
<mat-select-trigger>
|
||||
{{ getSelectedParentName() }}
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||
<div>{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox formControlName="excludeParentChanges">
|
||||
{{ 'excludeParentChanges' | translate }}
|
||||
</mat-checkbox>
|
||||
</form>
|
||||
<mat-form-field class="form-field description-form-field" appearance="fill">
|
||||
<mat-label>Descripción</mat-label>
|
||||
<textarea matInput formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Paso 2: Información del Aula -->
|
||||
<span *ngIf="generalFormGroup.value.type === 'classroom'" class="step-title">Información del aula</span>
|
||||
<form *ngIf="generalFormGroup.value.type === 'classroom'" class="grid-form" [formGroup]="classroomInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Localización</mat-label>
|
||||
<input matInput formControlName="location">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Aforo</mat-label>
|
||||
<input matInput formControlName="capacity" type="number" min="0">
|
||||
<mat-error *ngIf="classroomInfoFormGroup.get('capacity')?.hasError('min')">
|
||||
El aforo no puede ser negativo
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill" style="grid-column: span 1;">
|
||||
<mat-label>Calendario Asociado</mat-label>
|
||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
{{ calendar.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="projector-board-field">
|
||||
<mat-slide-toggle formControlName="projector">Proyector</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">Pizarra</mat-slide-toggle>
|
||||
</div>
|
||||
</form>
|
||||
<mat-checkbox formControlName="excludeParentChanges">
|
||||
{{ 'excludeParentChanges' | translate }}
|
||||
</mat-checkbox>
|
||||
</form>
|
||||
|
||||
<!-- Paso 3: Configuración de Red -->
|
||||
<span class="step-title">Configuración de Red</span>
|
||||
<form [formGroup]="networkSettingsFormGroup" class="grid-form">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>OgLive</mat-label>
|
||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Repositorio</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Proxy</mat-label>
|
||||
<input matInput formControlName="proxy">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>DNS</mat-label>
|
||||
<input matInput formControlName="dns">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Máscara de Red</mat-label>
|
||||
<input matInput formControlName="netmask">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Router</mat-label>
|
||||
<input matInput formControlName="router">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>NTP</mat-label>
|
||||
<input matInput formControlName="ntp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Modo P2P</mat-label>
|
||||
<mat-select formControlName="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Tiempo P2P</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast IP</mat-label>
|
||||
<input matInput formControlName="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast Speed</mat-label>
|
||||
<input matInput formControlName="mcastSpeed" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast Port</mat-label>
|
||||
<input matInput formControlName="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast Mode</mat-label>
|
||||
<mat-select formControlName="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Menu</mat-label>
|
||||
<mat-select formControlName="menu">
|
||||
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
|
||||
{{ menu.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Perfil de Hardware</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description
|
||||
}}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>Formato de URL incorrecto</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<!-- Paso 2: Información del Aula -->
|
||||
<span *ngIf="generalFormGroup.value.type === 'classroom'" class="step-title">Información del aula</span>
|
||||
<form *ngIf="generalFormGroup.value.type === 'classroom'" class="grid-form"
|
||||
[formGroup]="classroomInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Localización</mat-label>
|
||||
<input matInput formControlName="location">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Aforo</mat-label>
|
||||
<input matInput formControlName="capacity" type="number" min="0">
|
||||
<mat-error *ngIf="classroomInfoFormGroup.get('capacity')?.hasError('min')">
|
||||
El aforo no puede ser negativo
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill" style="grid-column: span 1;">
|
||||
<mat-label>Calendario Asociado</mat-label>
|
||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
{{ calendar.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="projector-board-field">
|
||||
<mat-slide-toggle formControlName="projector">Proyector</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">Pizarra</mat-slide-toggle>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Paso 4: Información Adicional -->
|
||||
<span class="step-title">Información Adicional</span>
|
||||
<form [formGroup]="additionalInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Comentarios</mat-label>
|
||||
<textarea matInput formControlName="comments"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mat-dialog-actions">
|
||||
<button mat-button (click)="onNoClick()">Cancelar</button>
|
||||
<button mat-button (click)="onSubmit()"
|
||||
[disabled]="!generalFormGroup.valid || !additionalInfoFormGroup.valid || !networkSettingsFormGroup.valid">{{
|
||||
isEditMode ? 'Editar' : 'Crear' }}</button>
|
||||
<!-- Paso 3: Configuración de Red -->
|
||||
<span class="step-title">Configuración de Red</span>
|
||||
<form [formGroup]="networkSettingsFormGroup" class="grid-form">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>OgLive</mat-label>
|
||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Repositorio</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Proxy</mat-label>
|
||||
<input matInput formControlName="proxy">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>DNS</mat-label>
|
||||
<input matInput formControlName="dns">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Máscara de Red</mat-label>
|
||||
<input matInput formControlName="netmask">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@netiface-label">Interfaz de red</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Router</mat-label>
|
||||
<input matInput formControlName="router">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>NTP</mat-label>
|
||||
<input matInput formControlName="ntp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Modo P2P</mat-label>
|
||||
<mat-select formControlName="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Tiempo P2P</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast IP</mat-label>
|
||||
<input matInput formControlName="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast Speed</mat-label>
|
||||
<input matInput formControlName="mcastSpeed" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast Port</mat-label>
|
||||
<input matInput formControlName="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast Mode</mat-label>
|
||||
<mat-select formControlName="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Menu</mat-label>
|
||||
<mat-select formControlName="menu">
|
||||
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
|
||||
{{ menu.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Perfil de Hardware</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description
|
||||
}}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>Formato de URL incorrecto</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<!-- Paso 4: Información Adicional -->
|
||||
<span class="step-title">Información Adicional</span>
|
||||
<form [formGroup]="additionalInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Comentarios</mat-label>
|
||||
<textarea matInput formControlName="comments"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mat-dialog-actions">
|
||||
<button class="ordinary-button" (click)="onNoClick()">Cancelar</button>
|
||||
<button class="submit-button" (click)="onSubmit()"
|
||||
[disabled]="!generalFormGroup.valid || !additionalInfoFormGroup.valid || !networkSettingsFormGroup.valid">{{
|
||||
isEditMode ? 'Editar' : 'Crear' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,8 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { LoadingComponent } from '../../../../../shared/loading/loading.component';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
|
||||
describe('ManageOrganizationalUnitComponent', () => {
|
||||
let component: ManageOrganizationalUnitComponent;
|
||||
|
@ -18,7 +20,7 @@ describe('ManageOrganizationalUnitComponent', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ManageOrganizationalUnitComponent],
|
||||
declarations: [ManageOrganizationalUnitComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ReactiveFormsModule,
|
||||
|
@ -29,14 +31,15 @@ describe('ManageOrganizationalUnitComponent', () => {
|
|||
MatSlideToggleModule,
|
||||
MatCheckboxModule,
|
||||
TranslateModule.forRoot(),
|
||||
BrowserAnimationsModule
|
||||
BrowserAnimationsModule,
|
||||
MatProgressSpinnerModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ManageOrganizationalUnitComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -41,8 +41,14 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
{ "name": 'Half duplex', "value": "half" },
|
||||
{ "name": 'Full duplex', "value": "full" },
|
||||
];
|
||||
protected netifaceTypes = [
|
||||
{ name: 'Eth0', value: 'eth0' },
|
||||
{ name: 'Eth1', value: 'eth1' },
|
||||
{ name: 'Eth2', value: 'eth2' }
|
||||
];
|
||||
@Output() unitAdded = new EventEmitter();
|
||||
calendars: any;
|
||||
loading: boolean = false;
|
||||
|
||||
constructor(
|
||||
private _formBuilder: FormBuilder,
|
||||
|
@ -74,6 +80,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
netmask: [null],
|
||||
router: [null],
|
||||
ntp: [null],
|
||||
netiface: [null],
|
||||
p2pMode: [null],
|
||||
p2pTime: [null],
|
||||
mcastIp: [null],
|
||||
|
@ -111,6 +118,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadParentUnits() {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
|
@ -120,8 +128,12 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
name: unit.name,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits)
|
||||
}));
|
||||
this.loading = false;
|
||||
},
|
||||
error => console.error('Error fetching parent units:', error)
|
||||
error => {
|
||||
console.error('Error fetching parent units:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -131,64 +143,91 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadHardwareProfiles(): void {
|
||||
this.loading = true;
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => {
|
||||
this.hardwareProfiles = data;
|
||||
this.loading = false;
|
||||
},
|
||||
(error: any) => {
|
||||
console.error('Error fetching hardware profiles', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadMenus(): void {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.menus = response['hydra:member'];
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching menus:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
loadOgLives() {
|
||||
this.loading = true;
|
||||
this.dataService.getOgLives().subscribe(
|
||||
(data: any[]) => {
|
||||
this.ogLives = data
|
||||
this.ogLives = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error => console.error('Error fetching ogLives', error)
|
||||
error => {
|
||||
console.error('Error fetching ogLives', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadRepositories() {
|
||||
this.loading = true;
|
||||
this.dataService.getRepositories().subscribe(
|
||||
(data: any[]) => this.repositories = data,
|
||||
error => console.error('Error fetching repositories', error)
|
||||
(data: any[]) => {
|
||||
this.repositories = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching repositories', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadCalendars() {
|
||||
this.loading = true;
|
||||
const apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=30`;
|
||||
this.http.get<any>(apiUrl).subscribe(
|
||||
response => this.calendars = response['hydra:member'],
|
||||
response => {
|
||||
this.calendars = response['hydra:member'];
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error loading calendars', error);
|
||||
this.toastService.error('Error loading current calendar');
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadCurrentCalendar(uuid: string): void {
|
||||
this.loading = true;
|
||||
const apiUrl = `${this.baseUrl}/remote-calendars/${uuid}`;
|
||||
this.http.get<any>(apiUrl).subscribe(
|
||||
response => this.currentCalendar = response,
|
||||
response => {
|
||||
this.currentCalendar = response;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error loading current calendar', error);
|
||||
this.toastService.error('Error loading current calendar');
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -206,6 +245,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadData(uuid: string) {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/organizational-units/${uuid}`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
|
@ -226,6 +266,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
netmask: data.networkSettings.netmask,
|
||||
router: data.networkSettings.router,
|
||||
ntp: data.networkSettings.ntp,
|
||||
netiface: data.networkSettings.netiface,
|
||||
p2pMode: data.networkSettings.p2pMode,
|
||||
p2pTime: data.networkSettings.p2pTime,
|
||||
mcastIp: data.networkSettings.mcastIp,
|
||||
|
@ -244,11 +285,13 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
capacity: data.capacity,
|
||||
remoteCalendar: data.remoteCalendar ? data.remoteCalendar['@id'] : null
|
||||
});
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
error => {
|
||||
console.error('Error fetching data for edit:', error);
|
||||
this.toastService.error('Error fetching data');
|
||||
this.loading = false;
|
||||
this.onNoClick();
|
||||
}
|
||||
);
|
||||
|
@ -284,7 +327,11 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
},
|
||||
error => {
|
||||
console.error('Error al realizar PUT:', error);
|
||||
this.toastService.error('Error al editar:', error);
|
||||
const errorMessages = error.error['hydra:description'].split('\n');
|
||||
errorMessages.forEach((message: string) => {
|
||||
const cleanedMessage = message.replace(/networkSettings\.(\w+):/, 'Error $1:');
|
||||
this.toastService.error(cleanedMessage);
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
@ -299,7 +346,11 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
},
|
||||
error => {
|
||||
console.error('Error al realizar POST:', error);
|
||||
this.toastService.error('Error al crear:', error.error['hydra:description']);
|
||||
const errorMessages = error.error['hydra:description'].split('\n');
|
||||
errorMessages.forEach((message: string) => {
|
||||
const cleanedMessage = message.replace(/networkSettings\.(\w+):/, 'Error $1:');
|
||||
this.toastService.error(cleanedMessage);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -46,5 +46,5 @@
|
|||
</mat-tab-group>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="ordinary-button" style="margin: 1em;" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px; /* Espacio entre los elementos del formulario */
|
||||
gap: 16px;
|
||||
/* Espacio entre los elementos del formulario */
|
||||
}
|
||||
|
||||
.image-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
|
@ -16,27 +18,19 @@
|
|||
}
|
||||
|
||||
.partition-info-container {
|
||||
background-color: #f0f8ff; /* Un color de fondo suave */
|
||||
background-color: #f0f8ff;
|
||||
/* Un color de fondo suave */
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
margin-top: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Botones alineados al final, con margen superior */
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 8px; /* Espacio entre los botones */
|
||||
}
|
||||
|
||||
.warning-card {
|
||||
background-color: #ffebee; /* Rojo claro */
|
||||
color: #d32f2f; /* Rojo oscuro */
|
||||
background-color: #ffebee;
|
||||
/* Rojo claro */
|
||||
color: #d32f2f;
|
||||
/* Rojo oscuro */
|
||||
padding: 16px;
|
||||
border-left: 5px solid #d32f2f;
|
||||
display: flex;
|
||||
|
@ -68,3 +62,10 @@ button {
|
|||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h2 mat-dialog-title>{{ imageId ? 'Editar' : 'Crear' }} imagen</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
|
@ -5,7 +7,8 @@
|
|||
<mat-card *ngIf="showWarning" class="warning-card">
|
||||
<mat-card-content>
|
||||
<mat-icon color="warn">warning</mat-icon>
|
||||
Ha marcado la casilla <strong>"Imagen Global"</strong>. Se transferirá la imagen al resto de repositorios en el caso de que no exista previamente.
|
||||
Ha marcado la casilla <strong>"Imagen Global"</strong>. Se transferirá la imagen al resto de repositorios en el
|
||||
caso de que no exista previamente.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-form-field appearance="fill" class="form-field">
|
||||
|
@ -13,10 +16,10 @@
|
|||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="form-field">
|
||||
<mat-form-field appearance="fill" class="form-field" *ngIf="!imageId">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="imageRepositories" required multiple>
|
||||
<mat-option *ngFor="let imageRepository of repositories" [value]="imageRepository['@id']" [disabled]="imageId !== null">
|
||||
<mat-option *ngFor="let imageRepository of repositories" [value]="imageRepository['@id']">
|
||||
{{ imageRepository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
|
@ -41,16 +44,11 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox
|
||||
formControlName="remotePc"
|
||||
>
|
||||
<mat-checkbox formControlName="remotePc">
|
||||
{{ 'remotePcLabel' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-checkbox
|
||||
formControlName="isGlobal"
|
||||
(click)="changeIsGlobal()"
|
||||
>
|
||||
<mat-checkbox formControlName="isGlobal" (click)="changeIsGlobal()">
|
||||
{{ 'globalImageLabel' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
|
@ -68,7 +66,7 @@
|
|||
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end" class="dialog-actions">
|
||||
<button mat-button (click)="close()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button color="primary" (click)="saveImage()">{{ 'saveButton' | translate }}</button>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" (click)="saveImage()">{{ 'saveButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -16,8 +16,9 @@ export class CreateImageComponent implements OnInit {
|
|||
imageId: string | null = null;
|
||||
softwareProfiles: any[] = [];
|
||||
repositories: any[] = [];
|
||||
loading: boolean = false;
|
||||
partitionInfo: { [key: string]: string } = {};
|
||||
showWarning: boolean = false; // Nueva variable para mostrar la advertencia
|
||||
showWarning: boolean = false;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
|
@ -39,12 +40,13 @@ export class CreateImageComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loading = true;
|
||||
if (this.data) {
|
||||
this.load()
|
||||
}
|
||||
this.fetchSoftwareProfiles();
|
||||
this.fetchRepositories();
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
load(): void {
|
||||
|
@ -79,7 +81,6 @@ export class CreateImageComponent implements OnInit {
|
|||
this.softwareProfiles = response['hydra:member'];
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al obtener los perfiles de software:', error);
|
||||
this.toastService.error('Error al obtener los perfiles de software');
|
||||
}
|
||||
});
|
||||
|
@ -92,7 +93,6 @@ export class CreateImageComponent implements OnInit {
|
|||
this.repositories = response['hydra:member'];
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al obtener los repositorios de imágenes:', error);
|
||||
this.toastService.error('Error al obtener los repositorios de imágenes');
|
||||
}
|
||||
});
|
||||
|
@ -117,7 +117,6 @@ export class CreateImageComponent implements OnInit {
|
|||
},
|
||||
(error) => {
|
||||
this.toastService.error(error['error']['hydra:description']);
|
||||
console.error('Error al editar la imagen', error);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
@ -128,7 +127,6 @@ export class CreateImageComponent implements OnInit {
|
|||
},
|
||||
(error) => {
|
||||
this.toastService.error(error['error']['hydra:description']);
|
||||
console.error('Error al añadir la imagen', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="close()">Cancelar</button>
|
||||
<button mat-button (click)="save()">Continuar</button>
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="action-button" (click)="save()">Continuar</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="addImage()">
|
||||
<button class="action-button" (click)="addImage()">
|
||||
{{ 'addImageButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -38,12 +38,10 @@
|
|||
</mat-icon>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'imageRepositories'">
|
||||
<button mat-button [matMenuTriggerFor]="menu">Ver repositorios</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item *ngFor="let repository of image.imageRepositories">
|
||||
{{ repository.imageRepository.name }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-chip-set>
|
||||
<mat-chip *ngFor="let repository of image.imageRepositories"> {{ repository.imageRepository.name }} </mat-chip>
|
||||
</mat-chip-set>
|
||||
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'isGlobal'">
|
||||
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
||||
|
|
|
@ -5,11 +5,8 @@ import { MatTableDataSource } from '@angular/material/table';
|
|||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { CreateImageComponent } from './create-image/create-image.component';
|
||||
import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {ServerInfoDialogComponent} from "../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
|
||||
import {Observable} from "rxjs";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import {ExportImageComponent} from "./export-image/export-image.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-images',
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue