Compare commits

..

11 Commits
main ... 0.0.2

Author SHA1 Message Date
Nicolas Arenas a7ce0f3859 Just update changelog if tag is set
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details
ogrepository/pipeline/tag Build started... Details
ogdhcp/pipeline/tag Build started... Details
ogboot/pipeline/tag Build started... Details
ogclone-engine/pipeline/tag Build started... Details
2025-03-18 00:15:45 +01:00
Nicolas Arenas e04a8204a7 Adjust values
oggui-debian-package/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-18 00:08:39 +01:00
Nicolas Arenas 189d6b8cb2 use null values if empty
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/head This commit looks good Details
2025-03-17 23:46:09 +01:00
Nicolas Arenas 91848a90e6 use null values if empty
oggui-debian-package/pipeline/head There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-17 23:44:03 +01:00
Nicolas Arenas d0c3730fd1 Add parameters for changelog mantainers
oggui-debian-package/pipeline/head There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-17 20:20:14 +01:00
Nicolas Arenas ca1a4d0fc2 Add parameters for changelog mantainers
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/head There was a failure building this commit Details
2025-03-17 19:50:13 +01:00
Nicolas Arenas 74ef13906d Add parameters for changelog mantainers
oggui-debian-package/pipeline/head There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-17 19:48:17 +01:00
Nicolas Arenas 7bf25a86c8 Use shared library
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/head This commit looks good Details
2025-03-17 19:20:58 +01:00
Nicolas Arenas eedc6cb59d FIxes Jnskinsfile
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/head This commit looks good Details
2025-03-17 17:41:18 +01:00
Nicolas Arenas df1c42feec Test packagin in Jenkins
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/head There was a failure building this commit Details
2025-03-17 17:37:32 +01:00
Nicolas Arenas c41ca6e8ce Add Jenkinsfile to build deb pkg
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-17 16:40:22 +01:00
239 changed files with 2645 additions and 6748 deletions

View File

@ -1,98 +1,40 @@
# Changelog # Changelog
## [0.11.2] - 2025-4-16
### Fixed
- Se ha corregido un error en la actualizacion del estado de los pcs en la vista tarjetas.
---
## [0.11.1] - 2025-4-16
### Improved
- Nuevos campos en la tabla de clientes. Tipo de firmware y mac.
## Fixed
- Se ha corregido error al crear OUs, que no refrescaba la web.
- Se ha corregido error en el formulario de creacion de imagenes. Si se seleccionaba una imagen para un versionado, no dejaba deseleccionar.
- Se ha corregido un bug en el particionador que impedia ejecutar, cuando eliminabamos una particion.
---
## [0.11.0] - 2025-4-11
### Added
- Se ha diseñado el nuevo formulario para poder ejecutar script. Sistema mejorado con variables etiquetadas.
- Se puede añadir descripcion a una imagen.
- Se han añadido al formulario de crear/editar repositorio, la posibilidad de añadir usuario y puerto ssh.
- Nuevo estado en pc => desconectado.
- Se ha añadido nueva accion para renombrar imagen monolitica.
### Improved
- Se ha mejorado la interfaz de usuario tanto para el despliegue de imagenes, como el particionado.
- Se ha mejorado la responsividad de la vista de grupos.
- Cambios en el comportamiento general de muchos componentes modales. Se han añadido spinners de carga mas intuitivos.
---
## [0.10.1] - 2025-3-27
### Improved
- Mejoras en el comportamiento del arbol de grupos.
- Nueva regexp para controlar las "macs" en la creacion de clientes.
---
## [0.10.0] - 2025-3-25
### Added
- Nuevo componenten de estado global.
- Servicio para que el ogGui obtenga de forma dinamica las variables de entorno.
- Nueva funcionalidad para convertir imagen en imagen virtual.
- Nueva funcionalidad para importar imágenes externas al sistema.
- Despliegue de imangenes sin cache. Cambios en el formulario de "despliegue".
### Improved
- Mejoras en la internacionalización.
- Nueva UX ogRepository. Ahora se gestionan las imagenes de forma mas sencilla.
- Cambios en ogLive. Mejora en la sincronizacion y obtención de datos en la API
### Fixed
- Cambios en la expresion regular para la validacion de documentos DHCP en la carga masiva de pc.
---
## [0.9.2] - 2025-03-19
### Changed
- Jenkinsfile to pubilsh packages in repo in case og release
---
## [0.9.1] - 2025-03-12 ## [0.9.1] - 2025-03-12
### Changed ### ⚡ Changed
- Se ha modificado el acceso a Mercure añadiendo nueva variable de entorno. - Se ha modificado el acceso a Mercure añadiendo nueva variable de entorno.
---
## [0.9.0] - 2025-3-4 ## [0.9.0] - 2025-3-4
### Added ### 🔹 Added
- Integracion con Mercure. Subscriber tanto en "Trazas" con en "Clientes". - 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. - Nueva funcionalidad para checkear la integridad de una imagen. Boton en apartado "imagenes" dentro del repositorio.
- Centralizacion de estilos. - Centralizacion de estilos.
- Nueva funcionalidad para realizar backup de imágenes. - Nueva funcionalidad para realizar backup de imágenes.
- Botón para cancelar despliegues de imagenes. Aparece en "trazas" tan solo para los comendos "deploy" y para el estado "en progreso".
### Changed ### ⚡ 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. - 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. - 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. - Refactorizados compontentes de crear/editar clientes en uno solo.
- Cambios en DHCP. Nueva UX en "ver clientes". Ahora tenemos un buscador detallado. - 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. - 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 ## [0.7.0] - 2024-12-10
### Refactored ### Refactored
- Refactored the group screen, removing the separate tabs for clients, advanced search, and organizational units. - 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. - 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 ## [0.6.1] - 2024-11-19
### Improved ### Improved
- Introduced a new automatic sync mode for the ogdhcp and ogBoot components. - Introduced a new automatic sync mode for the ogdhcp and ogBoot components.
- Improve test coverage. - Improve test coverage.
- New view for clients inside the classroom on the main page. - New view for clients inside the classroom on the main page.
---
## [0.6.0] - 2024-11-19 ## [0.6.0] - 2024-11-19
### Added
### 🔹 Added
- Added functionality to execute actions from the menu in the general groups screen. - Added functionality to execute actions from the menu in the general groups screen.
- Displayed the selected center on the general screen for better context. - Displayed the selected center on the general screen for better context.
- Implemented the option to collapse the sidebar for improved usability. - Implemented the option to collapse the sidebar for improved usability.

View File

@ -36,7 +36,7 @@ pipeline {
stage('Generate Changelog') { stage('Generate Changelog') {
when { when {
expression { expression {
return env.TAG_NAME != null return env.GIT_TAG_NAME != null
} }
} }
steps { steps {
@ -48,60 +48,25 @@ pipeline {
} }
} }
} }
stage('Generate Changelog (Nightly)'){
when {
branch 'main'
}
steps {
script {
def devName = params.DEV_NAME ? params.DEV_NAME : env.DEFAULT_DEV_NAME
def devEmail = params.DEV_EMAIL ? params.DEV_EMAIL : env.DEFAULT_DEV_EMAIL
generateDebianChangelog(env.BUILD_DIR, devName, devEmail,"nightly")
}
}
}
stage('Build') { stage('Build') {
steps { steps {
script { dir("${env.BUILD_DIR}") {
construirPaquete(env.BUILD_DIR, "../artifacts", "172.17.8.68", "/var/tmp/opengnsys/debian-repo/oggui") sh '''
} dpkg-buildpackage -us -uc
} mkdir -p ../artifacts && mv ../*.deb ../*.changes ../*.buildinfo ../artifacts/
} ssh aptly@172.17.8.68 "rm -rf /var/tmp/opengnsys/debian-repo && mkdir -p /var/tmp/opengnsys/debian-repo"
stage ('Publish to Debian Repository') { scp -r ../artifacts/* aptly@172.17.8.68:/var/tmp/opengnsys/debian-repo/
when { '''
expression {
return env.TAG_NAME != null
}
}
agent { label 'debian-repo' }
steps {
script {
// Construir el patrón de versión esperado en el nombre del paquete
def versionPattern = "${env.TAG_NAME}-${env.BUILD_NUMBER}"
publicarEnAptly('/var/tmp/opengnsys/debian-repo/oggui', 'opengnsys-devel', versionPattern)
}
}
}
stage ('Publish to Debian Repository (Nightly)') {
when {
branch 'main'
}
agent { label 'debian-repo' }
steps {
script {
// Construir el patrón de versión esperado en el nombre del paquete
def versionPattern = "-${env.BUILD_NUMBER}~nightly"
publicarEnAptly('/var/tmp/opengnsys/debian-repo/oggui', 'nightly', versionPattern)
} }
} }
} }
} }
post {
always {
notifyBuildStatus('narenas@qindel.com')
}
}
} }
// stage ('Publish to Debian Repository') {
// agent { label 'debian-repo' }
// steps {
// sh "aptly repo add opengnsys-devel /var/tmp/opengnsys/debian-repo/*.deb"
// }
// }

6
debian/control vendored
View File

@ -8,6 +8,6 @@ Standards-Version: 4.5.0
Package: oggui Package: oggui
Architecture: any Architecture: any
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com> Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
Depends: ${shlibs:Depends}, ${misc:Depends}, nginx Depends: ${shlibs:Depends}, ${misc:Depends}, nginx, nodejs, npm
Description: OpenGnsys GUI created for the Opengnsys Team Description: OpenGnsys GUI
Opengnsys Graphical Intercface Una interfaz gráfica para OpenGnsys.

2
debian/oggui.config vendored
View File

@ -5,6 +5,6 @@ set -e
. /usr/share/debconf/confmodule . /usr/share/debconf/confmodule
db_input high opengnsys/oggui_ogcoreUrl || true db_input high opengnsys/oggui_ogcoreUrl || true
db_input high opengnsys/oggui_ogmercureUrl || true
db_go db_go

View File

@ -1,4 +1,9 @@
ogWebconsole/dist/oggui/browser /opt/opengnsys/oggui/ ogWebconsole/dist/oggui/browser /opt/opengnsys/oggui/browser/
etc /opt/opengnsys/oggui/ etc /opt/opengnsys/oggui/
bin /opt/opengnsys/oggui/
var /opt/opengnsys/oggui/
ogWebconsole/*.json /opt/opengnsys/oggui/src/
ogWebconsole/*.js /opt/opengnsys/oggui/src/
ogWebconsole/src /opt/opengnsys/oggui/src/
ogWebconsole/ssl/* /opt/opengnsys/oggui/etc/nginx/certs/ ogWebconsole/ssl/* /opt/opengnsys/oggui/etc/nginx/certs/

60
debian/oggui.postinst vendored
View File

@ -6,56 +6,46 @@ set -e
db_get opengnsys/oggui_ogcoreUrl db_get opengnsys/oggui_ogcoreUrl
OGCORE_URL="$RET" OGCORE_URL="$RET"
db_get opengnsys/oggui_ogmercureUrl
OGMERCURE_URL="$RET"
# Asegurarse de que el usuario exista # Asegurarse de que el usuario exista
USER="opengnsys" USER="opengnsys"
CONFIG_FILE="/opt/opengnsys/oggui/browser/assets/config.json" HASH_FILE="/opt/opengnsys/oggui/var/lib/oggui/oggui.config.hash"
CONFIG_FILE="/opt/opengnsys/oggui/src/.env"
restore_config_if_modified() {
local new="$1"
local backup="$1.bak"
if [ -f "$backup" ]; then
if ! cmp -s "$new" "$backup"; then # Provisionar base de datos si es necesario en caso de instalación.
echo ">>> Archivo modificado por el usuario detectado en $new"
echo " - Guardando archivo nuevo como ${new}.new"
mv -f "$new" "${new}.new"
echo " - Restaurando archivo anterior desde backup"
mv -f "$backup" "$new"
else
echo ">>> El archivo $new no ha cambiado desde la última versión, eliminando backup"
rm -f "$backup"
fi
fi
}
# Detectar si es una instalación nueva o una actualización # Detectar si es una instalación nueva o una actualización
if [ "$1" = "configure" ] && [ -z "$2" ]; then if [ "$1" = "configure" ] && [ -z "$2" ]; then
if [ ! -f "$CONFIG_FILE" ]; then cd /opt/opengnsys/oggui/src/
jq --arg apiUrl "$OGCORE_URL" --arg mercureUrl "$OGMERCURE_URL" \ echo NG_APP_BASE_API_URL=$OGCORE_URL > "$CONFIG_FILE"
'.apiUrl = $apiUrl | .mercureUrl = $mercureUrl' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE" npm install -g @angular/cli
fi npm install
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf /usr/local/bin/ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production --localize=false
ln -s $CONFIG_FILE /opt/opengnsys/oggui/etc/config.json cp -pr /opt/opengnsys/oggui/src/dist/oggui/browser/* /opt/opengnsys/oggui/browser/
mkdir -p /etc/nginx/certs/ md5sum "$CONFIG_FILE" > "$HASH_FILE"
cp -p /opt/opengnsys/oggui/etc/nginx/certs/* /etc/nginx/certs/ ln -s /opt/opengnsys/oggui/etc/systemd/system/oggui.service /etc/systemd/system/oggui.service
chown -R www-data:www-data /etc/nginx/certs
systemctl daemon-reload systemctl daemon-reload
systemctl restart nginx systemctl enable oggui
elif [ "$1" = "configure" ] && [ -n "$2" ]; then elif [ "$1" = "configure" ] && [ -n "$2" ]; then
cd /opt/opengnsys/oggui cd /opt/opengnsys/oggui
echo "Actualización desde la versión $2" echo "Actualización desde la versión $2"
# Si upgrade recupero los archivos de configuracion
echo ">>> Backup de archivos de configuración reales en /opt/opengnsys"
restore_config_if_modified "/opt/opengnsys/oggui/etc/nginx/oggui.conf"
restore_config_if_modified "$CONFIG_FILE"
fi fi
# Cambiar la propiedad de los archivos al usuario especificado # Cambiar la propiedad de los archivos al usuario especificado
chown opengnsys:www-data /opt/opengnsys/ chown opengnsys:www-data /opt/opengnsys/
chown -R opengnsys:www-data /opt/opengnsys/oggui chown -R opengnsys:www-data /opt/opengnsys/oggui
chmod 755 /opt/opengnsys/oggui/bin/start-oggui.sh
# Install http server stuff
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf
mkdir -p /etc/nginx/certs/
cp -p /opt/opengnsys/oggui/etc/nginx/certs/* /etc/nginx/certs/
chown -R www-data:www-data /etc/nginx/certs
# Reiniciar servicios si es necesario
# systemctl restart nombre_del_servicio
systemctl daemon-reload
systemctl restart nginx
exit 0 exit 0

17
debian/oggui.preinst vendored
View File

@ -2,16 +2,6 @@
set -e set -e
backup_file_if_exists() {
local original="$1"
local backup="$1.bak"
if [ -e "$original" ]; then
echo " - Guardando backup de $original en $backup"
cp -a "$original" "$backup"
fi
}
CONFIG_FILE="/opt/opengnsys/oggui/browser/assets/config.json"
# Asegurarse de que el usuario exista # Asegurarse de que el usuario exista
USER="opengnsys" USER="opengnsys"
HOME_DIR="/opt/opengnsys" HOME_DIR="/opt/opengnsys"
@ -22,11 +12,4 @@ else
useradd -m -d "$HOME_DIR" -s /bin/bash "$USER" useradd -m -d "$HOME_DIR" -s /bin/bash "$USER"
fi fi
# Si upgrade hago backup del archivo de configuración
if [ "$1" = "upgrade" ]; then
echo ">>> Backup de archivos de configuración reales en /opt/opengnsys"
backup_file_if_exists "/opt/opengnsys/oggui/etc/nginx/sites-available/oggui.conf"
backup_file_if_exists "$CONFIG_FILE"
fi
exit 0 exit 0

5
debian/oggui.prerm vendored
View File

@ -3,8 +3,9 @@
set -e set -e
set -x set -x
# Solo eliminar archivos de configuración si se está eliminando el paquete if [ "$1" = "upgrade" ]; then
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then # Eliminar enlaces simbólicos creados en postinst
rm -f /etc/systemd/system/oggui.service
rm -f /etc/nginx/sites-enabled/oggui.conf rm -f /etc/nginx/sites-enabled/oggui.conf
systemctl daemon-reload systemctl daemon-reload
systemctl restart nginx systemctl restart nginx

View File

@ -3,7 +3,3 @@ Type: string
Default: https://127.0.0.1:8443 Default: https://127.0.0.1:8443
Description: Introduzca la URL delAPI de OgCore Description: Introduzca la URL delAPI de OgCore
Template: opengnsys/oggui_ogmercureUrl
Type: string
Default: https://127.0.0.1:3000/.well-known/mercure
Description: Introduzca el endpoint de mercure

11
debian/rules vendored
View File

@ -5,7 +5,16 @@
override_dh_auto_build: override_dh_auto_build:
cd ogWebconsole && npm install cd ogWebconsole && npm install
cd ogWebconsole && npx ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production cd ogWebconsole && /usr/local/bin/ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production --localize=false
override_dh_auto_install: override_dh_auto_install:
dh_auto_install dh_auto_install
mkdir -p debian/oggui/opt/opengnsys/oggui/browser
mkdir -p debian/oggui/opt/opengnsys/oggui/src/
cp -pr ogWebconsole/dist/oggui/browser/* debian/oggui/opt/opengnsys/oggui/browser/
rm -rf debian/oggui/opt/opengnsys/oggui/browser/node_modules
cp -pr etc debian/oggui/opt/opengnsys/oggui/
cp -pr bin debian/oggui/opt/opengnsys/oggui/
cp -pr var debian/oggui/opt/opengnsys/oggui/
cp -p ogWebconsole/.env debian/oggui/opt/opengnsys/oggui/src/
md5sum debian/oggui/opt/opengnsys/oggui/src/.env > debian/oggui/opt/opengnsys/oggui/var/lib/oggui/oggui.config.hash

View File

@ -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

View File

@ -1,2 +1,2 @@
# NG_APP_BASE_API_URL=https://127.0.0.1:8443 NG_APP_BASE_API_URL=https://127.0.0.1:8443
# NG_APP_OGCORE_MERCURE_BASE_URL=http://localhost:3000/.well-known/mercure NG_APP_OGCORE_MERCURE_BASE_URL=http://localhost:3000/.well-known/mercure

View File

@ -1,2 +0,0 @@
NG_APP_BASE_API_URL=https://localhost:8443
NG_APP_OGCORE_MERCURE_BASE_URL=http://localhost:3000/.well-known/mercure

View File

@ -41,5 +41,3 @@ testem.log
.DS_Store .DS_Store
Thumbs.db Thumbs.db
test-results/

View File

@ -4,6 +4,12 @@
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"ogWebconsole": { "ogWebconsole": {
"i18n": {
"sourceLocale": "es",
"locales": {
"en": "src/locale/en.json"
}
},
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
@ -24,7 +30,7 @@
"builder": "@ngx-env/builder:application", "builder": "@ngx-env/builder:application",
"options": { "options": {
"baseHref": "/oggui/", "baseHref": "/oggui/",
"localize": false, "localize": true,
"aot": true, "aot": true,
"outputPath": "dist/og-webconsole", "outputPath": "dist/og-webconsole",
"index": "src/index.html", "index": "src/index.html",
@ -48,10 +54,7 @@
"src/styles.css", "src/styles.css",
"node_modules/ngx-toastr/toastr.css" "node_modules/ngx-toastr/toastr.css"
], ],
"scripts": [], "scripts": []
"allowedCommonJsDependencies": [
"rfdc"
]
}, },
"configurations": { "configurations": {
"production": { "production": {
@ -63,7 +66,7 @@
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "7kb", "maximumWarning": "4kb",
"maximumError": "10kb" "maximumError": "10kb"
} }
], ],
@ -73,6 +76,16 @@
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": false "sourceMap": false
},
"es": {
"localize": [
"es-ES"
]
},
"en": {
"localize": [
"en-US"
]
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
@ -91,16 +104,29 @@
}, },
"development": { "development": {
"buildTarget": "ogWebconsole:build:development" "buildTarget": "ogWebconsole:build:development"
},
"es": {
"buildTarget": "ogWebconsole:build:es"
},
"en": {
"buildTarget": "ogWebconsole:build:en"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
}, },
"extract-i18n": {
"builder": "@ngx-env/builder:extract-i18n",
"options": {
"buildTarget": "ogWebconsole:build"
}
},
"test": { "test": {
"builder": "@ngx-env/builder:karma", "builder": "@ngx-env/builder:karma",
"options": { "options": {
"polyfills": [ "polyfills": [
"zone.js", "zone.js",
"zone.js/testing" "zone.js/testing",
"@angular/localize/init"
], ],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"assets": [ "assets": [

View File

View File

View File

@ -40,9 +40,6 @@ import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
import {MenusComponent} from "./components/menus/menus.component"; import {MenusComponent} from "./components/menus/menus.component";
import {OgDhcpSubnetsComponent} from "./components/ogdhcp/og-dhcp-subnets.component"; import {OgDhcpSubnetsComponent} from "./components/ogdhcp/og-dhcp-subnets.component";
import {StatusComponent} from "./components/ogdhcp/status/status.component"; import {StatusComponent} from "./components/ogdhcp/status/status.component";
import {
RunScriptAssistantComponent
} from "./components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component";
const routes: Routes = [ const routes: Routes = [
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' }, { path: '', redirectTo: 'auth/login', pathMatch: 'full' },
{ path: '', component: MainLayoutComponent, { path: '', component: MainLayoutComponent,
@ -66,9 +63,9 @@ const routes: Routes = [
{ path: 'calendars', component: CalendarComponent }, { path: 'calendars', component: CalendarComponent },
{ path: 'clients/deploy-image', component: DeployImageComponent }, { path: 'clients/deploy-image', component: DeployImageComponent },
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent }, { path: 'clients/partition-assistant', component: PartitionAssistantComponent },
{ path: 'clients/run-script', component: RunScriptAssistantComponent },
{ path: 'clients/:id', component: ClientMainViewComponent }, { path: 'clients/:id', component: ClientMainViewComponent },
{ path: 'clients/:id/create-image', component: CreateClientImageComponent }, { path: 'clients/:id/create-image', component: CreateClientImageComponent },
{ path: 'images', component: ImagesComponent },
{ path: 'repositories', component: RepositoriesComponent }, { path: 'repositories', component: RepositoriesComponent },
{ path: 'repository/:id', component: MainRepositoryViewComponent }, { path: 'repository/:id', component: MainRepositoryViewComponent },
{ path: 'software', component: SoftwareComponent }, { path: 'software', component: SoftwareComponent },

View File

@ -1,5 +1,4 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, LOCALE_ID, APP_INITIALIZER } from '@angular/core'; import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ConfigService } from './services/config.service';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@ -102,7 +101,7 @@ import { OperativeSystemComponent } from './components/operative-system/operativ
import { CreateOperativeSystemComponent } from './components/operative-system/create-operative-system/create-operative-system.component'; import { CreateOperativeSystemComponent } from './components/operative-system/create-operative-system/create-operative-system.component';
import { ShowTemplateContentComponent } from './components/ogboot/pxe/show-template-content/show-template-content.component'; import { ShowTemplateContentComponent } from './components/ogboot/pxe/show-template-content/show-template-content.component';
import { RepositoriesComponent } from './components/repositories/repositories.component'; import { RepositoriesComponent } from './components/repositories/repositories.component';
import { ManageRepositoryComponent } from './components/repositories/manage-repository/manage-repository.component'; import { CreateRepositoryComponent } from './components/repositories/create-repository/create-repository.component';
import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component'; import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component';
import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component'; import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component';
import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component'; import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component';
@ -118,6 +117,7 @@ import { CreateMultipleClientComponent } from './components/groups/shared/client
import { ExportImageComponent } from './components/images/export-image/export-image.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 { 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 { 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 { 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 { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
@ -129,31 +129,10 @@ import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-
import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component'; import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component';
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.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'; import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component';
import { ConvertImageComponent } from './components/repositories/convert-image/convert-image.component';
import { registerLocaleData } from '@angular/common';
import localeEs from '@angular/common/locales/es';
import { GlobalStatusComponent } from './components/global-status/global-status.component';
import { ShowMonoliticImagesComponent } from './components/repositories/show-monolitic-images/show-monolitic-images.component';
import { StatusTabComponent } from './components/global-status/status-tab/status-tab.component';
import { ConvertImageToVirtualComponent } from './components/repositories/convert-image-to-virtual/convert-image-to-virtual.component';
import { RunScriptAssistantComponent } from './components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component';
import {
SaveScriptComponent
} from "./components/groups/components/client-main-view/run-script-assistant/save-script/save-script.component";
import { EditImageComponent } from './components/repositories/edit-image/edit-image.component';
import { ShowGitImagesComponent } from './components/repositories/show-git-images/show-git-images.component';
import { RenameImageComponent } from './components/repositories/rename-image/rename-image.component';
export function HttpLoaderFactory(http: HttpClient) { export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json'); return new TranslateHttpLoader(http, './locale/', '.json');
} }
export function initializeApp(configService: ConfigService) {
return () => configService.loadConfig();
}
registerLocaleData(localeEs, 'es-ES');
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
@ -216,7 +195,7 @@ registerLocaleData(localeEs, 'es-ES');
CreateOperativeSystemComponent, CreateOperativeSystemComponent,
ShowTemplateContentComponent, ShowTemplateContentComponent,
RepositoriesComponent, RepositoriesComponent,
ManageRepositoryComponent, CreateRepositoryComponent,
ExecuteCommandComponent, ExecuteCommandComponent,
ExecuteCommandOuComponent, ExecuteCommandOuComponent,
DeployImageComponent, DeployImageComponent,
@ -229,21 +208,12 @@ registerLocaleData(localeEs, 'es-ES');
ExportImageComponent, ExportImageComponent,
ImportImageComponent, ImportImageComponent,
LoadingComponent, LoadingComponent,
RepositoryImagesComponent,
InputDialogComponent, InputDialogComponent,
ManageOrganizationalUnitComponent, ManageOrganizationalUnitComponent,
BackupImageComponent, BackupImageComponent,
ShowClientsComponent, ShowClientsComponent,
OperationResultDialogComponent, OperationResultDialogComponent
ConvertImageComponent,
GlobalStatusComponent,
ShowMonoliticImagesComponent,
StatusTabComponent,
ConvertImageToVirtualComponent,
RunScriptAssistantComponent,
SaveScriptComponent,
EditImageComponent,
ShowGitImagesComponent,
RenameImageComponent
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
imports: [BrowserModule, imports: [BrowserModule,
@ -303,16 +273,8 @@ registerLocaleData(localeEs, 'es-ES');
useClass: CustomInterceptor, useClass: CustomInterceptor,
multi: true multi: true
}, },
{ provide: LOCALE_ID, useValue: 'es-ES' },
provideAnimationsAsync(), provideAnimationsAsync(),
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi())
ConfigService,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [ConfigService],
multi: true
}
], ],
}) })
export class AppModule { } export class AppModule { }

View File

@ -13,17 +13,12 @@ import { TranslateModule } from '@ngx-translate/core';
import { ToastrModule, ToastrService } from 'ngx-toastr'; import { ToastrModule, ToastrService } from 'ngx-toastr';
import { DataService } from '../users/users/data.service'; import { DataService } from '../users/users/data.service';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ConfigService } from '@services/config.service';
describe('EnvVarsComponent', () => { describe('EnvVarsComponent', () => {
let component: EnvVarsComponent; let component: EnvVarsComponent;
let fixture: ComponentFixture<EnvVarsComponent>; let fixture: ComponentFixture<EnvVarsComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [EnvVarsComponent], declarations: [EnvVarsComponent],
imports: [ imports: [
@ -52,10 +47,6 @@ describe('EnvVarsComponent', () => {
{ {
provide: MAT_DIALOG_DATA, provide: MAT_DIALOG_DATA,
useValue: {} useValue: {}
},
{
provide: ConfigService,
useValue: mockConfigService
} }
] ]
}).compileComponents(); }).compileComponents();

View File

@ -1,7 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-env-vars', selector: 'app-env-vars',
@ -9,17 +8,16 @@ import { ConfigService } from '@services/config.service';
styleUrl: './env-vars.component.css' styleUrl: './env-vars.component.css'
}) })
export class EnvVarsComponent { export class EnvVarsComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
envVars: { name: string; value: string }[] = []; envVars: { name: string; value: string }[] = [];
displayedColumns: string[] = ['name', 'value']; displayedColumns: string[] = ['name', 'value'];
private apiUrl: string;
private apiUrl = `${this.baseUrl}/env-vars`;
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private toastService: ToastrService, private toastService: ToastrService,
private configService: ConfigService ) {}
) {
this.apiUrl = `${this.configService.apiUrl}/env-vars`;
}
ngOnInit(): void { ngOnInit(): void {
this.loadEnvVars(); this.loadEnvVars();

View File

@ -4,7 +4,6 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import {FormBuilder, FormGroup, Validators} from '@angular/forms'; import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {DataService} from "../data.service"; import {DataService} from "../data.service";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-add-role-modal', selector: 'app-add-role-modal',
@ -12,9 +11,9 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./add-role-modal.component.css'] styleUrls: ['./add-role-modal.component.css']
}) })
export class AddRoleModalComponent { export class AddRoleModalComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
roleForm: FormGroup<any>; roleForm: FormGroup<any>;
roleId: string | null = null; roleId: string | null = null;
baseUrl: string;
constructor( constructor(
public dialogRef: MatDialogRef<AddRoleModalComponent>, public dialogRef: MatDialogRef<AddRoleModalComponent>,
@ -22,10 +21,8 @@ export class AddRoleModalComponent {
private http: HttpClient, private http: HttpClient,
private fb: FormBuilder, private fb: FormBuilder,
private dataService: DataService, private dataService: DataService,
private toastService: ToastrService, private toastService: ToastrService
private configService: ConfigService
) { ) {
this.baseUrl = this.configService.apiUrl;
this.roleForm = this.fb.group({ this.roleForm = this.fb.group({
name: ['', Validators.required], name: ['', Validators.required],
superAdmin: [false], superAdmin: [false],

View File

@ -2,19 +2,15 @@ import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http'; import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { ConfigService } from '@services/config.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DataService { export class DataService {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string; private apiUrl = `${this.baseUrl}/user-groups?page=1&itemsPerPage=1000`;
constructor(private http: HttpClient, private configService: ConfigService) { constructor(private http: HttpClient) {}
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/user-groups?page=1&itemsPerPage=1000`;
}
getUserGroups(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> { getUserGroups(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
const params = new HttpParams({ fromObject: filters }); const params = new HttpParams({ fromObject: filters });

View File

@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { ConfigService } from '@services/config.service'; import { of } from 'rxjs';
import { MatDivider } from '@angular/material/divider'; import { MatDivider } from '@angular/material/divider';
import { MatFormField } from '@angular/material/form-field'; import { MatFormField } from '@angular/material/form-field';
import { MatLabel } from '@angular/material/form-field'; import { MatLabel } from '@angular/material/form-field';
@ -20,14 +20,12 @@ describe('RolesComponent', () => {
let mockHttpClient: jasmine.SpyObj<HttpClient>; let mockHttpClient: jasmine.SpyObj<HttpClient>;
let mockToastrService: jasmine.SpyObj<ToastrService>; let mockToastrService: jasmine.SpyObj<ToastrService>;
let mockDataService: jasmine.SpyObj<DataService>; let mockDataService: jasmine.SpyObj<DataService>;
let mockConfigService: jasmine.SpyObj<ConfigService>;
beforeEach(async () => { beforeEach(async () => {
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']); const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']); const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']);
const toastrServiceSpy = jasmine.createSpyObj('ToastrService', ['success', 'error']); const toastrServiceSpy = jasmine.createSpyObj('ToastrService', ['success', 'error']);
const dataServiceSpy = jasmine.createSpyObj('DataService', ['getRoles']); const dataServiceSpy = jasmine.createSpyObj('DataService', ['getRoles']);
const configServiceSpy = jasmine.createSpyObj('ConfigService', [], { apiUrl: 'http://mock-api-url' });
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [RolesComponent, LoadingComponent], declarations: [RolesComponent, LoadingComponent],
@ -37,8 +35,7 @@ describe('RolesComponent', () => {
{ provide: MatDialog, useValue: matDialogSpy }, { provide: MatDialog, useValue: matDialogSpy },
{ provide: HttpClient, useValue: httpClientSpy }, { provide: HttpClient, useValue: httpClientSpy },
{ provide: ToastrService, useValue: toastrServiceSpy }, { provide: ToastrService, useValue: toastrServiceSpy },
{ provide: DataService, useValue: dataServiceSpy }, { provide: DataService, useValue: dataServiceSpy }
{ provide: ConfigService, useValue: configServiceSpy }
] ]
}).compileComponents(); }).compileComponents();
}); });
@ -50,7 +47,6 @@ describe('RolesComponent', () => {
mockHttpClient = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>; mockHttpClient = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
mockToastrService = TestBed.inject(ToastrService) as jasmine.SpyObj<ToastrService>; mockToastrService = TestBed.inject(ToastrService) as jasmine.SpyObj<ToastrService>;
mockDataService = TestBed.inject(DataService) as jasmine.SpyObj<DataService>; mockDataService = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
mockConfigService = TestBed.inject(ConfigService) as jasmine.SpyObj<ConfigService>;
}); });
it('should create', () => { it('should create', () => {

View File

@ -7,7 +7,6 @@ import { DataService } from "./data.service";
import { PageEvent } from "@angular/material/paginator"; import { PageEvent } from "@angular/material/paginator";
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component'; import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
import { AddRoleModalComponent } from './add-role-modal/add-role-modal.component'; import { AddRoleModalComponent } from './add-role-modal/add-role-modal.component';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-roles', selector: 'app-roles',
@ -15,7 +14,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./roles.component.css'] styleUrls: ['./roles.component.css']
}) })
export class RolesComponent implements OnInit { export class RolesComponent implements OnInit {
baseUrl: string = this.configService.apiUrl; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
filters: { [key: string]: string } = {}; filters: { [key: string]: string } = {};
loading: boolean = false; loading: boolean = false;
@ -49,8 +48,7 @@ export class RolesComponent implements OnInit {
public dialog: MatDialog, public dialog: MatDialog,
private http: HttpClient, private http: HttpClient,
private dataService: DataService, private dataService: DataService,
private toastService: ToastrService, private toastService: ToastrService
private configService: ConfigService
) {} ) {}
ngOnInit() { ngOnInit() {

View File

@ -4,7 +4,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ToastrService } from "ngx-toastr"; import { ToastrService } from "ngx-toastr";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { DataService } from "../data.service"; import { DataService } from "../data.service";
import { ConfigService } from '@services/config.service';
interface UserGroup { interface UserGroup {
'@id': string; '@id': string;
@ -18,7 +17,7 @@ interface UserGroup {
styleUrls: ['./add-user-modal.component.css'] styleUrls: ['./add-user-modal.component.css']
}) })
export class AddUserModalComponent implements OnInit { export class AddUserModalComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
@Output() userAdded = new EventEmitter<void>(); @Output() userAdded = new EventEmitter<void>();
@Output() userEdited = new EventEmitter<void>(); @Output() userEdited = new EventEmitter<void>();
userForm: FormGroup<any>; userForm: FormGroup<any>;
@ -39,10 +38,8 @@ export class AddUserModalComponent implements OnInit {
private http: HttpClient, private http: HttpClient,
private fb: FormBuilder, private fb: FormBuilder,
private dataService: DataService, private dataService: DataService,
private toastService: ToastrService, private toastService: ToastrService
private configService: ConfigService
) { ) {
this.baseUrl = this.configService.apiUrl;
this.userForm = this.fb.group({ this.userForm = this.fb.group({
username: ['', Validators.required], username: ['', Validators.required],
password: ['', Validators.required], password: ['', Validators.required],

View File

@ -3,19 +3,15 @@ import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http'; import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { ConfigService } from '@services/config.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DataService { export class DataService {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string; private apiUrl = `${this.baseUrl}/users?page=1&itemsPerPage=1000`;
constructor(private http: HttpClient, private configService: ConfigService) { constructor(private http: HttpClient) {}
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/users?page=1&itemsPerPage=1000`;
}
getUsers(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> { getUsers(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
const params = new HttpParams({ fromObject: filters }); const params = new HttpParams({ fromObject: filters });

View File

@ -7,7 +7,6 @@ import { ToastrService } from 'ngx-toastr';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ConfigService } from '@services/config.service';
class MockToastrService { class MockToastrService {
success() {} success() {}
@ -19,11 +18,6 @@ describe('UsersComponent', () => {
let fixture: ComponentFixture<UsersComponent>; let fixture: ComponentFixture<UsersComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [UsersComponent], declarations: [UsersComponent],
imports: [ imports: [
@ -34,7 +28,6 @@ describe('UsersComponent', () => {
], ],
providers: [ providers: [
{ provide: ToastrService, useClass: MockToastrService }, { provide: ToastrService, useClass: MockToastrService },
{ provide: ConfigService, useValue: mockConfigService }
], ],
schemas: [NO_ERRORS_SCHEMA], // Ignorar elementos desconocidos schemas: [NO_ERRORS_SCHEMA], // Ignorar elementos desconocidos
}).compileComponents(); }).compileComponents();

View File

@ -5,7 +5,7 @@ import { AddUserModalComponent } from './add-user-modal/add-user-modal.component
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component'; import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { ConfigService } from '@services/config.service'; import { DataService } from "./data.service";
@Component({ @Component({
selector: 'app-users', selector: 'app-users',
@ -13,8 +13,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./users.component.css'] styleUrls: ['./users.component.css']
}) })
export class UsersComponent implements OnInit { export class UsersComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string;
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
filters: { [key: string]: string } = {}; filters: { [key: string]: string } = {};
loading: boolean = false; loading: boolean = false;
@ -51,15 +50,14 @@ export class UsersComponent implements OnInit {
]; ];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/users`;
constructor( constructor(
public dialog: MatDialog, public dialog: MatDialog,
private configService: ConfigService,
private http: HttpClient, private http: HttpClient,
private dataService: DataService,
private toastService: ToastrService private toastService: ToastrService
) { ) {}
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/users`;
}
ngOnInit() { ngOnInit() {
this.search(); this.search();

View File

@ -16,18 +16,12 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { JoyrideModule, JoyrideService } from 'ngx-joyride'; import { JoyrideModule, JoyrideService } from 'ngx-joyride';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { LoadingComponent } from '../../shared/loading/loading.component'; import { LoadingComponent } from '../../shared/loading/loading.component';
import { ConfigService } from '@services/config.service';
describe('CalendarComponent', () => { describe('CalendarComponent', () => {
let component: CalendarComponent; let component: CalendarComponent;
let fixture: ComponentFixture<CalendarComponent>; let fixture: ComponentFixture<CalendarComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [CalendarComponent, LoadingComponent], declarations: [CalendarComponent, LoadingComponent],
imports: [ imports: [
@ -47,9 +41,6 @@ describe('CalendarComponent', () => {
JoyrideModule.forRoot(), JoyrideModule.forRoot(),
TranslateModule.forRoot(), TranslateModule.forRoot(),
], ],
providers: [
{ provide: ConfigService, useValue: mockConfigService }
]
}) })
.compileComponents(); .compileComponents();

View File

@ -9,7 +9,6 @@ import { PageEvent } from "@angular/material/paginator";
import { CreateCalendarComponent } from "./create-calendar/create-calendar.component"; import { CreateCalendarComponent } from "./create-calendar/create-calendar.component";
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component"; import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
import { JoyrideService } from 'ngx-joyride'; import { JoyrideService } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-calendar', selector: 'app-calendar',
@ -17,8 +16,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './calendar.component.css' styleUrl: './calendar.component.css'
}) })
export class CalendarComponent implements OnInit { export class CalendarComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string;
images: { downloadUrl: string; name: string; uuid: string }[] = []; images: { downloadUrl: string; name: string; uuid: string }[] = [];
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
length: number = 0; length: number = 0;
@ -54,18 +52,15 @@ export class CalendarComponent implements OnInit {
} }
]; ];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/remote-calendars`;
constructor( constructor(
public dialog: MatDialog, public dialog: MatDialog,
private http: HttpClient, private http: HttpClient,
private dataService: DataService, private dataService: DataService,
private toastService: ToastrService, private toastService: ToastrService,
private configService: ConfigService,
private joyrideService: JoyrideService private joyrideService: JoyrideService
) { ) {}
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/remote-calendars`;
}
ngOnInit(): void { ngOnInit(): void {
this.search(); this.search();

View File

@ -2,7 +2,6 @@ import { Component, Inject } from '@angular/core';
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-create-calendar-rule', selector: 'app-create-calendar-rule',
@ -10,7 +9,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './create-calendar-rule.component.css' styleUrl: './create-calendar-rule.component.css'
}) })
export class CreateCalendarRuleComponent { export class CreateCalendarRuleComponent {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
name: string = ''; name: string = '';
remoteCalendarRules: any[] = []; remoteCalendarRules: any[] = [];
weekDays: string[] = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo']; weekDays: string[] = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
@ -30,12 +29,9 @@ export class CreateCalendarRuleComponent {
constructor( constructor(
private toastService: ToastrService, private toastService: ToastrService,
private http: HttpClient, private http: HttpClient,
private configService: ConfigService,
public dialogRef: MatDialogRef<CreateCalendarRuleComponent>, public dialogRef: MatDialogRef<CreateCalendarRuleComponent>,
@Inject(MAT_DIALOG_DATA) public data: any, @Inject(MAT_DIALOG_DATA) public data: any,
) { ) { }
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void { ngOnInit(): void {
this.calendarId = this.data.calendar this.calendarId = this.data.calendar

View File

@ -5,7 +5,6 @@ import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dial
import {CreateCalendarRuleComponent} from "../create-calendar-rule/create-calendar-rule.component"; import {CreateCalendarRuleComponent} from "../create-calendar-rule/create-calendar-rule.component";
import {DataService} from "../data.service"; import {DataService} from "../data.service";
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component"; import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-create-calendar', selector: 'app-create-calendar',
@ -13,7 +12,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './create-calendar.component.css' styleUrl: './create-calendar.component.css'
}) })
export class CreateCalendarComponent implements OnInit { export class CreateCalendarComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
name: string = ''; name: string = '';
remoteCalendarRules: any[] = []; remoteCalendarRules: any[] = [];
isEditMode: boolean = false; isEditMode: boolean = false;
@ -23,14 +22,11 @@ export class CreateCalendarComponent implements OnInit {
constructor( constructor(
private toastService: ToastrService, private toastService: ToastrService,
private http: HttpClient, private http: HttpClient,
private configService: ConfigService,
public dialogRef: MatDialogRef<CreateCalendarComponent>, public dialogRef: MatDialogRef<CreateCalendarComponent>,
@Inject(MAT_DIALOG_DATA) public data: any, @Inject(MAT_DIALOG_DATA) public data: any,
public dialog: MatDialog, public dialog: MatDialog,
private dataService: DataService, private dataService: DataService,
) { ) { }
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void { ngOnInit(): void {
if (this.data) { if (this.data) {

View File

@ -2,19 +2,15 @@ import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http'; import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { ConfigService } from '@services/config.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DataService { export class DataService {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string; private apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=1000`;
constructor(private http: HttpClient, private configService: ConfigService) { constructor(private http: HttpClient) {}
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=1000`;
}
getRemoteCalendars(filters: { [key: string]: string }): Observable<any[]> { getRemoteCalendars(filters: { [key: string]: string }): Observable<any[]> {
const params = new HttpParams({ fromObject: filters }); const params = new HttpParams({ fromObject: filters });

View File

@ -8,7 +8,6 @@ import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/
import { MatTableDataSource } from "@angular/material/table"; import { MatTableDataSource } from "@angular/material/table";
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { JoyrideService } from 'ngx-joyride'; import { JoyrideService } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-commands-groups', selector: 'app-commands-groups',
@ -16,8 +15,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./commands-groups.component.css'] styleUrls: ['./commands-groups.component.css']
}) })
export class CommandsGroupsComponent implements OnInit { export class CommandsGroupsComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string;
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
filters: { [key: string]: string | boolean } = {}; filters: { [key: string]: string | boolean } = {};
length: number = 0; length: number = 0;
@ -49,12 +47,10 @@ export class CommandsGroupsComponent implements OnInit {
} }
]; ];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/command-groups`;
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService, constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
private configService: ConfigService, private joyrideService: JoyrideService) { private joyrideService: JoyrideService) {}
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/command-groups`;
}
ngOnInit(): void { ngOnInit(): void {
this.search(); this.search();

View File

@ -3,7 +3,6 @@ import { HttpClient } from '@angular/common/http';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-create-command-group', selector: 'app-create-command-group',
@ -11,25 +10,21 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./create-command-group.component.css'] styleUrls: ['./create-command-group.component.css']
}) })
export class CreateCommandGroupComponent implements OnInit { export class CreateCommandGroupComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
availableCommands: any[] = []; availableCommands: any[] = [];
selectedCommands: any[] = []; selectedCommands: any[] = [];
groupName: string = ''; groupName: string = '';
enabled: boolean = true; enabled: boolean = true;
editing: boolean = false; editing: boolean = false;
loading: boolean = false; loading: boolean = false;
private apiUrl: string; private apiUrl = `${this.baseUrl}/commands`;
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private dialogRef: MatDialogRef<CreateCommandGroupComponent>, private dialogRef: MatDialogRef<CreateCommandGroupComponent>,
private toastService: ToastrService, private toastService: ToastrService,
private configService: ConfigService,
@Inject(MAT_DIALOG_DATA) public data: any @Inject(MAT_DIALOG_DATA) public data: any
) { ) {}
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/commands`;
}
ngOnInit(): void { ngOnInit(): void {
this.loadAvailableCommands(); this.loadAvailableCommands();

View File

@ -3,7 +3,6 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-detail-command-group', selector: 'app-detail-command-group',
@ -11,7 +10,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./detail-command-group.component.css'] styleUrls: ['./detail-command-group.component.css']
}) })
export class DetailCommandGroupComponent implements OnInit { export class DetailCommandGroupComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
form!: FormGroup; form!: FormGroup;
clients: any[] = []; clients: any[] = [];
showClientSelect = false; showClientSelect = false;
@ -22,12 +21,9 @@ export class DetailCommandGroupComponent implements OnInit {
@Inject(MAT_DIALOG_DATA) public data: any, @Inject(MAT_DIALOG_DATA) public data: any,
private dialogRef: MatDialogRef<DetailCommandGroupComponent>, private dialogRef: MatDialogRef<DetailCommandGroupComponent>,
private fb: FormBuilder, private fb: FormBuilder,
private configService: ConfigService,
private http: HttpClient, private http: HttpClient,
private toastService: ToastrService private toastService: ToastrService
) { ) { }
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void { ngOnInit(): void {
this.form = this.fb.group({ this.form = this.fb.group({

View File

@ -13,16 +13,11 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { LoadingComponent } from '../../../shared/loading/loading.component'; import { LoadingComponent } from '../../../shared/loading/loading.component';
import { JoyrideModule, JoyrideService, JoyrideStepService } from 'ngx-joyride'; import { JoyrideModule, JoyrideService, JoyrideStepService } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
describe('CommandsTaskComponent', () => { describe('CommandsTaskComponent', () => {
let component: CommandsTaskComponent; let component: CommandsTaskComponent;
let fixture: ComponentFixture<CommandsTaskComponent>; let fixture: ComponentFixture<CommandsTaskComponent>;
let mockConfigService: jasmine.SpyObj<ConfigService>;
beforeEach(async () => { beforeEach(async () => {
const configServiceSpy = jasmine.createSpyObj('ConfigService', [], { apiUrl: 'http://mock-api-url' });
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [
HttpClientTestingModule, HttpClientTestingModule,
@ -38,13 +33,8 @@ describe('CommandsTaskComponent', () => {
JoyrideModule.forRoot(), JoyrideModule.forRoot(),
], ],
declarations: [CommandsTaskComponent, LoadingComponent], declarations: [CommandsTaskComponent, LoadingComponent],
providers: [
{ provide: ConfigService, useValue: configServiceSpy }
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
mockConfigService = TestBed.inject(ConfigService) as jasmine.SpyObj<ConfigService>;
}); });
beforeEach(() => { beforeEach(() => {

View File

@ -6,7 +6,6 @@ import { CreateTaskComponent } from './create-task/create-task.component';
import { DetailTaskComponent } from './detail-task/detail-task.component'; import { DetailTaskComponent } from './detail-task/detail-task.component';
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component'; import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { JoyrideService } from 'ngx-joyride'; import { JoyrideService } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-commands-task', selector: 'app-commands-task',
@ -14,7 +13,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./commands-task.component.css'] styleUrls: ['./commands-task.component.css']
}) })
export class CommandsTaskComponent implements OnInit { export class CommandsTaskComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
tasks: any[] = []; tasks: any[] = [];
filters: { [key: string]: string | boolean } = {}; filters: { [key: string]: string | boolean } = {};
length: number = 0; length: number = 0;
@ -23,14 +22,10 @@ export class CommandsTaskComponent implements OnInit {
pageSizeOptions: number[] = [5, 10, 20, 40, 100]; pageSizeOptions: number[] = [5, 10, 20, 40, 100];
displayedColumns: string[] = ['taskid', 'notes', 'name', 'scheduledDate', 'enabled', 'actions']; displayedColumns: string[] = ['taskid', 'notes', 'name', 'scheduledDate', 'enabled', 'actions'];
loading: boolean = false; loading: boolean = false;
private apiUrl: string; private apiUrl = `${this.baseUrl}/command-tasks`;
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService, constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
private configService: ConfigService, private joyrideService: JoyrideService) {}
private joyrideService: JoyrideService) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/command-tasks`;
}
ngOnInit(): void { ngOnInit(): void {
this.loadTasks(); this.loadTasks();

View File

@ -3,7 +3,6 @@ import { HttpClient } from '@angular/common/http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-create-task', selector: 'app-create-task',
@ -11,12 +10,12 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./create-task.component.css'] styleUrls: ['./create-task.component.css']
}) })
export class CreateTaskComponent implements OnInit { export class CreateTaskComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
taskForm: FormGroup; taskForm: FormGroup;
availableCommandGroups: any[] = []; availableCommandGroups: any[] = [];
selectedGroupCommands: any[] = []; selectedGroupCommands: any[] = [];
availableIndividualCommands: any[] = []; availableIndividualCommands: any[] = [];
apiUrl: string; apiUrl = `${this.baseUrl}/command-tasks`;
editing: boolean = false; editing: boolean = false;
availableOrganizationalUnits: any[] = []; availableOrganizationalUnits: any[] = [];
selectedUnitChildren: any[] = []; selectedUnitChildren: any[] = [];
@ -26,13 +25,10 @@ export class CreateTaskComponent implements OnInit {
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
private http: HttpClient, private http: HttpClient,
private configService: ConfigService,
private toastr: ToastrService, private toastr: ToastrService,
public dialogRef: MatDialogRef<CreateTaskComponent>, public dialogRef: MatDialogRef<CreateTaskComponent>,
@Inject(MAT_DIALOG_DATA) public data: any @Inject(MAT_DIALOG_DATA) public data: any
) { ) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/command-tasks`;
this.taskForm = this.fb.group({ this.taskForm = this.fb.group({
commandGroup: ['', Validators.required], commandGroup: ['', Validators.required],
extraCommands: [[]], extraCommands: [[]],

View File

@ -7,10 +7,9 @@ import { DatePipe } from '@angular/common';
import { JoyrideService } from 'ngx-joyride'; import { JoyrideService } from 'ngx-joyride';
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
import { InputDialogComponent } from "./input-dialog/input-dialog.component"; import { InputDialogComponent } from "./input-dialog/input-dialog.component";
import { ProgressBarMode } from '@angular/material/progress-bar'; import { ProgressBarMode, MatProgressBarModule } from '@angular/material/progress-bar';
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component"; import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-task-logs', selector: 'app-task-logs',
@ -18,8 +17,8 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./task-logs.component.css'] styleUrls: ['./task-logs.component.css']
}) })
export class TaskLogsComponent implements OnInit { export class TaskLogsComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
mercureUrl: string; mercureUrl: string = import.meta.env.NG_APP_OGCORE_MERCURE_BASE_URL;
traces: any[] = []; traces: any[] = [];
groupedTraces: any[] = []; groupedTraces: any[] = [];
commands: any[] = []; commands: any[] = [];
@ -93,12 +92,8 @@ export class TaskLogsComponent implements OnInit {
private joyrideService: JoyrideService, private joyrideService: JoyrideService,
private dialog: MatDialog, private dialog: MatDialog,
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
private configService: ConfigService,
private toastService: ToastrService private toastService: ToastrService
) { ) { }
this.baseUrl = this.configService.apiUrl;
this.mercureUrl = this.configService.mercureUrl;
}
ngOnInit(): void { ngOnInit(): void {
this.loadTraces(); this.loadTraces();
@ -228,6 +223,7 @@ export class TaskLogsComponent implements OnInit {
this.http.get<any>(`${this.baseUrl}/commands?&page=1&itemsPerPage=10000`).subscribe( this.http.get<any>(`${this.baseUrl}/commands?&page=1&itemsPerPage=10000`).subscribe(
response => { response => {
this.commands = response['hydra:member']; this.commands = response['hydra:member'];
console.log(this.commands);
this.loading = false; this.loading = false;
}, },
error => { error => {

View File

@ -20,18 +20,12 @@ import { NgxChartsModule } from '@swimlane/ngx-charts';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { JoyrideModule } from 'ngx-joyride'; import { JoyrideModule } from 'ngx-joyride';
import { LoadingComponent } from '../../../shared/loading/loading.component'; import { LoadingComponent } from '../../../shared/loading/loading.component';
import { ConfigService } from '@services/config.service';
describe('CommandsComponent', () => { describe('CommandsComponent', () => {
let component: CommandsComponent; let component: CommandsComponent;
let fixture: ComponentFixture<CommandsComponent>; let fixture: ComponentFixture<CommandsComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [CommandsComponent, LoadingComponent], declarations: [CommandsComponent, LoadingComponent],
imports: [ imports: [
@ -59,8 +53,7 @@ describe('CommandsComponent', () => {
], ],
providers: [ providers: [
{ provide: MatDialogRef, useValue: {} }, { provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: {} }, { provide: MAT_DIALOG_DATA, useValue: {} }
{ provide: ConfigService, useValue: mockConfigService }
] ]
}).compileComponents(); }).compileComponents();

View File

@ -7,7 +7,7 @@ import { CreateCommandComponent } from './create-command/create-command.componen
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component'; import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { ConfigService } from '@services/config.service'; import { ExecuteCommandComponent } from './execute-command/execute-command.component';
import { JoyrideService } from 'ngx-joyride'; import { JoyrideService } from 'ngx-joyride';
@Component({ @Component({
@ -16,8 +16,7 @@ import { JoyrideService } from 'ngx-joyride';
styleUrls: ['./commands.component.css'] styleUrls: ['./commands.component.css']
}) })
export class CommandsComponent implements OnInit { export class CommandsComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string;
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
filters: { [key: string]: string | boolean } = {}; filters: { [key: string]: string | boolean } = {};
length: number = 0; length: number = 0;
@ -49,12 +48,10 @@ export class CommandsComponent implements OnInit {
} }
]; ];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/commands`;
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService, constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
private joyrideService: JoyrideService, private configService: ConfigService) { private joyrideService: JoyrideService) {}
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/commands`;
}
ngOnInit(): void { ngOnInit(): void {
this.search(); this.search();
@ -85,14 +82,14 @@ export class CommandsComponent implements OnInit {
openCreateCommandModal(): void { openCreateCommandModal(): void {
this.dialog.open(CreateCommandComponent, { this.dialog.open(CreateCommandComponent, {
width: '800px', width: '600px',
}).afterClosed().subscribe(() => this.search()); }).afterClosed().subscribe(() => this.search());
} }
editCommand(event: MouseEvent, command: any): void { editCommand(event: MouseEvent, command: any): void {
event.stopPropagation(); event.stopPropagation();
this.dialog.open(CreateCommandComponent, { this.dialog.open(CreateCommandComponent, {
width: '800px', width: '600px',
data: command['@id'] data: command['@id']
}).afterClosed().subscribe(() => this.search()); }).afterClosed().subscribe(() => this.search());
} }

View File

@ -58,14 +58,3 @@
gap: 1em; gap: 1em;
padding: 1.5em; padding: 1.5em;
} }
.checkbox-with-hint {
display: flex;
flex-direction: column;
}
.hint-text {
font-size: 12px;
color: gray;
margin-left: 40px;
}

View File

@ -13,13 +13,7 @@
<div class="checkbox-group"> <div class="checkbox-group">
<mat-checkbox formControlName="readOnly">{{ 'readOnlyLabel' | translate }}</mat-checkbox> <mat-checkbox formControlName="readOnly">{{ 'readOnlyLabel' | translate }}</mat-checkbox>
<mat-checkbox formControlName="enabled">{{ 'enabledLabel' | translate }}</mat-checkbox> <mat-checkbox formControlName="enabled">{{ 'enabledLabel' | translate }}</mat-checkbox>
<div class="checkbox-with-hint">
<mat-checkbox formControlName="parameters">{{ 'parameters' | translate }}</mat-checkbox>
<span class="hint-text">Si se selecciona esta opción los parámetros deben indicarse en el script con el símbolo &#64;.</span>
</div>
</div> </div>
<mat-form-field appearance="fill" class="full-width"> <mat-form-field appearance="fill" class="full-width">

View File

@ -13,18 +13,12 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ConfigService } from '@services/config.service';
describe('CreateCommandComponent', () => { describe('CreateCommandComponent', () => {
let component: CreateCommandComponent; let component: CreateCommandComponent;
let fixture: ComponentFixture<CreateCommandComponent>; let fixture: ComponentFixture<CreateCommandComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [CreateCommandComponent], declarations: [CreateCommandComponent],
imports: [ imports: [
@ -51,8 +45,7 @@ describe('CreateCommandComponent', () => {
{ {
provide: MAT_DIALOG_DATA, provide: MAT_DIALOG_DATA,
useValue: {} useValue: {}
}, }
{ provide: ConfigService, useValue: mockConfigService }
] ]
}).compileComponents(); }).compileComponents();
}); });

View File

@ -1,19 +1,19 @@
import {Component, Inject, OnInit} from '@angular/core'; import { Component, Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import {DataService} from "../data.service"; import {DataService} from "../data.service";
import { ConfigService } from "@services/config.service";
@Component({ @Component({
selector: 'app-create-command', selector: 'app-create-command',
templateUrl: './create-command.component.html', templateUrl: './create-command.component.html',
styleUrls: ['./create-command.component.css'] styleUrls: ['./create-command.component.css']
}) })
export class CreateCommandComponent implements OnInit{ export class CreateCommandComponent {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
createCommandForm: FormGroup<any>; createCommandForm: FormGroup<any>;
private apiUrl = `${this.baseUrl}/commands`;
commandId: string | null = null; commandId: string | null = null;
constructor( constructor(
@ -21,16 +21,13 @@ export class CreateCommandComponent implements OnInit{
private http: HttpClient, private http: HttpClient,
public dialogRef: MatDialogRef<CreateCommandComponent>, public dialogRef: MatDialogRef<CreateCommandComponent>,
private toastService: ToastrService, private toastService: ToastrService,
private configService: ConfigService,
private dataService: DataService, private dataService: DataService,
@Inject(MAT_DIALOG_DATA) public data: any @Inject(MAT_DIALOG_DATA) public data: any
) { ) {
this.baseUrl = this.configService.apiUrl;
this.createCommandForm = this.fb.group({ this.createCommandForm = this.fb.group({
name: ['', Validators.required], name: ['', Validators.required],
script: [''], script: [''],
readOnly: [false], readOnly: [false],
parameters: [false],
enabled: [true], enabled: [true],
comments: [''], comments: [''],
}); });
@ -45,12 +42,12 @@ export class CreateCommandComponent implements OnInit{
load(): void { load(): void {
this.dataService.getCommand(this.data).subscribe({ this.dataService.getCommand(this.data).subscribe({
next: (response) => { next: (response) => {
console.log(response);
this.createCommandForm = this.fb.group({ this.createCommandForm = this.fb.group({
name: [response.name, Validators.required], name: [response.name, Validators.required],
notes: [response.notes], notes: [response.notes],
script: [response.script], script: [response.script],
readOnly: [response.readOnly], readOnly: [response.readOnly],
parameters: [response.parameters],
enabled: [response.enabled], enabled: [response.enabled],
}); });
this.commandId = response['@id']; this.commandId = response['@id'];
@ -85,6 +82,7 @@ export class CreateCommandComponent implements OnInit{
}, },
(error) => { (error) => {
this.toastService.error(error['error']['hydra:description']); this.toastService.error(error['error']['hydra:description']);
console.error('Error al editar el comando', error);
} }
); );
} else { } else {
@ -95,6 +93,7 @@ export class CreateCommandComponent implements OnInit{
}, },
(error) => { (error) => {
this.toastService.error(error['error']['hydra:description']); this.toastService.error(error['error']['hydra:description']);
console.error('Error al añadir comando', error);
} }
); );
} }

View File

@ -1,4 +1,4 @@
import { ConfigService } from '@services/config.service';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http'; import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
@ -8,16 +8,10 @@ import { catchError, map } from 'rxjs/operators';
providedIn: 'root' providedIn: 'root'
}) })
export class DataService { export class DataService {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string; private apiUrl = `${this.baseUrl}/commands?page=1&itemsPerPage=1000`;
constructor( constructor(private http: HttpClient) {}
private http: HttpClient,
private configService: ConfigService
) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/commands?page=1&itemsPerPage=1000`;
}
getCommands(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> { getCommands(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
const params = new HttpParams({ fromObject: filters }); const params = new HttpParams({ fromObject: filters });

View File

@ -4,7 +4,6 @@ import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dial
import { CreateCommandComponent } from '../create-command/create-command.component'; import { CreateCommandComponent } from '../create-command/create-command.component';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-command-detail', selector: 'app-command-detail',
@ -12,7 +11,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./command-detail.component.css'] styleUrls: ['./command-detail.component.css']
}) })
export class CommandDetailComponent implements OnInit { export class CommandDetailComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
form!: FormGroup; form!: FormGroup;
clients: any[] = []; clients: any[] = [];
showClientSelect = false; showClientSelect = false;
@ -21,15 +20,12 @@ export class CommandDetailComponent implements OnInit {
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
private configService: ConfigService,
private http: HttpClient, private http: HttpClient,
public dialogRef: MatDialogRef<CommandDetailComponent>, public dialogRef: MatDialogRef<CommandDetailComponent>,
private dialog: MatDialog, private dialog: MatDialog,
private toastService: ToastrService, private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: any @Inject(MAT_DIALOG_DATA) public data: any
) { ) { }
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void { ngOnInit(): void {
this.form = this.fb.group({ this.form = this.fb.group({

View File

@ -8,17 +8,11 @@
[matMenuTriggerFor]="commandMenu"> [matMenuTriggerFor]="commandMenu">
{{ buttonText }} {{ buttonText }}
</button> </button>
<button mat-menu-item *ngSwitchCase="'menu-item'" [matMenuTriggerFor]="commandMenu" [disabled]="disabled">
<mat-icon>{{ icon }}</mat-icon>
<span>{{ buttonText }}</span>
</button>
</ng-container> </ng-container>
<mat-menu #commandMenu="matMenu"> <mat-menu #commandMenu="matMenu">
<button mat-menu-item [disabled]="command.disabled <button mat-menu-item [disabled]="command.disabled || (command.slug === 'create-image' && clientData.length > 1)"
|| (command.slug === 'create-image' && clientData.length > 1)" *ngFor="let command of arrayCommands" *ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
(click)="onCommandSelect(command.slug)">
{{ command.name }} {{ command.name }}
</button> </button>
</mat-menu> </mat-menu>

View File

@ -1,4 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExecuteCommandComponent } from './execute-command.component'; import { ExecuteCommandComponent } from './execute-command.component';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing'; import { provideHttpClientTesting } from '@angular/common/http/testing';
@ -16,18 +17,12 @@ import { ToastrModule, ToastrService } from 'ngx-toastr';
import { DataService } from '../data.service'; import { DataService } from '../data.service';
import {MatIconModule} from "@angular/material/icon"; import {MatIconModule} from "@angular/material/icon";
import {MatMenu, MatMenuModule} from "@angular/material/menu"; import {MatMenu, MatMenuModule} from "@angular/material/menu";
import { ConfigService } from '@services/config.service';
describe('ExecuteCommandComponent', () => { describe('ExecuteCommandComponent', () => {
let component: ExecuteCommandComponent; let component: ExecuteCommandComponent;
let fixture: ComponentFixture<ExecuteCommandComponent>; let fixture: ComponentFixture<ExecuteCommandComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ExecuteCommandComponent], declarations: [ExecuteCommandComponent],
imports: [ imports: [
@ -59,8 +54,7 @@ describe('ExecuteCommandComponent', () => {
{ {
provide: MAT_DIALOG_DATA, provide: MAT_DIALOG_DATA,
useValue: {} useValue: {}
}, }
{ provide: ConfigService, useValue: mockConfigService }
] ]
}) })
.compileComponents(); .compileComponents();

View File

@ -1,8 +1,9 @@
import { Component, Input, OnInit } from '@angular/core'; import {Component, Inject, Input, OnInit, SimpleChanges} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup } from '@angular/forms';
import {Router} from "@angular/router"; import {Router} from "@angular/router";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-execute-command', selector: 'app-execute-command',
@ -11,11 +12,11 @@ import { ConfigService } from '@services/config.service';
}) })
export class ExecuteCommandComponent implements OnInit { export class ExecuteCommandComponent implements OnInit {
@Input() clientData: any[] = []; @Input() clientData: any[] = [];
@Input() buttonType: 'icon' | 'text' | 'menu-item' = 'icon'; @Input() buttonType: 'icon' | 'text' = 'icon';
@Input() buttonText: string = 'Ejecutar Comandos'; @Input() buttonText: string = 'Ejecutar Comandos';
@Input() icon: string = 'terminal'; @Input() icon: string = 'terminal';
@Input() disabled: boolean = false; @Input() disabled: boolean = false;
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
loading: boolean = true; loading: boolean = true;
arrayCommands: any[] = [ arrayCommands: any[] = [
@ -29,18 +30,18 @@ export class ExecuteCommandComponent implements OnInit {
{name: 'Particionar y Formatear', slug: 'partition', disabled: false}, {name: 'Particionar y Formatear', slug: 'partition', disabled: false},
{name: 'Inventario Software', slug: 'software-inventory', disabled: true}, {name: 'Inventario Software', slug: 'software-inventory', disabled: true},
{name: 'Inventario Hardware', slug: 'hardware-inventory', disabled: true}, {name: 'Inventario Hardware', slug: 'hardware-inventory', disabled: true},
{ name: 'Ejecutar comando', slug: 'run-script', disabled: false }, {name: 'Ejecutar script', slug: 'run-script', disabled: true},
]; ];
client: any = {}; client: any = {};
constructor( constructor(
private dialog: MatDialog,
private http: HttpClient, private http: HttpClient,
private fb: FormBuilder,
private router: Router, private router: Router,
private configService: ConfigService,
private toastService: ToastrService private toastService: ToastrService
) { ) {
this.baseUrl = this.configService.apiUrl;
} }
ngOnInit(): void { ngOnInit(): void {
@ -60,14 +61,6 @@ export class ExecuteCommandComponent implements OnInit {
this.openDeployImageAssistant(); this.openDeployImageAssistant();
} }
if (action === 'run-script') {
this.openRunScriptAssistant();
}
if (action === 'login') {
this.loginClient();
}
if (action === 'reboot') { if (action === 'reboot') {
this.rebootClient(); this.rebootClient();
} }
@ -94,19 +87,6 @@ export class ExecuteCommandComponent implements OnInit {
); );
} }
loginClient(): void {
this.http.post(`${this.baseUrl}/clients/server/login-client`, {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
}
);
}
powerOnClient(): void { powerOnClient(): void {
this.http.post(`${this.baseUrl}/image-repositories/wol`, { this.http.post(`${this.baseUrl}/image-repositories/wol`, {
clients: this.clientData.map((client: any) => client['@id']) clients: this.clientData.map((client: any) => client['@id'])
@ -134,18 +114,8 @@ export class ExecuteCommandComponent implements OnInit {
} }
openPartitionAssistant(): void { openPartitionAssistant(): void {
const clientDataToSend = this.clientData.map(client => ({
name: client.name,
mac: client.mac,
uuid: '/clients/'+client.uuid,
status: client.status,
partitions: client.partitions,
firmwareType: client.firmwareType,
ip: client.ip
}));
this.router.navigate(['/clients/partition-assistant'], { this.router.navigate(['/clients/partition-assistant'], {
queryParams: { clientData: JSON.stringify(clientDataToSend) } state: { clientData: this.clientData },
}).then(r => { }).then(r => {
console.log('Navigated to partition assistant with data:', this.clientData); console.log('Navigated to partition assistant with data:', this.clientData);
}); });
@ -158,38 +128,10 @@ export class ExecuteCommandComponent implements OnInit {
} }
openDeployImageAssistant(): void { openDeployImageAssistant(): void {
const clientDataToSend = this.clientData.map(client => ({
name: client.name,
mac: client.mac,
uuid: '/clients/'+client.uuid,
status: client.status,
partitions: client.partitions,
ip: client.ip
}));
this.router.navigate(['/clients/deploy-image'], { this.router.navigate(['/clients/deploy-image'], {
queryParams: { clientData: JSON.stringify(clientDataToSend) } state: { clientData: this.clientData },
}).then(r => { }).then(r => {
console.log('Navigated to deploy image with data:', this.clientData); console.log('Navigated to deploy image with data:', this.clientData);
}); });
} }
openRunScriptAssistant(): void {
const clientDataToSend = this.clientData.map(client => ({
name: client.name,
mac: client.mac,
uuid: '/clients/'+client.uuid,
status: client.status,
partitions: client.partitions,
ip: client.ip
}));
this.router.navigate(['/clients/run-script'], {
queryParams: { clientData: JSON.stringify(clientDataToSend) }
}).then(() => {
console.log('Navigated to run script with data:', clientDataToSend);
});
}
} }

View File

@ -1,44 +0,0 @@
mat-dialog-content {
height: calc(100% - 64px);
overflow: auto;
padding-top: 0.5em !important;
}
.content-container {
min-height: 100%;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
.spinner-container {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
}
.loading-spinner {
width: 100px;
height: 100px;
}
.error-card {
margin: 20px auto;
max-width: 600px;
text-align: center;
background-color: rgb(243, 243, 243);
color: rgb(48, 48, 48);
}
.error-card p {
margin-top: 0;
}

View File

@ -1,94 +0,0 @@
<header>
<h1 mat-dialog-title>{{'GlobalStatus' | translate}}</h1>
</header>
<mat-dialog-content [ngClass]="{'loading': loading}">
<div class="spinner-container" *ngIf="loading">
<mat-spinner class="loading-spinner"></mat-spinner>
</div>
<mat-tab-group (selectedTabChange)="onTabChange($event)">
<mat-tab label="OgBoot">
<div *ngIf="!loading && !errorOgBoot" class="content-container">
<app-status-tab
[loading]="loading"
[diskUsage]="ogBootDiskUsage"
[servicesStatus]="ogBootServicesStatus"
[installedOgLives]="installedOgLives"
[diskUsageChartData]="ogBootDiskUsageChartData"
[view]="view"
[colorScheme]="colorScheme"
[isDoughnut]="isDoughnut"
[showLabels]="showLabels"
[isDhcp]="isDhcp"
[isRepository]="false">
</app-status-tab>
</div>
<mat-card *ngIf="!loading && errorOgBoot" class="error-card">
<mat-card-content>
<p>{{ 'errorLoadingData' | translate }}</p>
</mat-card-content>
</mat-card>
</mat-tab>
<mat-tab label="Dhcp">
<div *ngIf="!loading && !errorDhcp" class="content-container">
<app-status-tab
[loading]="loading"
[diskUsage]="dhcpDiskUsage"
[servicesStatus]="dhcpServicesStatus"
[subnets]="subnets"
[diskUsageChartData]="dhcpDiskUsageChartData"
[view]="view"
[colorScheme]="colorScheme"
[isDoughnut]="isDoughnut"
[showLabels]="showLabels"
[isDhcp]="isDhcp"
[isRepository]="false">
</app-status-tab>
</div>
<mat-card *ngIf="!loading && errorDhcp" class="error-card">
<mat-card-content>
<p>{{ 'errorLoadingData' | translate }}</p>
</mat-card-content>
</mat-card>
</mat-tab>
<mat-tab label="Repositorios">
<mat-tab-group>
<mat-tab *ngFor="let repository of repositories" [label]="repository.name">
<div *ngIf="!loading && !errorRepositories[repository.uuid] && repositoryStatuses[repository.uuid]">
<app-status-tab
[loading]="loading"
[diskUsage]="repositoryStatuses[repository.uuid].disk"
[servicesStatus]="repositoryStatuses[repository.uuid].services"
[processesStatus]="repositoryStatuses[repository.uuid].processes"
[ramUsage]="repositoryStatuses[repository.uuid].ram"
[cpuUsage]="repositoryStatuses[repository.uuid].cpu"
[diskUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[repository.uuid].disk.used },
{ name: 'Disponible', value: repositoryStatuses[repository.uuid].disk.available }
]"
[ramUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[repository.uuid].ram.used },
{ name: 'Disponible', value: repositoryStatuses[repository.uuid].ram.available }
]"
[view]="view"
[colorScheme]="colorScheme"
[isDoughnut]="isDoughnut"
[showLabels]="showLabels"
[isDhcp]="false"
[isRepository]="true">
</app-status-tab>
</div>
<mat-card *ngIf="!loading && errorRepositories[repository.uuid]" class="error-card">
<mat-card-content>
<p>{{ 'errorLoadingData' | translate }}</p>
</mat-card-content>
</mat-card>
</mat-tab>
</mat-tab-group>
</mat-tab>
</mat-tab-group>
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" [mat-dialog-close]="true">{{ 'closeButton' | translate }}</button>
</mat-dialog-actions>

View File

@ -1,49 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ToastrModule } from 'ngx-toastr';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs';
import { GlobalStatusComponent } from './global-status.component';
import { ConfigService } from '@services/config.service';
import { LoadingComponent } from '../../shared/loading/loading.component';
import { TranslateModule } from '@ngx-translate/core';
import { NgxChartsModule } from '@swimlane/ngx-charts';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('GlobalStatusComponent', () => {
let component: GlobalStatusComponent;
let fixture: ComponentFixture<GlobalStatusComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url'
};
await TestBed.configureTestingModule({
declarations: [GlobalStatusComponent, LoadingComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
MatDialogModule,
MatTabsModule,
TranslateModule.forRoot(),
NgxChartsModule,
BrowserAnimationsModule
],
providers: [
{ provide: ConfigService, useValue: mockConfigService }
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
fixture = TestBed.createComponent(GlobalStatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,248 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { MatTabChangeEvent } from '@angular/material/tabs';
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-global-status',
templateUrl: './global-status.component.html',
styleUrl: './global-status.component.css'
})
export class GlobalStatusComponent implements OnInit {
baseUrl: string;
loading: boolean = false;
errorOgBoot: boolean = false;
errorDhcp: boolean = false;
errorRepositories: { [key: string]: boolean } = {};
installedOgLives: any[] = [];
subnets: any[] = [];
showLabels: boolean = true;
isDoughnut: boolean = true;
colorScheme: any = {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
repositoriesUrl: string;
repositories: any[] = [];
repositoryStatuses: { [key: string]: any } = {};
ogBootApiUrl: string;
ogBootDiskUsage: any = {};
ogBootServicesStatus: any = {};
ogBootDiskUsageChartData: any[] = [];
dhcpApiUrl: string;
dhcpDiskUsage: any = {};
dhcpServicesStatus: any = {};
dhcpDiskUsageChartData: any[] = [];
isDhcp: boolean = false;
isRepository: boolean = false;
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient
) {
this.baseUrl = this.configService.apiUrl;
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
this.dhcpApiUrl = `${this.baseUrl}/og-dhcp/status`;
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
}
ngOnInit(): void {
this.loadOgBootStatus();
this.syncSubnets()
this.syncTemplates()
this.syncOgLives()
}
syncSubnets() {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las subredes: tiempo de espera agotado');
}, 3500);
this.http.post(`${this.baseUrl}/subnets/sync`, {}).subscribe({
next: (response) => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización con componente DHCP exitosa');
},
error: (error) => {
clearTimeout(timeoutId);
this.toastService.error('Error al sincronizar las subredes DHCP');
}
});
}
syncTemplates() {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las plantillas Pxe: tiempo de espera agotado');
}, 3500);
this.http.post(`${this.baseUrl}/pxe-templates/sync`, {})
.subscribe(response => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización de las plantillas Pxe completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error('Error al sincronizar las plantillas Pxe');
});
}
syncOgLives(): void {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las imagenes ogLive : tiempo de espera agotado');
}, 3500);
this.http.post(`${this.baseUrl}/og-lives/sync`, {})
.subscribe(response => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización con los ogLives completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error('Error al sincronizar imágenes ogLive');
});
}
[key: string]: any;
loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string): void {
this.loading = true;
this[errorState] = false;
const timeoutId = setTimeout(() => {
this.loading = false;
this[errorState] = true;
}, 3500);
this.http.get<any>(apiUrl).subscribe({
next: data => {
diskUsage.used = data.message.disk_usage.used;
diskUsage.available = data.message.disk_usage.available;
diskUsage.total = data.message.disk_usage.total;
diskUsage.percentage = data.message.disk_usage.percentage;
Object.assign(servicesStatus, data.message.services_status);
if (isDhcp) {
this.subnets.length = 0;
if (data.message.subnets) {
this.subnets.push(...data.message.subnets);
}
} else {
installedOgLives.length = 0;
if (data.message.installed_oglives) {
installedOgLives.push(...data.message.installed_oglives);
}
}
diskUsageChartData.length = 0;
diskUsageChartData.push(
{ name: 'Usado', value: parseFloat(diskUsage.used) },
{ name: 'Disponible', value: parseFloat(diskUsage.available) }
);
this.loading = false;
clearTimeout(timeoutId);
},
error: error => {
console.log(error);
this.loading = false;
this[errorState] = true;
clearTimeout(timeoutId);
}
});
}
loadRepositories(): void {
this.loading = true;
this.errorRepositories = {};
const timeoutId = setTimeout(() => {
this.loading = false;
this.repositories.forEach(repository => {
if (!(repository.uuid in this.errorRepositories)) {
this.errorRepositories[repository.uuid] = true;
}
});
}, 5000);
this.http.get<any>(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe(
data => {
this.repositories = data['hydra:member'];
let remainingRepositories = this.repositories.length;
this.repositories.forEach(repository => {
this.loadRepositoryStatus(repository.uuid, (errorOccurred: boolean) => {
remainingRepositories--;
this.errorRepositories[repository.uuid] = errorOccurred;
if (remainingRepositories === 0) {
this.loading = false;
clearTimeout(timeoutId);
}
});
});
},
error => {
console.error('Error fetching repositories', error);
this.loading = false;
this.repositories.forEach(repository => {
this.errorRepositories[repository.uuid] = true;
});
clearTimeout(timeoutId);
}
);
}
loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${repositoryUuid}/status`).subscribe(
data => {
const output = data.output;
this.repositoryStatuses[repositoryUuid] = {
...output,
disk: {
...output.disk,
used: parseFloat(output.disk.used),
available: parseFloat(output.disk.available)
},
ram: {
...output.ram,
used: parseFloat(output.ram.used),
available: parseFloat(output.ram.available)
}
};
clearTimeout(timeoutId);
callback(false);
},
error => {
console.error(`Error fetching status for repository ${repositoryUuid}`, error);
clearTimeout(timeoutId);
callback(true);
}
);
}
loadOgBootStatus(): void {
this.isDhcp = false;
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot');
}
loadDhcpStatus(): void {
this.isDhcp = true;
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp');
}
onTabChange(event: MatTabChangeEvent): void {
if (event.tab.textLabel === 'OgBoot') {
this.loadOgBootStatus();
} else if (event.tab.textLabel === 'Dhcp') {
this.loadDhcpStatus();
} else if (event.tab.textLabel === 'Repositorios') {
if (this.repositories.length === 0) {
this.loadRepositories();
}
}
}
}

View File

@ -1,84 +0,0 @@
.dashboard {
display: flex;
flex-direction: column;
}
.disk-usage-container,
.ram-usage-container {
display: flex;
flex-direction: column;
}
.disk-usage,
.ram-usage {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.service-list,
.process-list {
margin-top: 0em;
margin-bottom: 0.5em;
}
.services-status,
.processes-status {
display: flex;
flex-direction: column;
}
.services-status li {
margin: 5px 0;
display: flex;
align-items: center;
}
.processes-status li {
margin: 5px 0;
display: flex;
align-items: center;
}
.status-led {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;
}
.status-led.active {
background-color: green;
}
.status-led.inactive {
background-color: red;
}
.disk-title,
.ram-title {
margin-bottom: 0px;
}
.service-title,
.process-title {
margin-top: 0px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
}
th {
background-color: #f4f4f4;
}

View File

@ -1,113 +0,0 @@
<app-loading [isLoading]="loading"></app-loading>
<div *ngIf="!loading" class="dashboard">
<!-- Disk Usage Section -->
<div class="disk-usage-container">
<h3 class="disk-title">{{ 'diskUsageTitle' | translate }}</h3>
<div class="disk-usage" joyrideStep="diskUsageStep" text="{{ 'diskUsageDescription' | translate }}">
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="diskUsageChartData" [doughnut]="isDoughnut"
[labels]="showLabels">
</ngx-charts-pie-chart>
<div class="disk-usage-info">
<p>{{ 'totalLabel' | translate }}: <strong>{{ isRepository ? diskUsage.total : formatBytes(diskUsage.total) }}</strong></p>
<p>{{ 'usedLabel' | translate }}: <strong>{{ isRepository ? diskUsage.used : formatBytes(diskUsage.used) }}</strong></p>
<p>{{ 'availableLabel' | translate }}: <strong>{{ isRepository ? diskUsage.available : formatBytes(diskUsage.available) }}</strong></p>
<p>{{ 'usedPercentageLabel' | translate }}: <strong>{{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}</strong></p>
</div>
</div>
</div>
<!-- RAM Usage Section -->
<div class="ram-usage-container" *ngIf="isRepository">
<h3 class="ram-title">{{ 'RamUsage' | translate }}</h3>
<div class="ram-usage">
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="ramUsageChartData" [doughnut]="isDoughnut"
[labels]="showLabels">
</ngx-charts-pie-chart>
<div class="ram-usage-info">
<p>{{ 'totalLabel' | translate }}: <strong>{{ ramUsage.total }}</strong></p>
<p>{{ 'usedLabel' | translate }}: <strong>{{ ramUsage.used }}</strong></p>
<p>{{ 'availableLabel' | translate }}: <strong>{{ ramUsage.available }}</strong></p>
<p>{{ 'usedPercentageLabel' | translate }}: <strong>{{ ramUsage.used_percentage }}</strong></p>
</div>
</div>
</div>
<!-- CPU Usage Section -->
<div class="cpu-usage-container" *ngIf="isRepository">
<h3 class="cpu-title">{{ 'CpuUsage' | translate }}</h3>
<div class="cpu-usage">
<p>{{ 'usedLabel' | translate }}: <strong>{{ cpuUsage.used_percentage }}</strong></p>
</div>
</div>
<!-- Services Status Section -->
<div class="services-status" joyrideStep="servicesStatusStep" text="{{ 'servicesStatusDescription' | translate }}">
<h3 class="service-title">{{ 'servicesTitle' | translate }}</h3>
<ul class="service-list">
<li *ngFor="let service of getServices()">
<span class="status-led"
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"></span>
{{ service.name }}: {{ service.status | translate }}
</li>
</ul>
</div>
<!-- Processes Status Section -->
<div class="processes-status" *ngIf="isRepository">
<h3 class="process-title">{{ 'processes' | translate }}</h3>
<ul class="process-list">
<li *ngFor="let process of getProcesses()">
<span class="status-led"
[ngClass]="{ 'active': process.status === 'running', 'inactive': process.status !== 'running' }"></span>
{{ process.name }}: {{ process.status }}
</li>
</ul>
</div>
<!-- Installed OgLives / Subnets Section -->
<div *ngIf="!isRepository && !isDhcp">
<h3>{{ 'InstalledOglivesTitle' | translate }}</h3>
<table>
<thead>
<tr>
<th>{{ 'idLabel' | translate }}</th>
<th>{{ 'kernelLabel' | translate }}</th>
<th>{{ 'architectureLabel' | translate }}</th>
<th>{{ 'revisionLabel' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of installedOgLives">
<td>{{ item.id }}</td>
<td>{{ item.kernel }}</td>
<td>{{ item.architecture }}</td>
<td>{{ item.revision }}</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="isDhcp">
<h3>{{ 'subnets' | translate }}</h3>
<table>
<thead>
<tr>
<th>{{ 'idLabel' | translate }}</th>
<th>{{ 'bootFileNameLabel' | translate }}</th>
<th>{{ 'nextServerLabel' | translate }}</th>
<th>{{ 'ipLabel' | translate }}</th>
<th>{{ 'clientsLabel' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of subnets">
<td>{{ item.id }}</td>
<td>{{ item['boot-file-name'] }}</td>
<td>{{ item['next-server'] }}</td>
<td>{{ item.subnet }}</td>
<td>{{ item.reservations.length }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -1,49 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ToastrModule } from 'ngx-toastr';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs';
import { ConfigService } from '@services/config.service';
import { LoadingComponent } from 'src/app/shared/loading/loading.component';
import { TranslateModule } from '@ngx-translate/core';
import { NgxChartsModule } from '@swimlane/ngx-charts';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { StatusTabComponent } from './status-tab.component';
describe('StatusTabComponent', () => {
let component: StatusTabComponent;
let fixture: ComponentFixture<StatusTabComponent>;
const mockConfigService = {
apiUrl: 'http://mock-api-url'
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [StatusTabComponent, LoadingComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
MatDialogModule,
MatTabsModule,
TranslateModule.forRoot(),
NgxChartsModule,
BrowserAnimationsModule
],
providers: [
{ provide: ConfigService, useValue: mockConfigService }
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
fixture = TestBed.createComponent(StatusTabComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,60 +0,0 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-status-tab',
templateUrl: './status-tab.component.html',
styleUrl: './status-tab.component.css'
})
export class StatusTabComponent {
@Input() loading: boolean = false;
@Input() diskUsage: any = {};
@Input() servicesStatus: any = {};
@Input() installedOgLives: any[] = [];
@Input() subnets: any[] = [];
@Input() diskUsageChartData: any[] = [];
@Input() showLabels: boolean = true;
@Input() isDoughnut: boolean = true;
@Input() colorScheme: any = {
domain: ['#df200d', '#26a700']
};
@Input() view: [number, number] = [400, 220];
@Input() isDhcp: boolean = false;
@Input() processesStatus: any = {};
@Input() ramUsage: any = {};
@Input() cpuUsage: any = {};
@Input() ramUsageChartData: any[] = [];
@Input() isRepository: boolean = false;
getServices(): { name: string, status: string }[] {
if (!this.servicesStatus) {
return [];
}
const services = Object.keys(this.servicesStatus).map(key => ({
name: key,
status: this.servicesStatus[key]
}))
return services;
}
getProcesses(): { name: string, status: string }[] {
if (!this.processesStatus) {
return [];
}
return Object.keys(this.processesStatus).map(key => ({
name: key,
status: this.processesStatus[key]
}));
}
formatBytes(bytes: number): string {
if (bytes >= 1e9) {
return (bytes / 1e9).toFixed(2) + ' GB';
} else if (bytes >= 1e6) {
return (bytes / 1e6).toFixed(2) + ' MB';
} else if (bytes >= 1e3) {
return (bytes / 1e3).toFixed(2) + ' KB';
} else {
return bytes + ' B';
}
}
}

View File

@ -5,15 +5,6 @@
padding: 10px; padding: 10px;
} }
.client-container {
flex-grow: 1;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0rem 1rem 0rem 0.5rem;
}
.client-icon { .client-icon {
flex-shrink: 0; flex-shrink: 0;
margin-right: 20px; margin-right: 20px;
@ -42,19 +33,23 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
/* Distribuye el espacio entre los gráficos */
gap: 20px; gap: 20px;
/* Añade espacio entre los gráficos */
} }
.disk-usage { .disk-usage {
text-align: center; text-align: center;
flex: 1; flex: 1;
min-width: 200px; min-width: 200px;
/* Ajusta este valor según el tamaño mínimo deseado para cada gráfico */
} }
.circular-chart { .circular-chart {
max-width: 150px; max-width: 150px;
max-height: 150px; max-height: 150px;
margin: 0 auto; margin: 0 auto;
/* Centra el gráfico dentro del contenedor */
} }
.chart { .chart {
@ -80,11 +75,6 @@
.client-info { .client-info {
margin: 20px 0; margin: 20px 0;
border-radius: 12px;
background-color: #f5f7fa;
padding: 20px;
border: 2px solid #d1d9e6;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
} }
.info-section { .info-section {
@ -149,7 +139,9 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
/* Distribuye el espacio entre los gráficos */
gap: 20px; gap: 20px;
/* Añade espacio entre los gráficos */
} }
.buttons-row { .buttons-row {
@ -234,22 +226,29 @@
animation: progress 1s ease-out forwards; animation: progress 1s ease-out forwards;
} }
/* Define colores distintos para cada partición */
.partition-0 { .partition-0 {
stroke: #00bfa5; stroke: #00bfa5;
} }
/* Ejemplo: verde */
.partition-1 { .partition-1 {
stroke: #ff6f61; stroke: #ff6f61;
} }
/* Ejemplo: rojo */
.partition-2 { .partition-2 {
stroke: #ffb400; stroke: #ffb400;
} }
/* Ejemplo: amarillo */
.partition-3 { .partition-3 {
stroke: #3498db; stroke: #3498db;
} }
/* Ejemplo: azul */
/* Texto en el centro del gráfico */
.percentage { .percentage {
fill: #333; fill: #333;
font-size: 0.7rem; font-size: 0.7rem;
@ -259,72 +258,27 @@
.disk-container { .disk-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between;
align-items: flex-start;
gap: 20px; gap: 20px;
background-color: #f5f7fa;
border: 2px solid #d1d9e6;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
flex-wrap: wrap;
justify-content: center;
align-items: stretch;
margin-bottom: 20px;
} }
.table-container { .table-container {
flex: 3; flex: 3;
display: flex; overflow-x: auto;
justify-content: center;
align-items: center;
}
table.mat-elevation-z8 {
width: 100%;
max-width: 800px;
background-color: white;
border-radius: 8px;
overflow: hidden;
}
.mat-header-cell {
background-color: #d1d9e6 !important;
color: #333;
font-weight: bold;
text-align: center;
}
.mat-cell {
text-align: center;
}
.mat-chip {
font-weight: bold;
color: white;
} }
.charts-container { .charts-container {
flex: 2; flex: 1;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; gap: 10px;
} }
.disk-usage { .disk-usage {
background-color: white;
padding: 15px;
border-radius: 8px;
justify-self: center;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
text-align: center; text-align: center;
} }
.chart {
display: flex;
justify-content: center;
}
.back-button { .back-button {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,17 +1,14 @@
<app-loading [isLoading]="loading"></app-loading>
<div class="client-container">
<div class="header-container"> <div class="header-container">
<h2 class="title">{{ 'clientDetailsTitle' | translate }} {{ clientData.name }}</h2> <h2 class="title">{{ 'clientDetailsTitle' | translate }}</h2>
<div class="client-button-row"> <div class="client-button-row">
<button class="back-button" (click)="navigateToGroups()"> <button class="back-button" (click)="navigateToGroups()">
<mat-icon>arrow_back</mat-icon> <mat-icon>arrow_back</mat-icon>
{{ 'back' | translate }} {{ 'Back' | translate }}
</button> </button>
</div> </div>
</div> </div>
<mat-divider></mat-divider> <app-loading [isLoading]="loading"></app-loading>
<div *ngIf="!loading" class="client-info"> <div *ngIf="!loading" class="client-info">
<div class="info-section"> <div class="info-section">
@ -34,6 +31,7 @@
</div> </div>
<div class="disk-container"> <div class="disk-container">
<!-- Tabla de particiones -->
<div class="table-container"> <div class="table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8"> <table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef"> <ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
@ -54,10 +52,11 @@
</table> </table>
</div> </div>
<!-- Gráfico circular -->
<div class="charts-container"> <div class="charts-container">
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0"> <ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
<div *ngFor="let disk of chartDisk" class="disk-usage"> <div *ngFor="let disk of chartDisk" class="disk-usage">
<ngx-charts-pie-chart class="chart" [view]="view" [results]="disk.chartData" [doughnut]="true"> <ngx-charts-pie-chart [view]="view" [results]="disk.chartData" [doughnut]="true">
</ngx-charts-pie-chart> </ngx-charts-pie-chart>
<h3>Disco {{ disk.diskNumber }}</h3> <h3>Disco {{ disk.diskNumber }}</h3>
@ -67,5 +66,3 @@
</ng-container> </ng-container>
</div> </div>
</div> </div>
</div>

View File

@ -2,11 +2,11 @@ import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import {DatePipe} from "@angular/common"; import {DatePipe} from "@angular/common";
import {MatTableDataSource} from "@angular/material/table"; import {MatTableDataSource} from "@angular/material/table";
import {PartitionAssistantComponent} from "./partition-assistant/partition-assistant.component";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {Router} from "@angular/router"; import {Router} from "@angular/router";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import { ManageClientComponent } from "../../shared/clients/manage-client/manage-client.component"; import { ManageClientComponent } from "../../shared/clients/manage-client/manage-client.component";
import { ConfigService } from '@services/config.service';
interface ClientInfo { interface ClientInfo {
property: string; property: string;
@ -19,7 +19,7 @@ interface ClientInfo {
styleUrl: './client-main-view.component.css' styleUrl: './client-main-view.component.css'
}) })
export class ClientMainViewComponent implements OnInit { export class ClientMainViewComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
@ViewChild('assistantContainer') assistantContainer!: ElementRef; @ViewChild('assistantContainer') assistantContainer!: ElementRef;
clientUuid: string; clientUuid: string;
clientData: any = {}; clientData: any = {};
@ -48,7 +48,7 @@ export class ClientMainViewComponent implements OnInit {
{name: 'Particionar y Formatear', slug: 'partition'}, {name: 'Particionar y Formatear', slug: 'partition'},
{name: 'Inventario Software', slug: 'software-inventory'}, {name: 'Inventario Software', slug: 'software-inventory'},
{name: 'Inventario Hardware', slug: 'hardware-inventory'}, {name: 'Inventario Hardware', slug: 'hardware-inventory'},
{ name: 'Ejecutar comando', slug: 'run-script' }, {name: 'Ejecutar script', slug: 'run-script'},
]; ];
datePipe: DatePipe = new DatePipe('es-ES'); datePipe: DatePipe = new DatePipe('es-ES');
@ -91,11 +91,9 @@ export class ClientMainViewComponent implements OnInit {
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private dialog: MatDialog, private dialog: MatDialog,
private configService: ConfigService,
private router: Router, private router: Router,
private toastService: ToastrService private toastService: ToastrService
) { ) {
this.baseUrl = this.configService.apiUrl;
const url = window.location.href; const url = window.location.href;
const segments = url.split('/'); const segments = url.split('/');
this.clientUuid = segments[segments.length - 1]; this.clientUuid = segments[segments.length - 1];
@ -207,9 +205,8 @@ export class ClientMainViewComponent implements OnInit {
loadPartitions(): void { loadPartitions(): void {
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({ this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({
next: data => { next: data => {
const filteredPartitions = data['hydra:member'].filter((partition: any) => partition.partitionNumber !== 0); this.dataSource = data['hydra:member'];
this.dataSource = filteredPartitions; this.partitions = data['hydra:member'];
this.partitions = filteredPartitions;
this.calculateDiskUsage(); this.calculateDiskUsage();
}, },
error: error => { error: error => {
@ -218,7 +215,6 @@ export class ClientMainViewComponent implements OnInit {
}); });
} }
loadCommands(): void { loadCommands(): void {
this.http.get<any>(`${this.baseUrl}/commands?`).subscribe({ this.http.get<any>(`${this.baseUrl}/commands?`).subscribe({
next: data => { next: data => {

View File

@ -24,7 +24,6 @@
table { table {
width: 100%; width: 100%;
margin-top: 50px; margin-top: 50px;
background-color: #eaeff6;
} }
.search-container { .search-container {
@ -37,26 +36,18 @@ table {
} }
.select-container { .select-container {
gap: 16px; margin-top: 20px;
align-items: center;
width: 100%; width: 100%;
box-sizing: border-box; padding: 0 5px;
padding: 20px;
}
.selector {
display: flex;
gap: 16px;
width: 100%;
margin-top: 30px;
box-sizing: border-box; box-sizing: border-box;
} }
.half-width { .full-width {
flex: 1; width: 100%;
max-width: 50%; margin-bottom: 16px;
} }
.search-string { .search-string {
flex: 2; flex: 2;
padding: 5px; padding: 5px;
@ -71,8 +62,7 @@ table {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 10px 10px; padding: 10px;
border-bottom: 1px solid #ddd;
} }
.mat-elevation-z8 { .mat-elevation-z8 {
@ -84,22 +74,3 @@ table {
justify-content: end; justify-content: end;
margin-bottom: 30px; margin-bottom: 30px;
} }
.header-container-title {
flex-grow: 1;
text-align: left;
padding-left: 1em;
}
.button-row {
display: flex;
padding-right: 1em;
}
.partition-table-container {
background-color: #eaeff6;
padding: 20px;
border-radius: 12px;
margin-top: 20px;
}

View File

@ -6,34 +6,19 @@
Crear imagen desde {{ clientName }} Crear imagen desde {{ clientName }}
</h2> </h2>
</div> </div>
<div class="button-row"> <div class="subnets-button-row">
<button class="action-button" [disabled]="!selectedPartition" (click)="save()">Ejecutar</button> <button class="action-button" (click)="save()">Ejecutar</button>
</div> </div>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div class="select-container"> <div class="select-container">
<div class="selector"> <mat-form-field appearance="fill" class="full-width">
<mat-form-field appearance="fill" class="half-width">
<mat-label>Nombre canónico</mat-label> <mat-label>Nombre canónico</mat-label>
<input matInput [disabled]="selectedImage" [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required> <input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
</mat-form-field>
<mat-form-field appearance="fill" class="half-width">
<mat-label>Seleccione imagen</mat-label>
<mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
<mat-option *ngFor="let image of images" [value]="image">{{ image?.name }}</mat-option>
</mat-select>
<button *ngIf="selectedImage" mat-icon-button matSuffix aria-label="Clear client search"
(click)="selectedImage = null; resetCanonicalName()">
<mat-icon>close</mat-icon>
</button>
<mat-hint>Seleccione la imagen para sobreescribir si se requiere. </mat-hint>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="partition-table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8"> <table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select"> <ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th> <th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th>
@ -58,6 +43,3 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
</div>
</div>

View File

@ -1,18 +1,16 @@
import {Component, EventEmitter, OnInit, Output} from '@angular/core'; import {Component, EventEmitter, Output} from '@angular/core';
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import {MatTableDataSource} from "@angular/material/table"; import {MatTableDataSource} from "@angular/material/table";
import {SelectionModel} from "@angular/cdk/collections"; import {SelectionModel} from "@angular/cdk/collections";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-create-image', selector: 'app-create-image',
templateUrl: './create-image.component.html', templateUrl: './create-image.component.html',
styleUrl: './create-image.component.css' styleUrl: './create-image.component.css'
}) })
export class CreateClientImageComponent implements OnInit{ export class CreateClientImageComponent {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
@Output() dataChange = new EventEmitter<any>(); @Output() dataChange = new EventEmitter<any>();
errorMessage = ''; errorMessage = '';
@ -24,7 +22,6 @@ export class CreateClientImageComponent implements OnInit{
name: string = ''; name: string = '';
client: any = null; client: any = null;
loading: boolean = false; loading: boolean = false;
selectedImage: any = null;
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
columns = [ columns = [
{ {
@ -60,12 +57,10 @@ export class CreateClientImageComponent implements OnInit{
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private toastService: ToastrService, private toastService: ToastrService,
private configService: ConfigService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
) {
this.baseUrl = this.configService.apiUrl; ) {}
}
ngOnInit() { ngOnInit() {
this.clientId = this.route.snapshot.paramMap.get('id'); this.clientId = this.route.snapshot.paramMap.get('id');
@ -104,29 +99,14 @@ export class CreateClientImageComponent implements OnInit{
); );
} }
resetCanonicalName() {
this.name = this.selectedImage ? this.selectedImage.name : '';
}
save(): void { save(): void {
this.loading = true; this.loading = true;
if (!this.selectedPartition) {
this.toastService.error('Debes seleccionar una partición');
this.loading = false;
return;
}
if (this.selectedImage) {
this.toastService.warning('Aviso: Está seleccionando una imagen previamente creada. Se procede a crear un backup de la misma. ');
}
const payload = { const payload = {
client: `/clients/${this.clientId}`, client: `/clients/${this.clientId}`,
name: this.name, name: this.name,
partition: this.selectedPartition['@id'], partition: this.selectedPartition['@id'],
source: 'assistant', source: 'assistant'
selectedImage: this.selectedImage?.['@id']
}; };

View File

@ -6,7 +6,6 @@
table { table {
width: 100%; width: 100%;
margin-top: 50px; margin-top: 50px;
background-color: #eaeff6;
} }
.search-container { .search-container {
@ -39,8 +38,10 @@ table {
.select-container { .select-container {
margin-top: 20px; margin-top: 20px;
align-items: center; align-items: center;
padding: 20px; width: 100%;
padding: 0 5px;
box-sizing: border-box; box-sizing: border-box;
padding-left: 1em;
} }
.input-group { .input-group {
@ -50,10 +51,6 @@ table {
margin-top: 20px; margin-top: 20px;
} }
mat-option .unit-name {
display: block;
}
.input-field { .input-field {
flex: 1 1 calc(33.33% - 16px); flex: 1 1 calc(33.33% - 16px);
min-width: 250px; min-width: 250px;
@ -78,8 +75,6 @@ mat-option .unit-name {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 10px 10px;
border-bottom: 1px solid #ddd;
} }
.mat-elevation-z8 { .mat-elevation-z8 {
@ -110,27 +105,6 @@ mat-option .unit-name {
position: relative; position: relative;
padding: 8px; padding: 8px;
text-align: center; text-align: center;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
&:hover {
background-color: #f0f0f0;
transform: scale(1.02);
}
}
::ng-deep .custom-tooltip {
white-space: pre-line !important;
max-width: 200px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px;
border-radius: 4px;
}
.selected-client {
background-color: #a0c2e5 !important; /* Azul */
color: white !important;
} }
.client-details { .client-details {
@ -138,15 +112,11 @@ mat-option .unit-name {
} }
.client-name { .client-name {
font-size: 0.9em; display: block;
font-size: 1.2em;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 5px; margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
display: inline-block;
} }
.client-ip { .client-ip {
@ -160,39 +130,3 @@ mat-option .unit-name {
text-align: left; text-align: left;
padding-left: 1em; padding-left: 1em;
} }
.button-row {
display: flex;
padding-right: 1em;
}
.partition-table-container {
background-color: #eaeff6;
padding: 20px;
border-radius: 12px;
margin-top: 20px;
}
.disabled-client {
pointer-events: none;
opacity: 0.5;
}
.error-message {
background-color: #de2323;
padding: 20px;
border-radius: 12px;
margin-top: 20px;
color: white;
font-weight: bold;
}
.action-button {
margin-top: 10px;
margin-bottom: 10px;
}
.mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}

View File

@ -6,96 +6,71 @@
{{ 'deployImage' | translate }} {{ 'deployImage' | translate }}
</h2> </h2>
</div> </div>
<div class="button-row"> <div class="subnets-button-row">
<button class="action-button" [disabled]="!allSelected || !selectedModelClient || !selectedImage || !selectedMethod || !selectedPartition" (click)="save()">Ejecutar</button> <button class="action-button" (click)="save()">Ejecutar</button>
</div> </div>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div class="select-container"> <div class="select-container">
<mat-expansion-panel> <mat-expansion-panel hideToggle>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title> <mat-panel-title> Clientes </mat-panel-title>
<mat-panel-description> <mat-panel-description> Listado de clientes donde se desplegará la imagen </mat-panel-description>
Listado de clientes donde se desplegará la imagen
<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div class="button-row">
<button class="action-button" (click)="toggleSelectAll()">
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
</button>
</div>
<div class="clients-grid" > <div class="clients-grid" >
<div *ngFor="let client of clientData" class="client-item"> <div *ngFor="let client of clientData" class="client-item">
<div class="client-card" <div class="client-card">
(click)="client.status === 'og-live' && toggleClientSelection(client)"
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}"
[matTooltip]="getPartitionsTooltip(client)"
matTooltipPosition="above"
matTooltipClass="custom-tooltip">
<img <img
[src]="'assets/images/computer_' + client.status + '.svg'" [src]="'assets/images/ordenador_' + client.status + '.png'"
alt="Client Icon" alt="Client Icon"
class="client-image" /> class="client-image" />
<div class="client-details"> <div class="client-details">
<span class="client-name">{{ client.name | slice:0:20 }}</span> <span class="client-name">{{ client.name }}</span>
<span class="client-ip">{{ client.ip }}</span> <span class="client-ip">{{ client.ip }}</span>
<span class="client-ip">{{ client.mac }}</span> <span class="client-ip">{{ client.mac }}</span>
</div> </div>
<mat-divider></mat-divider>
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
<mat-radio-button [value]="client"
color="primary"
[disabled]="!client.selected"
(click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
</mat-radio-group>
</div> </div>
</div> </div>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
<mat-divider style="margin-top: 20px;"></mat-divider> <mat-divider style="margin-top: 20px;"></mat-divider>
<div class="select-container"> <div class="select-container">
<div class="option-container">
<mat-radio-group [(ngModel)]="selectedOption" name="selectedOption" aria-label="Selecciona una opcion">
<mat-radio-button value="update-cache">Actualizar cache</mat-radio-button>
<mat-radio-button value="deploy-image">Desplegar imagen</mat-radio-button>
</mat-radio-group>
</div>
<div class="deploy-container"> <div class="deploy-container">
<mat-form-field appearance="fill" class="full-width"> <mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione imagen</mat-label> <mat-label>Seleccione imagen</mat-label>
<mat-select [(ngModel)]="selectedImage" name="selectedImage"> <mat-select [(ngModel)]="selectedImage" name="selectedImage">
<mat-option *ngFor="let image of images" [value]="image"> <mat-option *ngFor="let image of images" [value]="image">{{ image.image?.name }}</mat-option>
<div class="unit-name"> {{ image.name }}</div>
<div style="font-size: smaller; color: gray;">{{ image.description }}</div>
</mat-option>
</mat-select> </mat-select>
<mat-hint *ngIf="clientData">Imágenes alojadas en {{ clientData[0].repository?.name }}</mat-hint>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="fill" class="full-width"> <mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione método de deploy</mat-label> <mat-label>Seleccione método de deploy</mat-label>
<mat-select [(ngModel)]="selectedMethod" name="selectedMethod" (selectionChange)="validateImageSize()"> <mat-select [(ngModel)]="selectedMethod" name="selectedMethod">
<mat-option *ngFor="let method of allMethods" [value]="method.value">{{ method.name }}</mat-option> <mat-option *ngFor="let method of deployMethods" [value]="method">{{ method }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="errorMessage" class="error-message">
{{ errorMessage }}
</div> </div>
<div class="partition-table-container"> <table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table mat-table [dataSource]="filteredPartitions" class="mat-elevation-z8">
<ng-container matColumnDef="select"> <ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef style="text-align: start">Seleccionar partición</th> <th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th>
<td mat-cell *matCellDef="let row"> <td mat-cell *matCellDef="let row">
<mat-radio-group [(ngModel)]="selectedPartition" name="selectedPartition" (change)="validateImageSize()"> <mat-radio-group [(ngModel)]="selectedPartition" name="selectedPartition">
<mat-radio-button [value]="row"> <mat-radio-button [value]="row">
</mat-radio-button> </mat-radio-button>
</mat-radio-group> </mat-radio-group>
@ -112,13 +87,12 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
</div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div class="options-container"> <div class="options-container">
<h3 *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')" class="input-group">Opciones multicast</h3> <h3 *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">Opciones multicast</h3>
<h3 *ngIf="isMethod('p2p')" class="input-group">Opciones torrent</h3> <h3 *ngIf="isMethod('p2p')" class="input-group">Opciones torrent</h3>
<div *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')" class="input-group"> <div *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">
<mat-form-field appearance="fill" class="input-field"> <mat-form-field appearance="fill" class="input-field">
<mat-label>Puerto</mat-label> <mat-label>Puerto</mat-label>
<input matInput [(ngModel)]="mcastPort" name="mcastPort" type="number"> <input matInput [(ngModel)]="mcastPort" name="mcastPort" type="number">
@ -171,6 +145,3 @@
</div> </div>
</div> </div>
</div>

View File

@ -1,4 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeployImageComponent } from './deploy-image.component'; import { DeployImageComponent } from './deploy-image.component';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing'; import { provideHttpClientTesting } from '@angular/common/http/testing';
@ -10,27 +11,20 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatDividerModule } from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio'; // Importar MatRadioModule
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ToastrModule, ToastrService } from 'ngx-toastr'; import { ToastrModule, ToastrService } from 'ngx-toastr';
import { ActivatedRoute, provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import {MatExpansionModule} from "@angular/material/expansion"; import {MatExpansionModule} from "@angular/material/expansion";
import {LoadingComponent} from "../../../../../shared/loading/loading.component"; import {LoadingComponent} from "../../../../../shared/loading/loading.component";
import { ConfigService } from '@services/config.service';
describe('DeployImageComponent', () => { describe('DeployImageComponent', () => {
let component: DeployImageComponent; let component: DeployImageComponent;
let fixture: ComponentFixture<DeployImageComponent>; let fixture: ComponentFixture<DeployImageComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [DeployImageComponent, LoadingComponent], declarations: [DeployImageComponent, LoadingComponent],
imports: [ imports: [
@ -63,26 +57,13 @@ describe('DeployImageComponent', () => {
{ {
provide: MAT_DIALOG_DATA, provide: MAT_DIALOG_DATA,
useValue: {} useValue: {}
},
{
provide: ActivatedRoute,
useValue: {
queryParams: {
subscribe: (fn: (value: any) => void) => fn({ clientData: JSON.stringify([{ '@id': '123', uuid: 'client-uuid', status: 'og-live', partitions: [] }]) })
} }
} ]
},
{ provide: ConfigService, useValue: mockConfigService }
],
schemas: [NO_ERRORS_SCHEMA]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(DeployImageComponent); fixture = TestBed.createComponent(DeployImageComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.clientData = [{ '@id': '123', uuid: 'client-uuid', status: 'og-live', partitions: [] }];
component.selectedModelClient = component.clientData[0];
component.loadPartitions(component.selectedModelClient);
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -1,10 +1,9 @@
import { Component, EventEmitter, Output } from '@angular/core'; import {Component, EventEmitter, Input, Output} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table"; import {MatTableDataSource} from "@angular/material/table";
import {SelectionModel} from "@angular/cdk/collections"; import {SelectionModel} from "@angular/cdk/collections";
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-deploy-image', selector: 'app-deploy-image',
@ -12,14 +11,16 @@ import { ConfigService } from '@services/config.service';
styleUrl: './deploy-image.component.css' styleUrl: './deploy-image.component.css'
}) })
export class DeployImageComponent { export class DeployImageComponent {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
@Output() dataChange = new EventEmitter<any>(); @Output() dataChange = new EventEmitter<any>();
errorMessage = ''; errorMessage = '';
clientId: string | null = null; clientId: string | null = null;
partitions: any[] = []; partitions: any[] = [];
images: any[] = []; images: any[] = [];
clientName: string = '';
selectedImage: any = null; selectedImage: any = null;
selectedOption: string | null = 'deploy-image';
selectedMethod: string | null = null; selectedMethod: string | null = null;
selectedPartition: any = null; selectedPartition: any = null;
mcastIp: string = ''; mcastIp: string = '';
@ -30,10 +31,10 @@ export class DeployImageComponent {
mcastMaxTime: Number = 0; mcastMaxTime: Number = 0;
p2pMode: string = ''; p2pMode: string = '';
p2pTime: Number = 0; p2pTime: Number = 0;
name: string = '';
client: any = null; client: any = null;
clientData: any = []; clientData: any = [];
loading: boolean = false; loading: boolean = false;
allSelected = true;
protected p2pModeOptions = [ protected p2pModeOptions = [
{ name: 'Leecher', value: 'leecher' }, { name: 'Leecher', value: 'leecher' },
@ -45,17 +46,19 @@ export class DeployImageComponent {
{ name: 'Full duplex', value: "full"}, { name: 'Full duplex', value: "full"},
]; ];
selectedClients: any[] = [];
selectedModelClient: any = null;
filteredPartitions: any[] = [];
selectedRepository: any = null;
allMethods = [ allMethods = [
{ name: 'Multicast', value: 'udpcast' }, 'uftp',
{ name: 'Unicast', value: 'unicast' }, 'udpcast',
{ name: 'Multicast (direct)', value: 'udpcast-direct' }, 'unicast',
{ name: 'Unicast (direct)', value: 'unicast-direct' }, 'unicast-direct',
{ name: 'Torrent', value: 'p2p' }, 'p2p'
];
updateCacheMethods = [
'uftp',
'udpcast',
'unicast',
'p2p'
]; ];
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
@ -93,111 +96,60 @@ export class DeployImageComponent {
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private toastService: ToastrService, private toastService: ToastrService,
private configService: ConfigService, private route: ActivatedRoute,
private router: Router, private router: Router,
private route: ActivatedRoute
) { ) {
this.baseUrl = this.configService.apiUrl; const navigation = this.router.getCurrentNavigation();
this.route.queryParams.subscribe(params => { this.clientData = navigation?.extras?.state?.['clientData'];
if (params['clientData']) { this.clientId = this.clientData?.[0]['@id'];
this.clientData = JSON.parse(params['clientData']); this.loadImages();
this.loadPartitions()
} }
});
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
this.clientData.forEach((client: { selected: boolean; status: string}) => {
if (client.status === 'og-live') {
client.selected = true;
}
});
this.selectedClients = this.clientData.filter(
(client: { status: string }) => client.status === 'og-live'
);
this.selectedModelClient = this.clientData.find( get deployMethods() {
(client: { status: string }) => client.status === 'og-live' return this.selectedOption === 'update-cache' ? this.updateCacheMethods : this.allMethods;
) || null;
if (this.selectedModelClient) {
this.loadPartitions(this.selectedModelClient);
}
} }
isMethod(method: string): boolean { isMethod(method: string): boolean {
return this.selectedMethod === method; return this.selectedMethod === method;
} }
toggleClientSelection(client: any) { loadPartitions() {
client.selected = !client.selected; const url = `${this.baseUrl}${this.clientId}`;
this.updateSelectedClients(); this.http.get(url).subscribe(
} (response: any) => {
if (response.partitions) {
updateSelectedClients() { this.client = response;
this.selectedClients = this.clientData.filter( this.clientName = response.name;
(client: { selected: boolean; state: string }) => client.selected && client.state === "og-live" this.dataSource.data = response.partitions.filter((partition: any) => {
);
if (!this.selectedClients.includes(this.selectedModelClient)) {
this.selectedModelClient = null;
this.filteredPartitions = [];
}
}
getPartitionsTooltip(client: any): string {
if (!client.partitions || client.partitions.length === 0) {
return 'No hay particiones disponibles';
}
return client.partitions
.map((p: { partitionNumber: any; size: any; filesystem: any }) => `#${p.partitionNumber} ${p.filesystem} - ${p.size / 1024 }GB`)
.join('\n');
}
loadPartitions(client: any) {
if (client.selected) {
this.http.get(`${this.baseUrl}${client.uuid}`).subscribe(
(fullClientData: any) => {
this.filteredPartitions = fullClientData.partitions;
this.selectedRepository = fullClientData.repository ?? null;
if (fullClientData.partitions) {
this.filteredPartitions = fullClientData.partitions.filter((partition: any) => {
return partition.partitionNumber !== 0; return partition.partitionNumber !== 0;
}); });
this.p2pMode = fullClientData.organizationalUnit?.networkSettings?.p2pMode; this.p2pMode = response.organizationalUnit?.networkSettings?.p2pMode;
this.p2pTime = fullClientData.organizationalUnit?.networkSettings?.p2pTime; this.p2pTime = response.organizationalUnit?.networkSettings?.p2pTime;
this.mcastSpeed = fullClientData.organizationalUnit?.networkSettings?.mcastSpeed; this.mcastSpeed = response.organizationalUnit?.networkSettings?.mcastSpeed;
this.mcastMode = fullClientData.organizationalUnit?.networkSettings?.mcastMode; this.mcastMode = response.organizationalUnit?.networkSettings?.mcastMode;
this.mcastPort = fullClientData.organizationalUnit?.networkSettings?.mcastPort; this.mcastPort = response.organizationalUnit?.networkSettings?.mcastPort;
this.mcastIp = fullClientData.organizationalUnit?.networkSettings?.mcastIp; this.mcastIp = response.organizationalUnit?.networkSettings?.mcastIp;
} }
this.loadImages();
}, },
(error) => { (error) => {
console.error('Error al cargar los datos completos del cliente:', error); console.error('Error al cargar los datos del cliente:', error);
} }
); );
} else {
this.selectedClients = this.selectedClients.filter(c => c.uuid !== client.uuid);
this.filteredPartitions = [];
}
}
toggleSelectAll() {
this.allSelected = !this.allSelected;
this.clientData.forEach((client: { selected: boolean; status: string }) => {
if (client.status === "og-live") {
client.selected = this.allSelected;
}
});
} }
loadImages() { loadImages() {
const repositoryId = this.selectedRepository?.id; if (!this.clientData || this.clientData.length === 0 || !this.clientData[0]) {
console.error('Error: clientData es nulo, indefinido o vacío.');
return;
}
const repositoryId =
this.clientData[0]?.repository?.id ??
this.clientData[0]?.organizationalUnit?.networkSettings?.repository?.id;
if (!repositoryId) { if (!repositoryId) {
console.error('Error: No se encontró repositoryId en el cliente seleccionado.'); console.error('Error: No se encontró repositoryId en clientData.');
return; return;
} }
@ -213,27 +165,9 @@ export class DeployImageComponent {
); );
} }
validateImageSize() {
if (this.selectedImage && this.selectedPartition) {
if ((this.selectedImage.datasize / 1024) / 1024 > this.selectedPartition.size) {
this.errorMessage = "El tamaño de la imagen seleccionada excede el tamaño de la partición.";
return false;
}
}
this.errorMessage = "";
return true;
}
save(): void { save(): void {
this.loading = true; this.loading = true;
if (!this.selectedClients.length) {
this.toastService.error('Debe seleccionar al menos un cliente');
this.loading = false;
return;
}
if (!this.selectedImage) { if (!this.selectedImage) {
this.toastService.error('Debe seleccionar una imagen'); this.toastService.error('Debe seleccionar una imagen');
this.loading = false; this.loading = false;
@ -255,7 +189,7 @@ export class DeployImageComponent {
this.toastService.info('Preparando petición de despliegue'); this.toastService.info('Preparando petición de despliegue');
const payload = { const payload = {
clients: this.selectedClients.map((client: any) => client.uuid), clients: this.clientData.map((client: any) => client['@id']),
method: this.selectedMethod, method: this.selectedMethod,
// partition: this.selectedPartition['@id'], // partition: this.selectedPartition['@id'],
diskNumber: this.selectedPartition.diskNumber, diskNumber: this.selectedPartition.diskNumber,
@ -278,6 +212,7 @@ export class DeployImageComponent {
this.router.navigate(['/commands-logs']); this.router.navigate(['/commands-logs']);
}, },
error: (error) => { error: (error) => {
console.error('Error:', error);
this.toastService.error(error.error['hydra:description'], 'Se ha detectado un error en el despliegue de imágenes.', { this.toastService.error(error.error['hydra:description'], 'Se ha detectado un error en el despliegue de imágenes.', {
"closeButton": true, "closeButton": true,
"newestOnTop": false, "newestOnTop": false,
@ -289,6 +224,7 @@ export class DeployImageComponent {
}); });
this.loading = false; this.loading = false;
} }
}); }
);
} }
} }

View File

@ -1,8 +1,8 @@
.partition-assistant { .partition-assistant {
padding: 40px; font-family: 'Roboto', sans-serif;
margin: 20px; background-color: #f9f9f9;
background-color: #eaeff6; padding: 20px;
border-radius: 12px; margin: 20px auto;
} }
.header-container { .header-container {
@ -19,14 +19,40 @@
color: #555; color: #555;
} }
.partition-bar {
display: flex;
margin: 20px 0;
height: 40px;
border-radius: 8px;
overflow: hidden;
background-color: #e0e0e0;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.partition-segment {
text-align: center;
color: white;
line-height: 40px;
font-weight: 500;
font-size: 0.9rem;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
border-right: 2px solid white; /* Borde de separación */
}
.partition-segment:last-child {
border-right: none;
}
.partition-table { .partition-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
background-color: #fff;
overflow: hidden; overflow: hidden;
margin-bottom: 20px; margin-bottom: 20px;
} }
.partition-table th { .partition-table th {
background-color: #f5f5f5;
color: #333; color: #333;
padding: 12px; padding: 12px;
font-weight: 600; font-weight: 600;
@ -152,20 +178,26 @@ button.remove-btn:hover {
position: relative; position: relative;
} }
.client-card {
background: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
padding: 8px;
text-align: center;
}
.client-details { .client-details {
margin-top: 4px; margin-top: 4px;
} }
.client-name { .client-name {
font-size: 0.9em; display: block;
font-size: 1.2em;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 5px; margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
display: inline-block;
} }
.client-ip { .client-ip {
@ -184,68 +216,9 @@ button.remove-btn:hover {
margin-top: 20px; margin-top: 20px;
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: 0 5px;
box-sizing: border-box; box-sizing: border-box;
padding-left: 1em; padding-left: 1em;
} }
.client-card {
background: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
padding: 8px;
text-align: center;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
&:hover {
background-color: #f0f0f0;
transform: scale(1.02);
}
}
::ng-deep .custom-tooltip {
white-space: pre-line !important;
max-width: 200px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px;
border-radius: 4px;
}
.selected-client {
background-color: #a0c2e5 !important;
color: white !important;
}
.button-row {
display: flex;
padding-right: 1em;
}
.disabled-client {
pointer-events: none;
opacity: 0.5;
}
.row-button {
display: flex;
align-items: center;
gap: 30px;
}
.action-button {
margin-top: 10px;
margin-bottom: 10px;
}
.mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}

View File

@ -7,55 +7,27 @@
</h2> </h2>
</div> </div>
<div class="subnets-button-row"> <div class="subnets-button-row">
<button class="action-button" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected" (click)="save()">Ejecutar</button> <button class="action-button" [disabled]="data.status === 'busy'" (click)="save()">Ejecutar</button>
</div> </div>
</div> </div>
<div class="select-container"> <div class="select-container">
<mat-expansion-panel> <mat-expansion-panel hideToggle>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title> <mat-panel-title> Clientes </mat-panel-title>
<mat-panel-description> <mat-panel-description> Listado de clientes donde se realizará el particionado </mat-panel-description>
Listado de clientes donde se realizará el particionado
<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div class="button-row">
<button class="action-button" (click)="toggleSelectAll()">
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
</button>
</div>
<div class="clients-grid"> <div class="clients-grid">
<div *ngFor="let client of clientData" class="client-item"> <div *ngFor="let client of clientData" class="client-item">
<div class="client-card" <div class="client-card">
(click)="client.status === 'og-live' && toggleClientSelection(client)" <img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon" class="client-image" />
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}"
[matTooltip]="getPartitionsTooltip(client)"
matTooltipPosition="above"
matTooltipClass="custom-tooltip">
<img
[src]="'assets/images/computer_' + client.status + '.svg'"
alt="Client Icon"
class="client-image" />
<div class="client-details"> <div class="client-details">
<span class="client-name">{{ client.name }}</span> <span class="client-name">{{ client.name }}</span>
<span class="client-ip">{{ client.ip }}</span> <span class="client-ip">{{ client.ip }}</span>
<span class="client-ip">{{ client.mac }}</span> <span class="client-ip">{{ client.mac }}</span>
</div> </div>
<mat-divider></mat-divider>
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
<mat-radio-button [value]="client"
color="primary"
[disabled]="!client.selected"
(click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
</mat-radio-group>
</div> </div>
</div> </div>
</div> </div>
@ -77,11 +49,15 @@
</div> </div>
<div class="partition-assistant" *ngIf="selectedDisk"> <div class="partition-assistant" *ngIf="selectedDisk">
<div class="row-button"> <div class="partition-bar">
<div *ngFor="let partition of activePartitions(selectedDisk.diskNumber)"
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
class="partition-segment">
{{ partition.partitionCode }} ({{ (partition.size / 1024).toFixed(2) }} GB)
</div>
</div>
<div class="row">
<button class="action-button" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button> <button class="action-button" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
<mat-chip *ngIf="selectedModelClient.firmwareType">
Tabla de particiones: {{ selectedModelClient.firmwareType }}
</mat-chip>
</div> </div>
<div class="row"> <div class="row">
@ -143,3 +119,5 @@
</div> </div>
</div> </div>
</mat-dialog-content> </mat-dialog-content>
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>

View File

@ -1,10 +1,11 @@
import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core'; import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import { PARTITION_TYPES } from '../../../../../shared/constants/partition-types'; import { PARTITION_TYPES } from '../../../../../shared/constants/partition-types';
import { FILESYSTEM_TYPES } from '../../../../../shared/constants/filesystem-types'; import { FILESYSTEM_TYPES } from '../../../../../shared/constants/filesystem-types';
import { ConfigService } from '@services/config.service'; import {toUnredirectedSourceFile} from "@angular/compiler-cli/src/ngtsc/util/src/typescript";
interface Partition { interface Partition {
uuid?: string; uuid?: string;
@ -26,8 +27,7 @@ interface Partition {
styleUrls: ['./partition-assistant.component.css'] styleUrls: ['./partition-assistant.component.css']
}) })
export class PartitionAssistantComponent { export class PartitionAssistantComponent {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string;
@Output() dataChange = new EventEmitter<any>(); @Output() dataChange = new EventEmitter<any>();
partitionTypes = PARTITION_TYPES; partitionTypes = PARTITION_TYPES;
filesystemTypes = FILESYSTEM_TYPES; filesystemTypes = FILESYSTEM_TYPES;
@ -42,56 +42,31 @@ export class PartitionAssistantComponent {
clientData: any = []; clientData: any = [];
loading: boolean = false; loading: boolean = false;
private apiUrl: string = this.baseUrl + '/partitions';
view: [number, number] = [400, 300]; view: [number, number] = [400, 300];
showLegend = true; showLegend = true;
showLabels = true; showLabels = true;
allSelected = true;
selectedClients: any[] = [];
selectedModelClient: any = null;
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private toastService: ToastrService, private toastService: ToastrService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private configService: ConfigService,
) { ) {
this.baseUrl = this.configService.apiUrl; const navigation = this.router.getCurrentNavigation();
this.apiUrl = this.baseUrl + '/partitions'; this.clientData = navigation?.extras?.state?.['clientData'];
this.route.queryParams.subscribe(params => { this.clientId = this.clientData[0]['@id'];
if (params['clientData']) { this.loadPartitions();
this.clientData = JSON.parse(params['clientData']);
}
});
this.clientId = this.clientData?.[0]['@id'];
this.clientData.forEach((client: { selected: boolean; status: string}) => {
if (client.status === 'og-live') {
client.selected = true;
}
});
this.selectedClients = this.clientData.filter(
(client: { status: string }) => client.status === 'og-live'
);
this.selectedModelClient = this.clientData.find(
(client: { status: string }) => client.status === 'og-live'
) || null;
if (this.selectedModelClient) {
this.loadPartitions(this.selectedModelClient);
}
} }
get selectedDisk():any { get selectedDisk():any {
return this.disks.find(disk => disk.diskNumber === this.selectedDiskNumber) || null; return this.disks.find(disk => disk.diskNumber === this.selectedDiskNumber) || null;
} }
loadPartitions(client: any) { loadPartitions() {
if (!client.selected) { const url = `${this.baseUrl}${this.clientId}`;
this.selectedModelClient = null;
}
const url = `${this.baseUrl}${client.uuid}`;
this.http.get(url).subscribe( this.http.get(url).subscribe(
(response) => { (response) => {
this.data = response; this.data = response;
@ -103,17 +78,7 @@ export class PartitionAssistantComponent {
); );
} }
toggleSelectAll() {
this.allSelected = !this.allSelected;
this.clientData.forEach((client: { selected: boolean; status: string }) => {
if (client.status === "og-live") {
client.selected = this.allSelected;
}
});
}
initializeDisks() { initializeDisks() {
this.disks = [];
const partitionsFromData = this.data.partitions; const partitionsFromData = this.data.partitions;
this.originalPartitions = JSON.parse(JSON.stringify(partitionsFromData)); this.originalPartitions = JSON.parse(JSON.stringify(partitionsFromData));
@ -169,6 +134,15 @@ export class PartitionAssistantComponent {
return bytes return bytes
} }
activePartitions(diskNumber: number) {
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
if (disk) {
return disk.partitions.filter((partition) => !partition.removed);
}
return null;
}
updatePartitionPercentages(partitions: Partition[], totalDiskSize: number) { updatePartitionPercentages(partitions: Partition[], totalDiskSize: number) {
let totalUsedPercentage = 0; let totalUsedPercentage = 0;
@ -201,31 +175,11 @@ export class PartitionAssistantComponent {
} }
} }
toggleClientSelection(client: any) {
client.selected = !client.selected;
this.updateSelectedClients();
}
updateSelectedClients() {
this.selectedClients = this.clientData.filter((client: { selected: any; }) => client.selected);
}
getPartitionsTooltip(client: any): string {
if (!client.partitions || client.partitions.length === 0) {
return 'No hay particiones disponibles';
}
return client.partitions
.map((p: { partitionNumber: any; size: any; filesystem: any }) => `#${p.partitionNumber} ${p.filesystem} - ${p.size / 1024 }GB`)
.join('\n');
}
addPartition(diskNumber: number) { addPartition(diskNumber: number) {
const disk = this.disks.find((d) => d.diskNumber === diskNumber); const disk = this.disks.find((d) => d.diskNumber === diskNumber);
if (disk) { if (disk) {
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize); const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize);
if (remainingGB > 0) { if (remainingGB > 0) {
const removedPartitions = disk.partitions.filter((p) => !p.removed); const removedPartitions = disk.partitions.filter((p) => !p.removed);
const maxPartitionNumber = const maxPartitionNumber =
@ -248,7 +202,7 @@ export class PartitionAssistantComponent {
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
this.updateDiskChart(disk); this.updateDiskChart(disk);
} else { } else {
this.toastService.error('No hay suficiente espacio libre en el disco para crear una nueva partición.'); this.errorMessage = 'No hay suficiente espacio libre en el disco para crear una nueva partición.';
} }
} }
} }
@ -280,20 +234,37 @@ export class PartitionAssistantComponent {
return Math.max(0, totalDiskSize - totalUsedGB); return Math.max(0, totalDiskSize - totalUsedGB);
} }
getModifiedOrNewPartitions() {
const modifiedPartitions: any[] = [];
this.disks.forEach((disk) => {
disk.partitions.forEach((partition) => {
const originalPartition = this.originalPartitions.find(
(p) => p.diskNumber === disk.diskNumber && p.partitionNumber === partition.partitionNumber
);
modifiedPartitions.push({
partition,
diskNumber: disk.diskNumber,
partitionNumber: partition.partitionNumber,
});
});
});
return modifiedPartitions;
}
save() { save() {
if (!this.selectedDisk) { if (!this.selectedDisk) {
this.toastService.error('No se ha seleccionado un disco.'); this.errorMessage = 'Por favor selecciona un disco antes de guardar.';
return; return;
} }
this.loading = true; this.loading = true;
const totalPartitionSize = this.selectedDisk.partitions const totalPartitionSize = this.selectedDisk.partitions.reduce((sum: any, partition: { size: any; }) => sum + partition.size, 0);
.filter((partition: any) => !partition.removed)
.reduce((sum: any, partition: any) => sum + partition.size, 0);
if (totalPartitionSize > this.selectedDisk.totalDiskSize) { if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
this.toastService.error('El tamaño total de las particiones en el disco seleccionado excede el tamaño total del disco.'); this.errorMessage = 'El tamaño total de las particiones en el disco seleccionado excede el tamaño total del disco.';
this.loading = false; this.loading = false;
return; return;
} }
@ -302,7 +273,7 @@ export class PartitionAssistantComponent {
if (modifiedPartitions.length === 0) { if (modifiedPartitions.length === 0) {
this.loading = false; this.loading = false;
this.toastService.info('No hay cambios para guardar en el disco seleccionado.'); this.errorMessage = 'No hay cambios para guardar en el disco seleccionado.';
return; return;
} }
@ -321,7 +292,7 @@ export class PartitionAssistantComponent {
if (newPartitions.length > 0) { if (newPartitions.length > 0) {
const bulkPayload = { const bulkPayload = {
partitions: newPartitions, partitions: newPartitions,
clients: this.selectedClients.map((client: any) => client.uuid), clients: this.clientData.map((client: any) => client['@id']),
}; };
this.http.post(this.apiUrl, bulkPayload).subscribe( this.http.post(this.apiUrl, bulkPayload).subscribe(
@ -331,6 +302,7 @@ export class PartitionAssistantComponent {
this.router.navigate(['/commands-logs']); this.router.navigate(['/commands-logs']);
}, },
(error) => { (error) => {
console.error('Error al crear las particiones:', error);
this.loading = false; this.loading = false;
this.toastService.error('Error al crear las particiones.'); this.toastService.error('Error al crear las particiones.');
} }

View File

@ -1,264 +0,0 @@
.divider {
margin: 20px 0;
}
table {
width: 100%;
margin-top: 50px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.deploy-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
gap: 10px;
}
.script-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 20px;
background-color: #eaeff6;
border-radius: 12px;
margin-top: 20px;
}
.script-content {
flex: 2;
min-width: 60%;
}
.script-params {
flex: 1;
min-width: 35%;
}
@media (max-width: 768px) {
.script-container {
flex-direction: column;
}
.script-content, .script-params {
min-width: 100%;
}
}
.select-container {
margin-top: 20px;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.input-group {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 20px;
}
.input-field {
flex: 1 1 calc(33.33% - 16px);
min-width: 250px;
}
.script-preview {
background-color: #f4f4f4;
border: 1px solid #ccc;
padding: 10px;
border-radius: 5px;
font-family: monospace;
white-space: pre-wrap;
min-height: 50px;
}
.custom-width {
width: 50%;
margin-bottom: 16px;
}
.search-string {
flex: 2;
padding: 5px;
}
.search-boolean {
flex: 1;
padding: 5px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 10px;
border-bottom: 1px solid #ddd;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.client-item {
position: relative;
}
.client-card {
background: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
padding: 8px;
text-align: center;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
&:hover {
background-color: #f0f0f0;
transform: scale(1.02);
}
}
.client-details {
margin-top: 4px;
}
.client-name {
font-size: 0.9em;
font-weight: 600;
color: #333;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
display: inline-block;
}
.client-ip {
display: block;
font-size: 0.9em;
color: #666;
}
.header-container-title {
flex-grow: 1;
text-align: left;
padding-left: 1em;
}
.button-row {
display: flex;
padding-right: 1em;
}
.client-card {
background: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
padding: 8px;
text-align: center;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
&:hover {
background-color: #f0f0f0;
transform: scale(1.02);
}
}
::ng-deep .custom-tooltip {
white-space: pre-line !important;
max-width: 200px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px;
border-radius: 4px;
}
.selected-client {
background-color: #a0c2e5 !important;
color: white !important;
}
.button-row {
display: flex;
padding-right: 1em;
}
.disabled-client {
pointer-events: none;
opacity: 0.5;
}
.action-button {
margin-top: 10px;
margin-bottom: 10px;
}
.mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.new-command-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 15px;
background-color: #eaeff6;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
margin-top: 15px;
}
.new-command-container mat-form-field {
width: 100%;
}
.new-command-container textarea {
font-family: monospace;
resize: vertical;
}
.new-command-container .action-button {
align-self: flex-end;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.full-width {
width: 100%;
}

View File

@ -1,103 +0,0 @@
<app-loading [isLoading]="loading"></app-loading>
<div class="header-container">
<div class="header-container-title">
<h2>
{{ 'runScript' | translate }}
</h2>
</div>
<div class="button-row">
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript)" (click)="save()">Ejecutar</button>
</div>
</div>
<mat-divider></mat-divider>
<div class="select-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-description>
Listado de clientes donde se ejecutara el script seleccionado
<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
<div class="button-row">
<button class="action-button" (click)="toggleSelectAll()">
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
</button>
</div>
<div class="clients-grid">
<div *ngFor="let client of clientData" class="client-item">
<div class="client-card"
(click)="client.status === 'og-live' && toggleClientSelection(client)"
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}"
[matTooltip]="getPartitionsTooltip(client)"
matTooltipPosition="above"
matTooltipClass="custom-tooltip">
<img
[src]="'assets/images/computer_' + client.status + '.svg'"
alt="Client Icon"
class="client-image" />
<div class="client-details">
<span class="client-name">{{ client.name }}</span>
<span class="client-ip">{{ client.ip }}</span>
<span class="client-ip">{{ client.mac }}</span>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<mat-divider style="margin-top: 20px;"></mat-divider>
<div class="select-container">
<div class="command-toggle">
<mat-radio-group [(ngModel)]="commandType">
<mat-radio-button value="new">Comando nuevo</mat-radio-button>
<mat-radio-button value="existing">Comando existente</mat-radio-button>
</mat-radio-group>
</div>
<div *ngIf="commandType === 'new'" class="new-command-container">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Ingrese el script</mat-label>
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
</mat-form-field>
<button class="action-button" (click)="saveNewScript()">Guardar Comando</button>
</div>
<div *ngIf="commandType === 'existing'" class="select-container">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Seleccione script a ejecutar</mat-label>
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
<div class="script-content">
<h3> Script:</h3>
<div class="script-preview" [innerHTML]="scriptContent"></div>
</div>
<div class="script-params" *ngIf="parameterNames.length > 0">
<h3>Ingrese los valores de los parámetros detectados:</h3>
<div *ngFor="let paramName of parameterNames">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ paramName }}</mat-label>
<input matInput
[ngModel]="parameters[paramName]"
(ngModelChange)="onParamChange(paramName, $event)"
placeholder="Ingrese el valor">
</mat-form-field>
</div>
</div>
</div>
</div>

View File

@ -1,99 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RunScriptAssistantComponent } from './run-script-assistant.component';
import { DeployImageComponent } from "../deploy-image/deploy-image.component";
import { LoadingComponent } from "../../../../../shared/loading/loading.component";
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatButtonModule } from "@angular/material/button";
import { MatTableModule } from "@angular/material/table";
import { MatDividerModule } from "@angular/material/divider";
import { MatRadioModule } from "@angular/material/radio";
import { MatSelectModule } from "@angular/material/select";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ToastrModule, ToastrService } from "ngx-toastr";
import { TranslateModule } from "@ngx-translate/core";
import { provideHttpClient } from "@angular/common/http";
import { provideHttpClientTesting } from "@angular/common/http/testing";
import { provideRouter } from "@angular/router";
import { ConfigService } from "@services/config.service";
import { TranslateLoader } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import {MatIconModule} from "@angular/material/icon";
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http);
}
describe('RunScriptAssistantComponent', () => {
let component: RunScriptAssistantComponent;
let fixture: ComponentFixture<RunScriptAssistantComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [RunScriptAssistantComponent, DeployImageComponent, LoadingComponent],
imports: [
ReactiveFormsModule,
FormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatExpansionModule,
MatButtonModule,
MatTableModule,
MatDividerModule,
MatRadioModule,
MatProgressSpinnerModule,
MatSelectModule,
BrowserAnimationsModule,
MatIconModule,
ToastrModule.forRoot(),
HttpClientTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
],
providers: [
FormBuilder,
ToastrService,
provideHttpClient(),
provideHttpClientTesting(),
provideRouter([]),
{
provide: MatDialogRef,
useValue: {}
},
{
provide: MAT_DIALOG_DATA,
useValue: {}
},
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();
fixture = TestBed.createComponent(RunScriptAssistantComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,177 +0,0 @@
import {Component, EventEmitter, Output} from '@angular/core';
import {SelectionModel} from "@angular/cdk/collections";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {ConfigService} from "@services/config.service";
import {ActivatedRoute, Router} from "@angular/router";
import {SaveScriptComponent} from "./save-script/save-script.component";
import {MatDialog} from "@angular/material/dialog";
@Component({
selector: 'app-run-script-assistant',
templateUrl: './run-script-assistant.component.html',
styleUrl: './run-script-assistant.component.css'
})
export class RunScriptAssistantComponent {
baseUrl: string;
@Output() dataChange = new EventEmitter<any>();
errorMessage = '';
clientId: string | null = null;
name: string = '';
client: any = null;
clientData: any = [];
loading: boolean = false;
scripts: any[] = [];
scriptContent: string = "";
parameters: any = {};
selectedScript: any = null;
selectedClients: any[] = [];
allSelected: boolean = true;
commandType: string = 'existing';
newScript: string = '';
selection = new SelectionModel(true, []);
parameterNames: string[] = Object.keys(this.parameters);
constructor(
private http: HttpClient,
private toastService: ToastrService,
private configService: ConfigService,
private router: Router,
private dialog: MatDialog,
private route: ActivatedRoute
) {
this.baseUrl = this.configService.apiUrl;
this.route.queryParams.subscribe(params => {
if (params['clientData']) {
this.clientData = JSON.parse(params['clientData']);
}
});
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
this.clientData.forEach((client: { selected: boolean; status: string}) => {
if (client.status === 'og-live') {
client.selected = true;
}
});
this.selectedClients = this.clientData.filter(
(client: { status: string }) => client.status === 'og-live'
);
this.loadScripts()
}
loadScripts(): void {
this.loading = true;
this.http.get(`${this.baseUrl}/commands?readOnly=false&enabled=true`).subscribe((data: any) => {
this.scripts = data['hydra:member'];
this.loading = false;
}, (error) => {
this.toastService.error(error.error['hydra:description']);
this.loading = false;
});
}
saveNewScript() {
if (!this.newScript.trim()) {
this.toastService.error('Debe ingresar un script antes de guardar.');
return;
}
const dialogRef = this.dialog.open(SaveScriptComponent, {
width: '400px',
data: this.newScript
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Script guardado correctamente');
}
});
}
toggleClientSelection(client: any) {
client.selected = !client.selected;
this.updateSelectedClients();
}
updateSelectedClients() {
this.selectedClients = this.clientData.filter(
(client: { selected: boolean; status: string }) => client.selected && client.status === "og-live"
);
}
toggleSelectAll() {
this.allSelected = !this.allSelected;
this.clientData.forEach((client: { selected: boolean; status: string }) => {
if (client.status === "og-live") {
client.selected = this.allSelected;
}
});
}
getPartitionsTooltip(client: any): string {
if (!client.partitions || client.partitions.length === 0) {
return 'No hay particiones disponibles';
}
return client.partitions
.map((p: { partitionNumber: any; size: any; filesystem: any }) => `#${p.partitionNumber} ${p.filesystem} - ${p.size / 1024 }GB`)
.join('\n');
}
onScriptChange() {
if (this.selectedScript) {
this.scriptContent = this.selectedScript.script;
const matches = this.scriptContent.match(/@(\w+)/g) || [];
const uniqueParams = Array.from(new Set(matches.map(m => m.slice(1))));
this.parameters = {};
uniqueParams.forEach(param => this.parameters[param] = '');
this.parameterNames = uniqueParams;
this.updateScript();
}
}
onParamChange(name: string, value: string): void {
this.parameters[name] = value;
this.updateScript();
}
updateScript(): void {
let updatedScript = this.selectedScript.script;
for (const [key, value] of Object.entries(this.parameters)) {
const regex = new RegExp(`@${key}\\b`, 'g');
updatedScript = updatedScript.replace(regex, value || `@${key}`);
}
this.scriptContent = updatedScript;
}
trackByIndex(index: number): number {
return index;
}
save(): void {
this.loading = true;
this.http.post(`${this.baseUrl}/commands/run-script`, {
clients: this.selectedClients.map((client: any) => client.uuid),
script: this.commandType === 'existing' ? this.scriptContent : this.newScript,
}).subscribe(
response => {
this.toastService.success('Script ejecutado correctamente');
this.dataChange.emit();
this.router.navigate(['/commands-logs']);
},
error => {
this.toastService.error('Error al ejecutar el script');
}
);
this.loading = false;
}
}

View File

@ -1,40 +0,0 @@
.dialog-content {
display: flex;
flex-direction: column;
padding: 40px;
}
.repository-form {
width: 100%;
display: flex;
flex-direction: column;
}
.full-width{
width: 100%;
margin-top: 16px;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
@media (max-width: 600px) {
.form-field {
width: 100%;
}
.dialog-actions {
flex-direction: column;
align-items: stretch;
}
button {
width: 100%;
margin-left: 0;
margin-bottom: 8px;
}
}

View File

@ -1,12 +0,0 @@
<h2 mat-dialog-title>Guardar Comando</h2>
<mat-dialog-content>
<p>Introduce un nombre para el comando:</p>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Nombre del Comando</mat-label>
<input matInput [(ngModel)]="commandName">
</mat-form-field>
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="save()">Guardar</button>
</div>

View File

@ -1,94 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {RunScriptAssistantComponent} from "../run-script-assistant.component";
import {DeployImageComponent} from "../../deploy-image/deploy-image.component";
import {LoadingComponent} from "../../../../../../shared/loading/loading.component";
import {FormBuilder, FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatCheckboxModule} from "@angular/material/checkbox";
import {MatExpansionModule} from "@angular/material/expansion";
import {MatButtonModule} from "@angular/material/button";
import {MatTableModule} from "@angular/material/table";
import {MatDividerModule} from "@angular/material/divider";
import {MatRadioModule} from "@angular/material/radio";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MatSelectModule} from "@angular/material/select";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {MatIconModule} from "@angular/material/icon";
import {ToastrModule, ToastrService} from "ngx-toastr";
import {HttpClientTestingModule, provideHttpClientTesting} from "@angular/common/http/testing";
import {TranslateLoader, TranslateModule} from "@ngx-translate/core";
import {HttpClient, provideHttpClient} from "@angular/common/http";
import {provideRouter} from "@angular/router";
import {ConfigService} from "@services/config.service";
import {HttpLoaderFactory} from "../run-script-assistant.component.spec";
import {SaveScriptComponent} from "./save-script.component";
describe('SaveScriptComponent', () => {
let component: SaveScriptComponent;
let fixture: ComponentFixture<SaveScriptComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [RunScriptAssistantComponent, DeployImageComponent, LoadingComponent],
imports: [
ReactiveFormsModule,
FormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatExpansionModule,
MatButtonModule,
MatTableModule,
MatDividerModule,
MatRadioModule,
MatProgressSpinnerModule,
MatSelectModule,
BrowserAnimationsModule,
MatIconModule,
ToastrModule.forRoot(),
HttpClientTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
],
providers: [
FormBuilder,
ToastrService,
provideHttpClient(),
provideHttpClientTesting(),
provideRouter([]),
{
provide: MatDialogRef,
useValue: {}
},
{
provide: MAT_DIALOG_DATA,
useValue: {}
},
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();
fixture = TestBed.createComponent(SaveScriptComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,51 +0,0 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {FormBuilder, Validators} from "@angular/forms";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {ConfigService} from "@services/config.service";
import {DataService} from "../../../../../commands/main-commands/data.service";
@Component({
selector: 'app-save-script',
templateUrl: './save-script.component.html',
styleUrl: './save-script.component.css'
})
export class SaveScriptComponent {
commandName: string = '';
baseUrl: string;
constructor(
private http: HttpClient,
public dialogRef: MatDialogRef<SaveScriptComponent>,
private toastService: ToastrService,
private configService: ConfigService,
private dataService: DataService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.baseUrl = this.configService.apiUrl;
}
save() {
const payload = {
name: this.commandName,
script: this.data,
readOnly: false,
enabled: true,
};
this.http.post(`${this.baseUrl}/commands`, payload).subscribe(
(response) => {
this.toastService.success('Comando añadido correctamente');
this.dialogRef.close();
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
}
);
}
close() {
this.dialogRef.close();
}
}

View File

@ -22,96 +22,6 @@
overflow: hidden; overflow: hidden;
} }
.clients-mat-divider {
display: none;
}
@media (max-width: 992px) {
.main-container {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
}
.filters-and-tree-container {
display: flex !important;
flex-direction: row !important;
flex-grow: 1 !important;
flex-shrink: 1 !important;
width: 100% !important;
max-width: none !important;
min-width: 0 !important;
padding: 1rem !important;
box-sizing: border-box !important;
min-height: 250px !important;
overflow-y: auto !important;
}
.filters-panel,
.tree-container {
flex: 1 1 50% !important;
overflow-y: auto !important;
padding: 0.5rem !important;
box-sizing: border-box !important;
margin-bottom: 0 !important;
}
.filters-container {
padding: 0 !important;
}
.tree-mat-divider {
display: none !important;
}
.clients-mat-divider {
display: inline;
}
.clients-view-header {
display: flex;
flex-direction: row !important;
justify-content: space-between !important;
margin-bottom: 0.5rem !important;
margin-top: 0.5rem !important;
align-items: center !important;
padding-right: 1rem !important;
}
.groups-button-row {
display: none !important;
}
.cards-view {
max-height: none !important;
}
.clients-table {
max-height: unset !important;
overflow: unset !important;
display: table !important;
flex-direction: unset !important;
}
.clients-container {
padding: 0em 1em 0em 1em !important;
}
}
@media (max-width: 1400px) {
.type-view-text {
display: none;
}
}
@media (max-width: 450px) {
.clients-title-name {
font-size: 20px !important;
}
}
.header-container-title { .header-container-title {
flex-grow: 1; flex-grow: 1;
text-align: left; text-align: left;
@ -124,6 +34,15 @@
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.clients-container {
flex-grow: 1;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0rem 1rem 0rem 0.5rem;
}
.clients-view-header { .clients-view-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -134,32 +53,6 @@
padding-right: 1rem; padding-right: 1rem;
} }
@media (max-width: 768px) {
.clients-view-header {
display: flex;
flex-direction: column !important;
}
}
.clients-title-name {
font-size: x-large;
display: block;
padding: 1rem 1rem 1rem 13px;
margin-left: 0.6rem;
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.view-type-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
flex-shrink: 0;
}
.clients-view { .clients-view {
flex-grow: 1; flex-grow: 1;
overflow-y: auto; overflow-y: auto;
@ -182,35 +75,16 @@
padding-left: 1rem; padding-left: 1rem;
} }
.list-view {
overflow-x: auto;
}
.clients-table { .clients-table {
max-height: calc(100vh - 330px); max-height: calc(100vh - 330px);
overflow: auto; overflow: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%;
table-layout: auto;
border-collapse: collapse;
} }
.clients-table th, .clients-table table {
.clients-table td { flex-grow: 1;
text-align: left; overflow: auto;
padding: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.clients-table th {
font-weight: 500;
}
.clients-table td {
border-bottom: 1px solid #ddd;
} }
.paginator-container { .paginator-container {
@ -250,6 +124,18 @@
flex: 1; flex: 1;
} }
@media (max-width: 1024px) {
.header-container {
flex-direction: column;
gap: 10px;
}
.groups-button-row {
flex-wrap: wrap;
gap: 10px;
}
}
mat-tree { mat-tree {
background-color: #f9f9f9; background-color: #f9f9f9;
padding: 0px 10px 10px 10px; padding: 0px 10px 10px 10px;
@ -372,43 +258,28 @@ mat-tree mat-tree-node.disabled:hover {
.client-name { .client-name {
display: block; display: block;
font-weight: 500; font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 5px; margin-bottom: 5px;
margin-top: 5px; margin-top: 5px;
padding-left: 1rem;
padding-right: 1rem;
}
.clients-container {
flex: 8;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0rem 1rem 0rem 0.5rem;
}
.filters-and-tree-container {
flex: 2;
display: flex;
flex-direction: column;
flex-shrink: 1;
width: 100%;
box-sizing: border-box;
} }
.filters-container { .filters-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1em; justify-content: start;
flex-grow: 1; padding: 1em 1em 0em 1em;
} }
.filter-form-field { .filter-form-field {
flex: 1; min-width: 21rem;
min-width: 150px; }
max-width: 100%;
box-sizing: border-box; .filters-and-tree-container {
display: flex;
flex-direction: column;
height: 100%;
} }
.filters-panel { .filters-panel {
@ -448,30 +319,11 @@ mat-tree mat-tree-node.disabled:hover {
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
} }
.client-details { .client-image {
flex-grow: 1; width: 35px;
display: flex; height: 35px;
flex-direction: column;
justify-content: space-between;
margin-top: 4px;
}
.type-view-text {
margin-left: 0.5vw;
}
.action-icons {
display: flex;
justify-content: center;
align-items: center;
gap: 1px;
margin-top: 10px; margin-top: 10px;
} margin-bottom: 8px;
.client-status-container {
display: flex;
align-items: center;
gap: 5px;
} }
.client-ip { .client-ip {
@ -486,9 +338,11 @@ mat-tree mat-tree-node.disabled:hover {
gap: 4px; gap: 4px;
} }
.sync-spinner { .action-icons {
margin-left: 1em; display: flex;
margin-right: 1em; justify-content: center;
gap: 1px;
margin-top: 10px;
} }
.mat-elevation-z8 { .mat-elevation-z8 {
@ -506,6 +360,26 @@ mat-tree mat-tree-node.disabled:hover {
position: relative; position: relative;
} }
.client-details {
margin-top: 4px;
}
@media (max-width: 1560px) {
.clients-view-header {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
margin-top: 0;
}
}
.clients-title-name {
font-size: x-large;
display: block;
padding: 1rem 1rem 1rem 13px;
margin-left: 0.6rem;
}
.no-clients-info { .no-clients-info {
display: flex; display: flex;
align-items: center; align-items: center;
@ -514,6 +388,13 @@ mat-tree mat-tree-node.disabled:hover {
margin-left: 1.6rem; margin-left: 1.6rem;
} }
.view-type-container {
display: flex;
justify-content: flex-end;
gap: 2rem;
align-items: center;
}
mat-button-toggle-group { mat-button-toggle-group {
border: none; border: none;
} }

View File

@ -25,27 +25,6 @@
{{ 'legendButton' | translate }} {{ 'legendButton' | translate }}
</button> </button>
</div> </div>
<!-- Menú desplegable para pantallas pequeñas -->
<div *ngIf="isSmallScreen" class="groups-menu">
<button mat-icon-button [matMenuTriggerFor]="smallScreenMenu" matTooltip="Opciones" matTooltipShowDelay="1000">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #smallScreenMenu="matMenu">
<button mat-menu-item (click)="addOU($event)">
{{ 'newOrganizationalUnitButton' | translate }}
</button>
<button mat-menu-item (click)="addClient($event)">
{{ 'newSingleClientButton' | translate }}
</button>
<button mat-menu-item (click)="addMultipleClients($event)">
{{ 'newMultipleClientButton' | translate }}
</button>
<button mat-menu-item (click)="openBottomSheet()">
{{ 'legendButton' | translate }}
</button>
</mat-menu>
</div>
</div> </div>
<div *ngIf="initialLoading; else contentTemplate"> <div *ngIf="initialLoading; else contentTemplate">
@ -81,19 +60,17 @@
</button> </button>
</mat-form-field> </mat-form-field>
<mat-form-field class="form-field search-select" appearance="outline"> <mat-form-field class="form-field search-select" appearance="outline">
<mat-select placeholder="Buscar por estado..." #clientSearchStatusInput <mat-select placeholder="Buscar por estado..." #clientSearchStatusInput (selectionChange)="onClientFilterStatusInput($event.value)">
(selectionChange)="onClientFilterStatusInput($event.value)">
<mat-option *ngFor="let option of status" [value]="option.value"> <mat-option *ngFor="let option of status" [value]="option.value">
{{ option.name }} {{ option.name }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<button *ngIf="clientSearchStatusInput.value" mat-icon-button matSuffix aria-label="Clear tree search" <button *ngIf="clientSearchStatusInput.value" mat-icon-button matSuffix aria-label="Clear tree search" (click)="clearStatusFilter($event, clientSearchStatusInput)">
(click)="clearStatusFilter($event, clientSearchStatusInput)">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
</mat-form-field> </mat-form-field>
<mat-divider class="tree-mat-divider" style="padding-top: 10px;"></mat-divider> <mat-divider style="padding-top: 10px;"></mat-divider>
<!-- Funcionalidad actualmente deshabilitada--> <!-- Funcionalidad actualmente deshabilitada-->
<!-- <mat-form-field appearance="outline"> <!-- <mat-form-field appearance="outline">
@ -119,7 +96,7 @@
<div class="tree-container"> <div class="tree-container">
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl"> <mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}" <mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
*matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick($event, node)"> *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(node)">
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable" <button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable"
[ngClass]="{'disabled-toggle': !node.expandable}"> [ngClass]="{'disabled-toggle': !node.expandable}">
<mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon> <mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon>
@ -135,12 +112,12 @@
}} }}
</mat-icon> </mat-icon>
<span>{{ node.name }}</span> <span>{{ node.name }}</span>
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onMenuClick($event, node)"> <button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onNodeClick(node)">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</mat-tree-node> </mat-tree-node>
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}" <mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
*matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick($event, node)"> *matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick(node)">
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button> <button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button>
<mat-icon style="color: green;"> <mat-icon style="color: green;">
{{ {{
@ -156,7 +133,7 @@
<ng-container *ngIf="node.type === 'client'"> <ng-container *ngIf="node.type === 'client'">
<span> - IP: {{ node.ip }}</span> <span> - IP: {{ node.ip }}</span>
</ng-container> </ng-container>
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onMenuClick($event, node)"> <button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onNodeClick(node)">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</mat-tree-node> </mat-tree-node>
@ -198,9 +175,6 @@
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span> <span>{{ 'delete' | translate }}</span>
</button> </button>
<app-execute-command [clientData]="selectedNode?.clients || []" [buttonType]="'menu-item'"
[buttonText]="'Ejecutar comandos'" [icon]="'terminal'" [disabled]="!((selectedNode?.clients ?? []).length > 0)">
</app-execute-command>
</mat-menu> </mat-menu>
</div> </div>
@ -209,23 +183,25 @@
<!-- CLIENTS --> <!-- CLIENTS -->
<div class="clients-container"> <div class="clients-container">
<mat-divider class="clients-mat-divider"></mat-divider>
<!-- CLIENTS HEADER --> <!-- CLIENTS HEADER -->
<div class="clients-view-header"> <div class="clients-view-header">
<div>
<span [ngStyle]="{ visibility: isLoadingClients ? 'hidden' : 'visible' }" class="clients-title-name"> <span [ngStyle]="{ visibility: isLoadingClients ? 'hidden' : 'visible' }" class="clients-title-name">
{{ 'clients' | translate }} {{ 'clients' | translate }}
<strong>{{ selectedNode?.name }}</strong> <strong>{{ selectedNode?.name }}</strong>
</span> </span>
</div>
<div class="view-type-container"> <div class="view-type-container">
<app-execute-command [clientData]="selection.selected" [buttonType]="'text'" <app-execute-command [clientData]="selection.selected" [buttonType]="'text'"
[buttonText]="'Ejecutar comandos'" [disabled]="selection.selected.length === 0"></app-execute-command> [buttonText]="'Ejecutar comandos'" [disabled]="selection.selected.length === 0"></app-execute-command>
<mat-button-toggle-group name="viewType" aria-label="View Type" [hideSingleSelectionIndicator]="true" <mat-button-toggle-group name="viewType" aria-label="View Type" [hideSingleSelectionIndicator]="true"
(change)="toggleView($event.value)"> (change)="toggleView($event.value)">
<mat-button-toggle value="list" [disabled]="currentView === 'list'"> <mat-button-toggle value="list" [disabled]="currentView === 'list'">
<mat-icon>list</mat-icon> <span class="type-view-text">{{ 'Vista Lista' | translate }}</span> <mat-icon>list</mat-icon> {{ 'Vista Lista' | translate }}
</mat-button-toggle> </mat-button-toggle>
<mat-button-toggle value="card" [disabled]="currentView === 'card'"> <mat-button-toggle value="card" [disabled]="currentView === 'card'">
<mat-icon>grid_view</mat-icon> <span class="type-view-text">{{ 'Vista Tarjeta' | translate }}</span> <mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
</mat-button-toggle> </mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>
</div> </div>
@ -248,17 +224,24 @@
<div *ngFor="let client of arrayClients" class="client-item"> <div *ngFor="let client of arrayClients" class="client-item">
<div class="client-card"> <div class="client-card">
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(client)" <mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(client)"
[checked]="selection.isSelected(client)" [checked]="selection.isSelected(client)" [disabled]="client.status === 'busy'">
[disabled]="client.status === 'busy' || client.status === 'off' || client.status === 'disconnected'">
</mat-checkbox> </mat-checkbox>
<img style="margin-top: 0.5em;" [src]="'assets/images/computer_' + client.status + '.svg'" <img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon"
alt="Client Icon" class="client-image" /> class="client-image" />
<div class="client-details"> <div class="client-details">
<span class="client-name">{{ client.name }}</span> <span class="client-name">{{ client.name }}</span>
<span class="client-ip">{{ client.ip }}</span> <span class="client-ip">{{ client.ip }}</span>
<span class="client-ip">{{ client.mac }}</span> <span class="client-ip">{{ client.mac }}</span>
<div class="action-icons"> <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>
<app-execute-command [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'" <app-execute-command [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'"
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"></app-execute-command> [disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"></app-execute-command>
@ -269,10 +252,6 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
<span class="sync-spinner" *ngIf="syncStatus && syncingClientId === client.uuid">
<mat-spinner diameter="24"></mat-spinner>
</span>
<mat-menu #clientMenu="matMenu"> <mat-menu #clientMenu="matMenu">
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)"> <button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
@ -282,11 +261,6 @@
<mat-icon>visibility</mat-icon> <mat-icon>visibility</mat-icon>
<span>{{ 'viewDetails' | translate }}</span> <span>{{ 'viewDetails' | translate }}</span>
</button> </button>
<button mat-menu-item *ngIf="(!syncStatus || syncingClientId !== client.uuid)"
(click)="getStatus(client, selectedNode)">
<mat-icon>sync</mat-icon>
<span>{{ 'sync' | translate }}</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, client)"> <button mat-menu-item (click)="onDeleteClick($event, client)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span> <span>{{ 'delete' | translate }}</span>
@ -318,8 +292,7 @@
</th> </th>
<td mat-cell *matCellDef="let row"> <td mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(row)" <mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(row)"
[checked]="selection.isSelected(row)" [checked]="selection.isSelected(row)" [disabled]="row.status === 'busy'">
[disabled]="row.status === 'busy' || row.status === 'off' || row.status === 'disconnected'">
</mat-checkbox> </mat-checkbox>
</td> </td>
</ng-container> </ng-container>
@ -327,13 +300,8 @@
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" <td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
matTooltipPosition="left" matTooltipShowDelay="500"> matTooltipPosition="left" matTooltipShowDelay="500">
<div class="client-status-container"> <img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon"
<img [src]="'assets/images/computer_' + client.status + '.svg'" alt="Client Icon"
class="client-image" /> class="client-image" />
<span *ngIf="syncStatus && syncingClientId === client.uuid">
<mat-spinner diameter="24"></mat-spinner>
</span>
</div>
</td> </td>
</ng-container> </ng-container>
@ -341,29 +309,16 @@
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" <td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
matTooltipPosition="left" matTooltipShowDelay="500"> matTooltipPosition="left" matTooltipShowDelay="500">
<p>{{ client.name }}</p> <p style="font-weight: 500;">{{ client.name }}</p>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="ip"> <ng-container matColumnDef="ip">
<th mat-header-cell *matHeaderCellDef mat-sort-header>IP </th> <th mat-header-cell *matHeaderCellDef mat-sort-header>IP </th>
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" <td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
matTooltipPosition="left" matTooltipShowDelay="500"> matTooltipPosition="left" matTooltipShowDelay="500">
<div style="display: flex; flex-direction: column;"> {{ client.ip }}
<span>{{ client.ip }}</span>
<span style="font-size: 0.75rem; color: gray;">{{ client.mac }}</span>
</div>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="firmwareType">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'firmwareType' | translate }} </th>
<td mat-cell *matCellDef="let client">
<mat-chip s>
{{ client.firmwareType ? client.firmwareType : 'N/A' }}
</mat-chip>
</td>
</ng-container>
<ng-container matColumnDef="oglive"> <ng-container matColumnDef="oglive">
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
<td mat-cell *matCellDef="let client"> {{ client.ogLive?.date | date }} </td> <td mat-cell *matCellDef="let client"> {{ client.ogLive?.date | date }} </td>
@ -396,8 +351,7 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
<app-execute-command [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'" <app-execute-command [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'"
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"> [disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"></app-execute-command>
</app-execute-command>
<mat-menu #clientMenu="matMenu"> <mat-menu #clientMenu="matMenu">
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)"> <button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
@ -418,8 +372,7 @@
</mat-menu> </mat-menu>
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row style="background-color: #f3f3f3;" <tr mat-header-row style="background-color: #f3f3f3;" *matHeaderRowDef="displayedColumns; sticky: true"></tr>
*matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
</section> </section>

View File

@ -4,6 +4,7 @@ import { MatInputModule } from '@angular/material/input';
import { HttpClientTestingModule } from '@angular/common/http/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core';
import { MatDividerModule } from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@ -26,18 +27,12 @@ import { MatTreeModule } from '@angular/material/tree';
import { TreeNode } from './model/model'; import { TreeNode } from './model/model';
import { LoadingComponent } from '../../shared/loading/loading.component'; import { LoadingComponent } from '../../shared/loading/loading.component';
import { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component'; import { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component';
import { ConfigService } from '@services/config.service';
describe('GroupsComponent', () => { describe('GroupsComponent', () => {
let component: GroupsComponent; let component: GroupsComponent;
let fixture: ComponentFixture<GroupsComponent>; let fixture: ComponentFixture<GroupsComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent], declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent],
imports: [ imports: [
@ -68,8 +63,7 @@ describe('GroupsComponent', () => {
], ],
providers: [ providers: [
{ provide: MatDialogRef, useValue: {} }, { provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } }, { provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } }
{ provide: ConfigService, useValue: mockConfigService }
] ]
}).compileComponents(); }).compileComponents();
@ -116,13 +110,13 @@ describe('GroupsComponent', () => {
expect(component.expandPathToNode).toHaveBeenCalledWith(node); expect(component.expandPathToNode).toHaveBeenCalledWith(node);
}); });
/* it('should handle node click', () => { it('should handle node click', () => {
const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] }; const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] };
spyOn<any>(component, 'fetchClientsForNode'); spyOn<any>(component, 'fetchClientsForNode');
component.onNodeClick($event, node); component.onNodeClick(node);
expect(component.selectedNode).toBe(node); expect(component.selectedNode).toBe(node);
expect(component['fetchClientsForNode']).toHaveBeenCalledWith(node); expect(component['fetchClientsForNode']).toHaveBeenCalledWith(node);
});*/ });
it('should fetch clients for node', () => { it('should fetch clients for node', () => {
const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] }; const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] };
@ -134,4 +128,4 @@ describe('GroupsComponent', () => {
{ params: jasmine.any(Object) } { params: jasmine.any(Object) }
); );
}); });
}) });

View File

@ -1,4 +1,4 @@
import {Component, OnInit, OnDestroy, ViewChild, QueryList, ViewChildren, ChangeDetectorRef} from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -17,15 +17,12 @@ import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/del
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { PageEvent } from '@angular/material/paginator'; import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { CreateMultipleClientComponent } from "./shared/clients/create-multiple-client/create-multiple-client.component"; import { CreateMultipleClientComponent } from "./shared/clients/create-multiple-client/create-multiple-client.component";
import { SelectionModel } from "@angular/cdk/collections"; import { SelectionModel } from "@angular/cdk/collections";
import { ManageClientComponent } from "./shared/clients/manage-client/manage-client.component"; import { ManageClientComponent } from "./shared/clients/manage-client/manage-client.component";
import { debounceTime } from 'rxjs/operators'; import { debounceTime } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { ConfigService } from '@services/config.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MatMenuTrigger } from '@angular/material/menu';
enum NodeType { enum NodeType {
OrganizationalUnit = 'organizational-unit', OrganizationalUnit = 'organizational-unit',
@ -41,10 +38,8 @@ enum NodeType {
styleUrls: ['./groups.component.css'], styleUrls: ['./groups.component.css'],
}) })
export class GroupsComponent implements OnInit, OnDestroy { export class GroupsComponent implements OnInit, OnDestroy {
@ViewChildren(MatMenuTrigger) menuTriggers!: QueryList<MatMenuTrigger>; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
isSmallScreen: boolean = false; mercureUrl: string = import.meta.env.NG_APP_OGCORE_MERCURE_BASE_URL;
baseUrl: string;
mercureUrl: string;
organizationalUnits: UnidadOrganizativa[] = []; organizationalUnits: UnidadOrganizativa[] = [];
selectedUnidad: UnidadOrganizativa | null = null; selectedUnidad: UnidadOrganizativa | null = null;
selectedDetail: UnidadOrganizativa | null = null; selectedDetail: UnidadOrganizativa | null = null;
@ -87,7 +82,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
{ value: 'mac', name: 'Mac' }, { value: 'mac', name: 'Mac' },
]; ];
displayedColumns: string[] = ['select', 'status', 'ip', 'firmwareType', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions']; displayedColumns: string[] = ['select', 'status', 'ip', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
private _sort!: MatSort; private _sort!: MatSort;
@ -108,13 +103,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
public dialog: MatDialog, public dialog: MatDialog,
private bottomSheet: MatBottomSheet, private bottomSheet: MatBottomSheet,
private joyrideService: JoyrideService, private joyrideService: JoyrideService,
private breakpointObserver: BreakpointObserver, private toastr: ToastrService
private toastr: ToastrService,
private configService: ConfigService,
private cd: ChangeDetectorRef,
) { ) {
this.baseUrl = this.configService.apiUrl;
this.mercureUrl = this.configService.mercureUrl;
this.treeFlattener = new MatTreeFlattener<TreeNode, FlatNode>( this.treeFlattener = new MatTreeFlattener<TreeNode, FlatNode>(
this.transformer, this.transformer,
(node) => node.level, (node) => node.level,
@ -149,62 +139,38 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.arrayClients = this.selectedClients.data; this.arrayClients = this.selectedClients.data;
const eventSource = new EventSource(`${this.mercureUrl}?topic=` + encodeURIComponent(`clients`)); const eventSource = new EventSource(`${this.mercureUrl}?topic=`
+ encodeURIComponent(`clients`));
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data && data['@id']) {
if (Array.isArray(data)) {
data.forEach((client) => {
if (client && client['@id']) {
this.updateClientStatus(client['@id'], client.status);
}
});
}
else if (data && data['@id']) {
this.updateClientStatus(data['@id'], data.status); this.updateClientStatus(data['@id'], data.status);
} }
}; }
this.clientFilterSubject.pipe(debounceTime(500)).subscribe(searchTerm => { this.clientFilterSubject.pipe(debounceTime(500)).subscribe(searchTerm => {
this.filters['query'] = searchTerm; this.filters['query'] = searchTerm;
this.filterClients(searchTerm); this.filterClients(searchTerm);
}); });
this.breakpointObserver.observe(['(max-width: 992px)']).subscribe((result) => {
this.isSmallScreen = result.matches;
})
} }
private updateClientStatus(clientUuid: string, status: string): void { private updateClientStatus(clientUuid: string, newStatus: string): void {
let updated = false; const clientIndex = this.selectedClients.data.findIndex(client => client['@id'] === clientUuid);
const index = this.arrayClients.findIndex(client => client['@id'] === clientUuid); if (clientIndex !== -1) {
if (index !== -1) {
const updatedClient = {...this.arrayClients[index], status};
this.arrayClients = [
...this.arrayClients.slice(0, index),
updatedClient,
...this.arrayClients.slice(index + 1)
];
updated = true;
}
const tableIndex = this.selectedClients.data.findIndex(client => client['@id'] === clientUuid);
if (tableIndex !== -1) {
const updatedClients = [...this.selectedClients.data]; const updatedClients = [...this.selectedClients.data];
updatedClients[tableIndex] = { updatedClients[clientIndex] = {
...updatedClients[tableIndex], ...updatedClients[clientIndex],
status: status status: newStatus
}; };
this.selectedClients.data = updatedClients; this.selectedClients.data = updatedClients;
}
if (updated) { console.log(`Estado actualizado para el cliente ${clientUuid}: ${newStatus}`);
this.cd.detectChanges(); } else {
console.warn(`Cliente con UUID ${clientUuid} no encontrado en la lista.`);
} }
} }
@ -224,7 +190,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
hasClients: node.hasClients, hasClients: node.hasClients,
ip: node.ip, ip: node.ip,
'@id': node['@id'], '@id': node['@id'],
networkSettings: node.networkSettings,
}); });
@ -379,18 +344,9 @@ export class GroupsComponent implements OnInit, OnDestroy {
} }
onNodeClick(event: MouseEvent, node: TreeNode): void { onNodeClick(node: TreeNode): void {
event.stopPropagation();
this.selectedNode = node; this.selectedNode = node;
const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid); this.fetchClientsForNode(node);
this.fetchClientsForNode(node, selectedClientsBeforeEdit);
}
onMenuClick(event: Event, node: any): void {
event.stopPropagation();
this.selectedNode = node;
const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
this.fetchClientsForNode(node, selectedClientsBeforeEdit);
} }
@ -401,12 +357,9 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params }).subscribe({ this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params }).subscribe({
next: (response: any) => { next: (response: any) => {
this.selectedClients.data = response['hydra:member']; this.selectedClients.data = response['hydra:member'];
if (this.selectedNode) {
this.selectedNode.clients = response['hydra:member'];
}
this.length = response['hydra:totalItems']; this.length = response['hydra:totalItems'];
this.arrayClients = this.selectedClients.data; this.arrayClients = this.selectedClients.data;
this.hasClients = this.selectedClients.data.length > 0; this.hasClients = node.hasClients ?? false;
this.isLoadingClients = false; this.isLoadingClients = false;
this.initialLoading = false; this.initialLoading = false;
this.selection.clear(); this.selection.clear();
@ -437,7 +390,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
width: '900px', width: '900px',
}); });
dialogRef.afterClosed().subscribe((newUnit) => { dialogRef.afterClosed().subscribe((newUnit) => {
if (newUnit) { if (newUnit?.uuid) {
this.refreshData(newUnit.uuid); this.refreshData(newUnit.uuid);
} }
}); });
@ -500,11 +453,10 @@ export class GroupsComponent implements OnInit, OnDestroy {
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' }); : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
dialogRef.afterClosed().subscribe((result) => { dialogRef.afterClosed().subscribe(() => {
if (result?.success) { if (node) {
this.refreshData(node?.id); this.refreshData(node.id);
} }
this.menuTriggers.forEach(trigger => trigger.closeMenu());
}); });
} }
@ -568,11 +520,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' }); : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
dialogRef.afterClosed().subscribe((result) => { dialogRef.afterClosed().subscribe(() => {
if (result?.success) {
this.refreshData(this.selectedNode?.id, selectedClientsBeforeEdit); this.refreshData(this.selectedNode?.id, selectedClientsBeforeEdit);
}
this.menuTriggers.forEach(trigger => trigger.closeMenu());
}); });
} }

View File

@ -71,7 +71,6 @@ export interface TreeNode {
hasClients?: boolean; hasClients?: boolean;
clients?: Client[]; clients?: Client[];
ip?: string; ip?: string;
networkSettings?: Object;
} }
export interface FlatNode { export interface FlatNode {
@ -81,7 +80,6 @@ export interface FlatNode {
level: number; level: number;
expandable: boolean; expandable: boolean;
hasClients?: boolean; hasClients?: boolean;
networkSettings?: Object;
ip?: string; ip?: string;
'@id'?: string; '@id'?: string;
} }

View File

@ -3,22 +3,18 @@ import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { UnidadOrganizativa } from '../model/model'; import { UnidadOrganizativa } from '../model/model';
import { ConfigService } from '@services/config.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DataService { export class DataService {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl: string;
private clientsUrl: string;
constructor(private http: HttpClient, private configService: ConfigService) { private apiUrl = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
this.baseUrl = this.configService.apiUrl; private clientsUrl = `${this.baseUrl}/clients`;
this.apiUrl = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
this.clientsUrl = `${this.baseUrl}/clients`; constructor(private http: HttpClient) {}
}
getOrganizationalUnits(search: string = ''): Observable<UnidadOrganizativa[]> { getOrganizationalUnits(search: string = ''): Observable<UnidadOrganizativa[]> {
let url = `${this.apiUrl}&type=organizational-unit`; let url = `${this.apiUrl}&type=organizational-unit`;

View File

@ -4,7 +4,6 @@ import { ClientViewComponent } from "../client-view/client-view.component";
import { CdkDragMove } from '@angular/cdk/drag-drop'; import { CdkDragMove } from '@angular/cdk/drag-drop';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { ConfigService } from '@services/config.service';
interface GroupedClients { interface GroupedClients {
organizationalUnitName: string; organizationalUnitName: string;
@ -17,14 +16,12 @@ interface GroupedClients {
styleUrls: ['./classroom-view.component.css'] styleUrls: ['./classroom-view.component.css']
}) })
export class ClassroomViewComponent implements OnInit, OnChanges { export class ClassroomViewComponent implements OnInit, OnChanges {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
@Input() clients: any[] = []; @Input() clients: any[] = [];
@Input() pcInTable: number = 5; @Input() pcInTable: number = 5;
groupedClients: GroupedClients[] = []; groupedClients: GroupedClients[] = [];
constructor(public dialog: MatDialog, private http: HttpClient, private toastService: ToastrService, private configService: ConfigService) { constructor(public dialog: MatDialog, private http: HttpClient, private toastService: ToastrService) { }
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void { ngOnInit(): void {
this.groupClientsByOrganizationalUnit(); this.groupClientsByOrganizationalUnit();

View File

@ -1,10 +1,10 @@
import {Component, Inject, OnInit, Optional} from '@angular/core'; import {Component, Inject, OnInit, Optional} from '@angular/core';
import {MatDialogRef} from "@angular/material/dialog"; import {MatDialogRef} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {MatSnackBar} from "@angular/material/snack-bar";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import {MAT_DIALOG_DATA} from "@angular/material/dialog"; import {MAT_DIALOG_DATA} from "@angular/material/dialog";
import { DataService } from '../../../services/data.service'; import { DataService } from '../../../services/data.service';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-create-multiple-client', selector: 'app-create-multiple-client',
@ -12,7 +12,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './create-multiple-client.component.css' styleUrl: './create-multiple-client.component.css'
}) })
export class CreateMultipleClientComponent implements OnInit{ export class CreateMultipleClientComponent implements OnInit{
baseUrl: string = this.configService.apiUrl; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
parentUnits: any[] = []; parentUnits: any[] = [];
parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
uploadedClients: any[] = []; uploadedClients: any[] = [];
@ -20,18 +20,16 @@ export class CreateMultipleClientComponent implements OnInit {
displayedColumns: string[] = ['name', 'ip', 'mac']; displayedColumns: string[] = ['name', 'ip', 'mac'];
showTextarea: boolean = true; showTextarea: boolean = true;
organizationalUnit: any; organizationalUnit: any;
regex: RegExp = /host\s+(\S+)\s*\{\s*hardware\s+ethernet\s+([a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5});\s*fixed-address\s+((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?));\s*\}/g; regex: RegExp = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([a-zA-Z0-9]{2}(:[a-zA-Z0-9]{2}){5});\s+fixed-address\s+((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?));\s+\}/g;
constructor( constructor(
@Optional() private dialogRef: MatDialogRef<CreateMultipleClientComponent>, @Optional() private dialogRef: MatDialogRef<CreateMultipleClientComponent>,
@Inject(MAT_DIALOG_DATA) private data: any, @Inject(MAT_DIALOG_DATA) private data: any,
private http: HttpClient, private http: HttpClient,
private configService: ConfigService, private snackBar: MatSnackBar,
private toastService: ToastrService, private toastService: ToastrService,
private dataService: DataService private dataService: DataService
) { ) {}
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void { ngOnInit(): void {
this.loadParentUnits(); this.loadParentUnits();

View File

@ -101,10 +101,6 @@
</mat-select> </mat-select>
<mat-error>{{ 'menuError' | translate }}</mat-error> <mat-error>{{ 'menuError' | translate }}</mat-error>
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="maintenance">
{{ 'maintenance' | translate }}
</mat-checkbox>
</form> </form>
</div> </div>

View File

@ -8,18 +8,12 @@ import { TranslateModule } from '@ngx-translate/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ManageClientComponent } from './manage-client.component'; import { ManageClientComponent } from './manage-client.component';
import { DataService } from '../../../services/data.service'; import { DataService } from '../../../services/data.service';
import { ConfigService } from '@services/config.service';
describe('ManageClientComponent', () => { describe('ManageClientComponent', () => {
let component: ManageClientComponent; let component: ManageClientComponent;
let fixture: ComponentFixture<ManageClientComponent>; let fixture: ComponentFixture<ManageClientComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ManageClientComponent], declarations: [ManageClientComponent],
imports: [ imports: [
@ -34,7 +28,6 @@ describe('ManageClientComponent', () => {
providers: [ providers: [
{ provide: MatDialogRef, useValue: {} }, { provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { uuid: '123', organizationalUnit: { '@id': '/units/1' } } }, { provide: MAT_DIALOG_DATA, useValue: { uuid: '123', organizationalUnit: { '@id': '/units/1' } } },
{ provide: ConfigService, useValue: mockConfigService },
DataService DataService
] ]
}) })

View File

@ -2,9 +2,9 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Component, Inject, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { DataService } from '../../../services/data.service'; import { DataService } from '../../../services/data.service';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-manage-client', selector: 'app-manage-client',
@ -12,7 +12,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./manage-client.component.css'] styleUrls: ['./manage-client.component.css']
}) })
export class ManageClientComponent implements OnInit { export class ManageClientComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
clientForm!: FormGroup; clientForm!: FormGroup;
parentUnits: any[] = []; parentUnits: any[] = [];
parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
@ -38,12 +38,11 @@ export class ManageClientComponent implements OnInit {
private fb: FormBuilder, private fb: FormBuilder,
private dialogRef: MatDialogRef<ManageClientComponent>, private dialogRef: MatDialogRef<ManageClientComponent>,
private http: HttpClient, private http: HttpClient,
private configService: ConfigService, private snackBar: MatSnackBar,
private toastService: ToastrService, private toastService: ToastrService,
private dataService: DataService, private dataService: DataService,
@Inject(MAT_DIALOG_DATA) public data: any @Inject(MAT_DIALOG_DATA) public data: any
) { ) {
this.baseUrl = this.configService.apiUrl;
this.isEditMode = !!data?.uuid; this.isEditMode = !!data?.uuid;
this.dialogTitle = this.isEditMode ? 'editClientDialogTitle' : 'addClientDialogTitle'; this.dialogTitle = this.isEditMode ? 'editClientDialogTitle' : 'addClientDialogTitle';
} }
@ -84,14 +83,13 @@ export class ManageClientComponent implements OnInit {
serialNumber: [''], serialNumber: [''],
netiface: null, netiface: null,
netDriver: null, netDriver: null,
mac: ['', Validators.pattern(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/)], mac: ['', Validators.required],
ip: ['', Validators.required], ip: ['', Validators.required],
template: [null], template: [null],
hardwareProfile: [null], hardwareProfile: [null],
ogLive: [null], ogLive: [null],
repository: [null], repository: [null],
menu: [null], menu: [null]
maintenance: [false]
}); });
} }
@ -105,7 +103,6 @@ export class ManageClientComponent implements OnInit {
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
id: unit['@id'], id: unit['@id'],
name: unit.name, name: unit.name,
netiface: unit.networkSettings?.netiface,
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits), path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits),
repository: unit.networkSettings?.repository?.['@id'], repository: unit.networkSettings?.repository?.['@id'],
hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'], hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'],
@ -227,8 +224,7 @@ export class ManageClientComponent implements OnInit {
repository: selectedUnit.repository || null, repository: selectedUnit.repository || null,
hardwareProfile: selectedUnit.hardwareProfile || null, hardwareProfile: selectedUnit.hardwareProfile || null,
ogLive: selectedUnit.ogLive || null, ogLive: selectedUnit.ogLive || null,
menu: selectedUnit.menu || null, menu: selectedUnit.menu || null
netiface: selectedUnit.netiface || null,
}); });
} }
} }
@ -252,7 +248,6 @@ export class ManageClientComponent implements OnInit {
ogLive: data.ogLive ? data.ogLive['@id'] : null, ogLive: data.ogLive ? data.ogLive['@id'] : null,
template: data.template ? data.template['@id'] : null, template: data.template ? data.template['@id'] : null,
menu: data.menu ? data.menu['@id'] : null, menu: data.menu ? data.menu['@id'] : null,
maintenance: data.maintenance
}); });
resolve(); resolve();
}, },
@ -274,7 +269,7 @@ export class ManageClientComponent implements OnInit {
this.http.patch<any>(putUrl, formData, { headers }).subscribe( this.http.patch<any>(putUrl, formData, { headers }).subscribe(
response => { response => {
this.dialogRef.close({ success: true }); this.dialogRef.close();
this.toastService.success('Cliente actualizado exitosamente', 'Éxito'); this.toastService.success('Cliente actualizado exitosamente', 'Éxito');
}, },
error => { error => {
@ -287,7 +282,6 @@ export class ManageClientComponent implements OnInit {
(response) => { (response) => {
this.toastService.success('Cliente creado exitosamente', 'Éxito'); this.toastService.success('Cliente creado exitosamente', 'Éxito');
this.dialogRef.close({ this.dialogRef.close({
success: true,
client: response, client: response,
organizationalUnit: formData.organizationalUnit, organizationalUnit: formData.organizationalUnit,
}); });
@ -303,6 +297,6 @@ export class ManageClientComponent implements OnInit {
} }
onNoClick(): void { onNoClick(): void {
this.dialogRef.close({ success: false }); this.dialogRef.close();
} }
} }

View File

@ -3,7 +3,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-execute-command-ou', selector: 'app-execute-command-ou',
@ -15,17 +14,15 @@ export class ExecuteCommandOuComponent implements OnInit {
clients: any[] = []; clients: any[] = [];
commands: any[] = []; commands: any[] = [];
commandGroups: any[] = []; commandGroups: any[] = [];
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
constructor( constructor(
private dialogRef: MatDialogRef<ExecuteCommandOuComponent>, private dialogRef: MatDialogRef<ExecuteCommandOuComponent>,
@Inject(MAT_DIALOG_DATA) public data: any, @Inject(MAT_DIALOG_DATA) public data: any,
private http: HttpClient, private http: HttpClient,
private fb: FormBuilder, private fb: FormBuilder,
private configService: ConfigService,
private toastService: ToastrService, private toastService: ToastrService,
) { ) {
this.baseUrl = this.configService.apiUrl;
this.form = this.fb.group({ this.form = this.fb.group({
selectedCommand: [null], selectedCommand: [null],
selectedCommandGroup: [null], selectedCommandGroup: [null],

View File

@ -6,10 +6,6 @@ h1 {
margin-top: 20px; margin-top: 20px;
} }
.create-ou-container {
position: relative;
}
.form-field { .form-field {
width: 100%; width: 100%;
} }
@ -24,13 +20,6 @@ h1 {
overflow-y: auto; overflow-y: auto;
} }
.loading-spinner {
display: block;
margin: 0 auto;
align-items: center;
justify-content: center;
}
.mat-dialog-actions { .mat-dialog-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -1,10 +1,11 @@
<div class="create-ou-container"> <app-loading [isLoading]="loading"></app-loading>
<div *ngIf="!loading">
<h1 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Crear' }} Unidad Organizativa</h1> <h1 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Crear' }} Unidad Organizativa</h1>
<div class="mat-dialog-content" [ngClass]="{'loading': loading}"> <div class="mat-dialog-content">
<!-- Paso 1: General --> <!-- Paso 1: General -->
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner> <span class="step-title">General</span>
<span *ngIf="!loading" class="step-title">General</span> <form [formGroup]="generalFormGroup" class="grid-form">
<form *ngIf="generalFormGroup && !loading" [formGroup]="generalFormGroup" class="grid-form">
<mat-form-field class="form-field" appearance="fill"> <mat-form-field class="form-field" appearance="fill">
<mat-label>Tipo</mat-label> <mat-label>Tipo</mat-label>
<mat-select formControlName="type" required> <mat-select formControlName="type" required>
@ -19,7 +20,7 @@
</mat-form-field> </mat-form-field>
<mat-form-field class="form-field" appearance="fill"> <mat-form-field class="form-field" appearance="fill">
<mat-label>Padre</mat-label> <mat-label>Padre</mat-label>
<mat-select formControlName="parent" (selectionChange)="onParentChange($event)"> <mat-select formControlName="parent">
<mat-select-trigger> <mat-select-trigger>
{{ getSelectedParentName() }} {{ getSelectedParentName() }}
</mat-select-trigger> </mat-select-trigger>
@ -41,8 +42,8 @@
</form> </form>
<!-- Paso 2: Información del Aula --> <!-- Paso 2: Información del Aula -->
<span *ngIf="generalFormGroup.value.type === 'classroom' && !loading" class="step-title">Información del aula</span> <span *ngIf="generalFormGroup.value.type === 'classroom'" class="step-title">Información del aula</span>
<form *ngIf="generalFormGroup.value.type === 'classroom' && !loading" class="grid-form" <form *ngIf="generalFormGroup.value.type === 'classroom'" class="grid-form"
[formGroup]="classroomInfoFormGroup"> [formGroup]="classroomInfoFormGroup">
<mat-form-field class="form-field"> <mat-form-field class="form-field">
<mat-label>Localización</mat-label> <mat-label>Localización</mat-label>
@ -70,8 +71,8 @@
</form> </form>
<!-- Paso 3: Configuración de Red --> <!-- Paso 3: Configuración de Red -->
<span *ngIf="!loading" class="step-title">Configuración de Red</span> <span class="step-title">Configuración de Red</span>
<form *ngIf="networkSettingsFormGroup && !loading" [formGroup]="networkSettingsFormGroup" class="grid-form"> <form [formGroup]="networkSettingsFormGroup" class="grid-form">
<mat-form-field class="form-field"> <mat-form-field class="form-field">
<mat-label>OgLive</mat-label> <mat-label>OgLive</mat-label>
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)"> <mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
@ -167,8 +168,8 @@
</form> </form>
<!-- Paso 4: Información Adicional --> <!-- Paso 4: Información Adicional -->
<span *ngIf="!loading" class="step-title">Información Adicional</span> <span class="step-title">Información Adicional</span>
<form *ngIf="additionalInfoFormGroup && !loading" [formGroup]="additionalInfoFormGroup"> <form [formGroup]="additionalInfoFormGroup">
<mat-form-field class="form-field"> <mat-form-field class="form-field">
<mat-label>Comentarios</mat-label> <mat-label>Comentarios</mat-label>
<textarea matInput formControlName="comments"></textarea> <textarea matInput formControlName="comments"></textarea>

View File

@ -13,18 +13,12 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { LoadingComponent } from '../../../../../shared/loading/loading.component'; import { LoadingComponent } from '../../../../../shared/loading/loading.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ConfigService } from '@services/config.service';
describe('ManageOrganizationalUnitComponent', () => { describe('ManageOrganizationalUnitComponent', () => {
let component: ManageOrganizationalUnitComponent; let component: ManageOrganizationalUnitComponent;
let fixture: ComponentFixture<ManageOrganizationalUnitComponent>; let fixture: ComponentFixture<ManageOrganizationalUnitComponent>;
beforeEach(async () => { beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ManageOrganizationalUnitComponent, LoadingComponent], declarations: [ManageOrganizationalUnitComponent, LoadingComponent],
imports: [ imports: [
@ -42,8 +36,7 @@ describe('ManageOrganizationalUnitComponent', () => {
], ],
providers: [ providers: [
{ provide: MatDialogRef, useValue: {} }, { provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: {} }, { provide: MAT_DIALOG_DATA, useValue: {} }
{ provide: ConfigService, useValue: mockConfigService }
] ]
}) })
.compileComponents(); .compileComponents();

View File

@ -4,7 +4,6 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from "../../../services/data.service"; import { DataService } from "../../../services/data.service";
import { ToastrService } from "ngx-toastr"; import { ToastrService } from "ngx-toastr";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-manage-organizational-unit', selector: 'app-manage-organizational-unit',
@ -12,7 +11,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./manage-organizational-unit.component.css'] styleUrls: ['./manage-organizational-unit.component.css']
}) })
export class ManageOrganizationalUnitComponent implements OnInit { export class ManageOrganizationalUnitComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
isLinear = true; isLinear = true;
generalFormGroup: FormGroup; generalFormGroup: FormGroup;
additionalInfoFormGroup: FormGroup; additionalInfoFormGroup: FormGroup;
@ -56,12 +55,11 @@ export class ManageOrganizationalUnitComponent implements OnInit {
private dialogRef: MatDialogRef<ManageOrganizationalUnitComponent>, private dialogRef: MatDialogRef<ManageOrganizationalUnitComponent>,
private http: HttpClient, private http: HttpClient,
private dataService: DataService, private dataService: DataService,
private configService: ConfigService,
private toastService: ToastrService, private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: any @Inject(MAT_DIALOG_DATA) public data: any
) { ) {
this.isEditMode = !!data?.uuid; this.isEditMode = !!data?.uuid;
this.baseUrl = this.configService.apiUrl;
this.generalFormGroup = this._formBuilder.group({ this.generalFormGroup = this._formBuilder.group({
name: [null, Validators.required], name: [null, Validators.required],
parent: [data?.parent ? data.parent['@id'] : null], parent: [data?.parent ? data.parent['@id'] : null],
@ -100,39 +98,27 @@ export class ManageOrganizationalUnitComponent implements OnInit {
capacity: [null, [Validators.required, Validators.min(0)]], capacity: [null, [Validators.required, Validators.min(0)]],
remoteCalendar: [null] remoteCalendar: [null]
}); });
}
ngOnInit(): void {
this.loading = true;
const observables = [
this.loadParentUnits(),
this.loadHardwareProfiles(),
this.loadCalendars(),
this.loadOgLives(),
this.loadRepositories(),
this.loadMenus(),
];
Promise.all(observables).then(() => {
if (this.isEditMode) { if (this.isEditMode) {
this.loadData(this.data.uuid).then(() => { this.loadData(data.uuid);
this.loading = false;
});
} else {
this.loading = false;
} }
}).catch(error => { }
console.error('Error loading data:', error);
this.loading = false; ngOnInit() {
}); this.loadParentUnits();
this.loadHardwareProfiles();
this.loadCalendars();
this.loadOgLives();
this.loadRepositories();
this.loadMenus()
} }
get filteredTypes(): string[] { get filteredTypes(): string[] {
return this.generalFormGroup.get('parent')?.value ? this.types.filter(type => type !== 'organizational-unit') : this.types; return this.generalFormGroup.get('parent')?.value ? this.types.filter(type => type !== 'organizational-unit') : this.types;
} }
loadParentUnits(): Promise<void> { loadParentUnits() {
return new Promise((resolve, reject) => { this.loading = true;
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`; const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
this.http.get<any>(url).subscribe( this.http.get<any>(url).subscribe(
response => { response => {
@ -140,63 +126,15 @@ export class ManageOrganizationalUnitComponent implements OnInit {
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
id: unit['@id'], id: unit['@id'],
name: unit.name, name: unit.name,
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits), 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'],
mcastIp: unit.networkSettings?.mcastIp,
mcastSpeed: unit.networkSettings?.mcastSpeed,
mcastPort: unit.networkSettings?.mcastPort,
mcastMode: unit.networkSettings?.mcastMode,
netiface: unit.networkSettings?.netiface,
p2pMode: unit.networkSettings?.p2pMode,
p2pTime: unit.networkSettings?.p2pTime,
dns: unit.networkSettings?.dns,
netmask: unit.networkSettings?.netmask,
router: unit.networkSettings?.router,
ntp: unit.networkSettings?.ntp
})); }));
this.loading = false;
const initialUnitId = this.generalFormGroup.get('parent')?.value;
if (initialUnitId) {
this.setOrganizationalUnitDefaults(initialUnitId);
}
resolve();
}, },
error => { error => {
console.error('Error fetching parent units:', error); console.error('Error fetching parent units:', error);
reject(error); this.loading = false;
} }
); );
});
}
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.networkSettingsFormGroup.patchValue({
repository: selectedUnit.repository || null,
hardwareProfile: selectedUnit.hardwareProfile || null,
ogLive: selectedUnit.ogLive || null,
menu: selectedUnit.menu || null,
mcastIp: selectedUnit.mcastIp || null,
mcastSpeed: selectedUnit.mcastSpeed || null,
mcastPort: selectedUnit.mcastPort || null,
mcastMode: selectedUnit.mcastMode || null,
netiface: selectedUnit.netiface || null,
p2pMode: selectedUnit.p2pMode || null,
p2pTime: selectedUnit.p2pTime || null,
dns: selectedUnit.dns || null,
netmask: selectedUnit.netmask || null,
router: selectedUnit.router || null,
ntp: selectedUnit.ntp || null
});
}
} }
getSelectedParentName(): string | undefined { getSelectedParentName(): string | undefined {
@ -204,87 +142,78 @@ export class ManageOrganizationalUnitComponent implements OnInit {
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
} }
loadHardwareProfiles(): Promise<void> { loadHardwareProfiles(): void {
return new Promise((resolve, reject) => { this.loading = true;
this.dataService.getHardwareProfiles().subscribe( this.dataService.getHardwareProfiles().subscribe(
(data: any[]) => { (data: any[]) => {
this.hardwareProfiles = data; this.hardwareProfiles = data;
resolve(); this.loading = false;
}, },
error => { (error: any) => {
console.error('Error fetching hardware profiles:', error); console.error('Error fetching hardware profiles', error);
reject(error); this.loading = false;
} }
); );
});
} }
loadOgLives(): Promise<void> { loadMenus(): void {
return new Promise((resolve, reject) => { this.loading = true;
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);
}
);
});
}
loadMenus(): Promise<void> {
return new Promise((resolve, reject) => {
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`; const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe( this.http.get<any>(url).subscribe(
response => { response => {
this.menus = response['hydra:member']; this.menus = response['hydra:member'];
resolve(); this.loading = false;
}, },
error => { error => {
console.error('Error fetching menus:', error); console.error('Error fetching menus:', error);
reject(error); this.loading = false;
} }
); );
});
} }
loadRepositories(): Promise<void> { loadOgLives() {
return new Promise((resolve, reject) => { this.loading = true;
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`; this.dataService.getOgLives().subscribe(
(data: any[]) => {
this.http.get<any>(url).subscribe( this.ogLives = data;
response => { this.loading = false;
this.repositories = response['hydra:member'];
resolve();
}, },
error => { error => {
console.error('Error fetching ogLives:', error); console.error('Error fetching ogLives', error);
reject(error); this.loading = false;
} }
); );
});
} }
loadCalendars(): Promise<void> { loadRepositories() {
return new Promise((resolve, reject) => { this.loading = true;
this.dataService.getRepositories().subscribe(
(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`; const apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=30`;
this.http.get<any>(apiUrl).subscribe( this.http.get<any>(apiUrl).subscribe(
response => { response => {
this.calendars = response['hydra:member']; this.calendars = response['hydra:member'];
resolve(); this.loading = false;
}, },
error => { error => {
console.error('Error loading calendars', error); console.error('Error loading calendars', error);
this.toastService.error('Error loading current calendar'); this.toastService.error('Error loading current calendar');
reject(error); this.loading = false;
} }
); );
});
} }
loadCurrentCalendar(uuid: string): void { loadCurrentCalendar(uuid: string): void {
@ -315,8 +244,8 @@ export class ManageOrganizationalUnitComponent implements OnInit {
this.networkSettingsFormGroup.value.repository = event.value; this.networkSettingsFormGroup.value.repository = event.value;
} }
loadData(uuid: string): Promise<void> { loadData(uuid: string) {
return new Promise((resolve, reject) => { this.loading = true;
const url = `${this.baseUrl}/organizational-units/${uuid}`; const url = `${this.baseUrl}/organizational-units/${uuid}`;
this.http.get<any>(url).subscribe( this.http.get<any>(url).subscribe(
@ -332,22 +261,22 @@ export class ManageOrganizationalUnitComponent implements OnInit {
comments: data.comments comments: data.comments
}); });
this.networkSettingsFormGroup.patchValue({ this.networkSettingsFormGroup.patchValue({
proxy: data.networkSettings?.proxy, proxy: data.networkSettings.proxy,
dns: data.networkSettings?.dns, dns: data.networkSettings.dns,
netmask: data.networkSettings?.netmask, netmask: data.networkSettings.netmask,
router: data.networkSettings?.router, router: data.networkSettings.router,
ntp: data.networkSettings?.ntp, ntp: data.networkSettings.ntp,
netiface: data.networkSettings?.netiface, netiface: data.networkSettings.netiface,
p2pMode: data.networkSettings?.p2pMode, p2pMode: data.networkSettings.p2pMode,
p2pTime: data.networkSettings?.p2pTime, p2pTime: data.networkSettings.p2pTime,
mcastIp: data.networkSettings?.mcastIp, mcastIp: data.networkSettings.mcastIp,
mcastSpeed: data.networkSettings?.mcastSpeed, mcastSpeed: data.networkSettings.mcastSpeed,
mcastPort: data.networkSettings?.mcastPort, mcastPort: data.networkSettings.mcastPort,
mcastMode: data.networkSettings?.mcastMode, mcastMode: data.networkSettings.mcastMode,
menu: data.networkSettings?.menu ? data.networkSettings.menu['@id'] : null, menu: data.networkSettings.menu ? data.networkSettings.menu['@id'] : null,
hardwareProfile: data.networkSettings?.hardwareProfile ? data.networkSettings.hardwareProfile['@id'] : null, hardwareProfile: data.networkSettings.hardwareProfile ? data.networkSettings.hardwareProfile['@id'] : null,
ogLive: data.networkSettings?.ogLive ? data.networkSettings.ogLive['@id'] : null, ogLive: data.networkSettings.ogLive ? data.networkSettings.ogLive['@id'] : null,
repository: data.networkSettings?.repository ? data.networkSettings.repository['@id'] : null repository: data.networkSettings.repository ? data.networkSettings.repository['@id'] : null
}); });
this.classroomInfoFormGroup.patchValue({ this.classroomInfoFormGroup.patchValue({
location: data.location, location: data.location,
@ -356,17 +285,16 @@ export class ManageOrganizationalUnitComponent implements OnInit {
capacity: data.capacity, capacity: data.capacity,
remoteCalendar: data.remoteCalendar ? data.remoteCalendar['@id'] : null remoteCalendar: data.remoteCalendar ? data.remoteCalendar['@id'] : null
}); });
resolve(); this.loading = false;
}, },
error => { error => {
console.error('Error fetching data for edit:', error); console.error('Error fetching data for edit:', error);
this.toastService.error('Error fetching data'); this.toastService.error('Error fetching data');
reject(error); this.loading = false;
this.onNoClick(); this.onNoClick();
} }
); );
});
} }
onSubmit() { onSubmit() {
@ -394,7 +322,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
this.http.put<any>(putUrl, formData, { headers }).subscribe( this.http.put<any>(putUrl, formData, { headers }).subscribe(
response => { response => {
this.unitAdded.emit(); this.unitAdded.emit();
this.dialogRef.close({ success: true }); this.dialogRef.close();
this.toastService.success('Editado exitosamente', 'Éxito'); this.toastService.success('Editado exitosamente', 'Éxito');
}, },
error => { error => {
@ -413,7 +341,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
this.http.post<any>(postUrl, formData, { headers }).subscribe( this.http.post<any>(postUrl, formData, { headers }).subscribe(
response => { response => {
this.unitAdded.emit(response); this.unitAdded.emit(response);
this.dialogRef.close({ success: true }); this.dialogRef.close(response);
this.toastService.success('Creado exitosamente', 'Éxito'); this.toastService.success('Creado exitosamente', 'Éxito');
}, },
error => { error => {
@ -430,6 +358,6 @@ export class ManageOrganizationalUnitComponent implements OnInit {
} }
onNoClick(): void { onNoClick(): void {
this.dialogRef.close({ success: false }); this.dialogRef.close();
} }
} }

View File

@ -2,7 +2,6 @@ import { HttpClient } from '@angular/common/http';
import {Component, Inject, OnInit} from '@angular/core'; import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {DatePipe} from "@angular/common"; import {DatePipe} from "@angular/common";
import { ConfigService } from '@services/config.service';
@Component({ @Component({
selector: 'app-show-organizational-unit', selector: 'app-show-organizational-unit',
@ -10,7 +9,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './show-organizational-unit.component.css' styleUrl: './show-organizational-unit.component.css'
}) })
export class ShowOrganizationalUnitComponent implements OnInit { export class ShowOrganizationalUnitComponent implements OnInit {
baseUrl: string; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
displayedColumns: string[] = ['property', 'value']; displayedColumns: string[] = ['property', 'value'];
currentCalendar: any; currentCalendar: any;
ou: any; ou: any;
@ -27,10 +26,8 @@ export class ShowOrganizationalUnitComponent implements OnInit {
constructor( constructor(
@Inject(MAT_DIALOG_DATA) public data: any, @Inject(MAT_DIALOG_DATA) public data: any,
private dialogRef: MatDialogRef<ShowOrganizationalUnitComponent>, private dialogRef: MatDialogRef<ShowOrganizationalUnitComponent>,
private configService: ConfigService,
private http: HttpClient private http: HttpClient
) { ) {
this.baseUrl = this.configService.apiUrl;
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -1,25 +1,8 @@
.create-image-container { .dialog-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1rem; gap: 16px;
} /* Espacio entre los elementos del formulario */
.loading-spinner {
display: block;
margin: 0 auto;
}
.mat-dialog-content.loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.mat-dialog-content {
padding-left: 1.5em;
padding-right: 1.5em;
padding-top: 1em;
} }
.image-form { .image-form {

View File

@ -1,13 +1,13 @@
<div class="create-image-container"> <app-loading [isLoading]="loading"></app-loading>
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Crear' }} imagen</h2>
<div class="mat-dialog-content" [ngClass]="{'loading': loading}"> <h2 mat-dialog-title>{{ imageId ? 'Editar' : 'Crear' }} imagen</h2>
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<form *ngIf="!loading" [formGroup]="imageForm" (ngSubmit)="saveImage()" class="image-form"> <mat-dialog-content class="dialog-content">
<form [formGroup]="imageForm" (ngSubmit)="saveImage()" class="image-form">
<mat-card *ngIf="showWarning" class="warning-card"> <mat-card *ngIf="showWarning" class="warning-card">
<mat-card-content> <mat-card-content>
<mat-icon color="warn">warning</mat-icon> <mat-icon color="warn">warning</mat-icon>
Ha marcado la casilla <strong>"Imagen Global"</strong>. Se transferirá la imagen al resto de repositorios en Ha marcado la casilla <strong>"Imagen Global"</strong>. Se transferirá la imagen al resto de repositorios en el
el
caso de que no exista previamente. caso de que no exista previamente.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -63,10 +63,10 @@
<p>Código de partición: {{ partitionInfo['partitionCode'] }}</p> <p>Código de partición: {{ partitionInfo['partitionCode'] }}</p>
</div> </div>
</form> </form>
</div>
</mat-dialog-content>
<mat-dialog-actions class="action-container"> <mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">{{ 'cancelButton' | translate }}</button> <button class="ordinary-button" (click)="close()">{{ 'cancelButton' | translate }}</button>
<button class="submit-button" (click)="saveImage()" [disabled]="loading">{{ 'saveButton' | translate }}</button> <button class="submit-button" (click)="saveImage()">{{ 'saveButton' | translate }}</button>
</mat-dialog-actions> </mat-dialog-actions>
</div>

Some files were not shown because too many files have changed in this diff Show More