Compare commits
13 Commits
main
...
jenkins-de
Author | SHA1 | Date |
---|---|---|
|
afaacd976e | |
|
b359703278 | |
|
a7ce0f3859 | |
|
e04a8204a7 | |
|
189d6b8cb2 | |
|
91848a90e6 | |
|
d0c3730fd1 | |
|
ca1a4d0fc2 | |
|
74ef13906d | |
|
7bf25a86c8 | |
|
eedc6cb59d | |
|
df1c42feec | |
|
c41ca6e8ce |
123
CHANGELOG.md
123
CHANGELOG.md
|
@ -1,145 +1,40 @@
|
|||
# Changelog
|
||||
## [0.14.1] - 2025-06-09
|
||||
### Fixed
|
||||
- Se han corregido los errores en produccion que hacia que no salieran mensajes desde la API correctamente.
|
||||
|
||||
---
|
||||
## [0.14.0] - 2025-06-02
|
||||
### Added
|
||||
- Se ha añadido funcionalidad de usuarios/roles para separar las vistas segun los permisos.
|
||||
- Nuevo boton de "mover" en la pantalla de grupos, para mover equipos entre aulas y grupos.
|
||||
|
||||
### Improved
|
||||
- Se ha completado la opcion de inicion de sesion y eliminar imagen cache.
|
||||
|
||||
---
|
||||
## [0.13.1] - 2025-05-23
|
||||
### Changed
|
||||
- Desactivado temporalmente la funcionalidad de ogGit.
|
||||
|
||||
---
|
||||
## [0.13.0] - 2025-05-20
|
||||
### Added
|
||||
- Se ha añadido nuevo campo "SSL_ENABLED" en el apartado configuracion.
|
||||
|
||||
### Improved
|
||||
- Mejoras en el asistente de particionado.
|
||||
- Se han añadido mejoras en los filtros de las trazas.
|
||||
|
||||
---
|
||||
## [0.12.0] - 2025-5-13
|
||||
### Added
|
||||
- Se ha añadido un nuevo modal del detalle de las acciones ejecutadas por cada cliente.
|
||||
- Se ha añadido un modulo para la gestion de las tareas y acciones programadas.
|
||||
- Se han añadido nuevos campos en el listado general de clientes.
|
||||
|
||||
### Improved
|
||||
- Se ha cambiado la pagina de detalles de un cliente, por un modal.
|
||||
- Se han actualizado gran parte de las ayudas contextuales de las distintas parrillas de datos.
|
||||
- Se ha mejorado y corregido los errores del particionador.
|
||||
- Mejoras en la pantalla de trazas.
|
||||
- Cambios en la estetica general de la aplicacion
|
||||
- Añadida la primera version de la la integracion con ogGit
|
||||
- Se ha mejorado la responsividad de la aplicacion, para pantallas pequeñas.
|
||||
|
||||
### Fixed
|
||||
- Se ha corregido un error que hacia que no apareciesen los calendarios en la pantalla de editar OU.
|
||||
- En la pantalla de hacer deploy, al seleccionar imagen ahora deja desmarcarla.
|
||||
|
||||
## [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
|
||||
### Changed
|
||||
### ⚡ Changed
|
||||
- Se ha modificado el acceso a Mercure añadiendo nueva variable de entorno.
|
||||
|
||||
---
|
||||
|
||||
## [0.9.0] - 2025-3-4
|
||||
### Added
|
||||
### 🔹 Added
|
||||
- Integracion con Mercure. Subscriber tanto en "Trazas" con en "Clientes".
|
||||
- Nueva funcionalidad para checkear la integridad de una imagen. Boton en apartado "imagenes" dentro del repositorio.
|
||||
- Centralizacion de estilos.
|
||||
- Nueva funcionalidad para realizar backup de imágenes.
|
||||
- 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.
|
||||
- Cambios en filtros de "Grupos". Ahora se pueden filtrar por "Centro" y "Unidad Organizativa" y estado. Ahora se busca en base de datos, y no en una lista de clientes dados.
|
||||
- Refactorizados compontentes de crear/editar clientes en uno solo.
|
||||
- Cambios en DHCP. Nueva UX en "ver clientes". Ahora tenemos un buscador detallado.
|
||||
- Para gestionar/añadir clientes a subredes ahora tenemos un botón para "añadir todos" y tan solo nos aparecn los equipos que no estén previamente asignados en una subred.
|
||||
|
||||
---
|
||||
## [0.7.0] - 2024-12-10
|
||||
|
||||
### Refactored
|
||||
- Refactored the group screen, removing the separate tabs for clients, advanced search, and organizational units.
|
||||
- Added support for partitioning functionality in the client detail view.
|
||||
- Boton para cancelar despliegues de imagenes. Aparece en "trazas" tan solo para los comendos "deploy" y para el estado "en progreos".
|
||||
|
||||
---
|
||||
## [0.6.1] - 2024-11-19
|
||||
|
||||
### Improved
|
||||
- Introduced a new automatic sync mode for the ogdhcp and ogBoot components.
|
||||
- Improve test coverage.
|
||||
- New view for clients inside the classroom on the main page.
|
||||
|
||||
---
|
||||
## [0.6.0] - 2024-11-19
|
||||
### Added
|
||||
|
||||
### 🔹 Added
|
||||
- Added functionality to execute actions from the menu in the general groups screen.
|
||||
- Displayed the selected center on the general screen for better context.
|
||||
- Implemented the option to collapse the sidebar for improved usability.
|
||||
|
|
|
@ -48,52 +48,16 @@ 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') {
|
||||
steps {
|
||||
script {
|
||||
construirPaquete(env.BUILD_DIR, "../artifacts", "172.17.8.68", "/var/tmp/opengnsys/debian-repo/oggui")
|
||||
}
|
||||
}
|
||||
}
|
||||
stage ('Publish to Debian Repository') {
|
||||
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)
|
||||
dir("${env.BUILD_DIR}") {
|
||||
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"
|
||||
scp -r ../artifacts/* aptly@172.17.8.68:/var/tmp/opengnsys/debian-repo/
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,4 +68,10 @@ pipeline {
|
|||
}
|
||||
}
|
||||
}
|
||||
// stage ('Publish to Debian Repository') {
|
||||
// agent { label 'debian-repo' }
|
||||
// steps {
|
||||
// sh "aptly repo add opengnsys-devel /var/tmp/opengnsys/debian-repo/*.deb"
|
||||
// }
|
||||
// }
|
||||
|
||||
|
|
|
@ -8,6 +8,6 @@ Standards-Version: 4.5.0
|
|||
Package: oggui
|
||||
Architecture: any
|
||||
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}, nginx
|
||||
Description: OpenGnsys GUI created for the Opengnsys Team
|
||||
Opengnsys Graphical Intercface
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}, nginx, nodejs, npm
|
||||
Description: OpenGnsys GUI
|
||||
Una interfaz gráfica para OpenGnsys.
|
||||
|
|
|
@ -5,6 +5,6 @@ set -e
|
|||
. /usr/share/debconf/confmodule
|
||||
|
||||
db_input high opengnsys/oggui_ogcoreUrl || true
|
||||
db_input high opengnsys/oggui_ogmercureUrl || true
|
||||
|
||||
|
||||
db_go
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
ogWebconsole/dist/oggui/browser /opt/opengnsys/oggui/
|
||||
ogWebconsole/dist/oggui/browser /opt/opengnsys/oggui/browser/
|
||||
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/
|
||||
|
||||
|
|
|
@ -1,58 +1,51 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
db_get opengnsys/oggui_ogcoreUrl
|
||||
OGCORE_URL="$RET"
|
||||
db_get opengnsys/oggui_ogmercureUrl
|
||||
OGMERCURE_URL="$RET"
|
||||
|
||||
# Asegurarse de que el usuario exista
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
# Provisionar base de datos si es necesario en caso de instalación.
|
||||
|
||||
|
||||
# Detectar si es una instalación nueva o una actualización
|
||||
if [ "$1" = "configure" ] && [ -z "$2" ]; then
|
||||
jq --arg apiUrl "$OGCORE_URL" --arg mercureUrl "$OGMERCURE_URL" '.apiUrl = $apiUrl | .mercureUrl = $mercureUrl' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf
|
||||
ln -s $CONFIG_FILE /opt/opengnsys/oggui/etc/config.json
|
||||
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
|
||||
cd /opt/opengnsys/oggui/src/
|
||||
echo NG_APP_BASE_API_URL=$OGCORE_URL > "$CONFIG_FILE"
|
||||
npm install -g @angular/cli
|
||||
npm install
|
||||
/usr/local/bin/ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production --localize=false
|
||||
cp -pr /opt/opengnsys/oggui/src/dist/oggui/browser/* /opt/opengnsys/oggui/browser/
|
||||
md5sum "$CONFIG_FILE" > "$HASH_FILE"
|
||||
ln -s /opt/opengnsys/oggui/etc/systemd/system/oggui.service /etc/systemd/system/oggui.service
|
||||
systemctl daemon-reload
|
||||
systemctl restart nginx
|
||||
systemctl enable oggui
|
||||
elif [ "$1" = "configure" ] && [ -n "$2" ]; then
|
||||
cd /opt/opengnsys/oggui
|
||||
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
|
||||
|
||||
# Cambiar la propiedad de los archivos al usuario especificado
|
||||
chown opengnsys:www-data /opt/opengnsys/
|
||||
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
|
||||
|
|
|
@ -2,16 +2,6 @@
|
|||
|
||||
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
|
||||
USER="opengnsys"
|
||||
HOME_DIR="/opt/opengnsys"
|
||||
|
@ -22,11 +12,4 @@ else
|
|||
useradd -m -d "$HOME_DIR" -s /bin/bash "$USER"
|
||||
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
|
|
@ -3,8 +3,9 @@
|
|||
set -e
|
||||
set -x
|
||||
|
||||
# Solo eliminar archivos de configuración si se está eliminando el paquete
|
||||
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
|
||||
if [ "$1" = "upgrade" ]; then
|
||||
# Eliminar enlaces simbólicos creados en postinst
|
||||
rm -f /etc/systemd/system/oggui.service
|
||||
rm -f /etc/nginx/sites-enabled/oggui.conf
|
||||
systemctl daemon-reload
|
||||
systemctl restart nginx
|
||||
|
|
|
@ -3,7 +3,3 @@ Type: string
|
|||
Default: https://127.0.0.1:8443
|
||||
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
|
||||
|
|
|
@ -5,7 +5,16 @@
|
|||
|
||||
override_dh_auto_build:
|
||||
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:
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -1,2 +1,2 @@
|
|||
# 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_BASE_API_URL=https://127.0.0.1:8443
|
||||
NG_APP_OGCORE_MERCURE_BASE_URL=http://localhost:3000/.well-known/mercure
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
NG_APP_BASE_API_URL=https://localhost:8443
|
||||
NG_APP_OGCORE_MERCURE_BASE_URL=http://localhost:3000/.well-known/mercure
|
|
@ -41,5 +41,3 @@ testem.log
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
test-results/
|
||||
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"ogWebconsole": {
|
||||
"i18n": {
|
||||
"sourceLocale": "es",
|
||||
"locales": {
|
||||
"en": "src/locale/en.json"
|
||||
}
|
||||
},
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
|
@ -24,7 +30,7 @@
|
|||
"builder": "@ngx-env/builder:application",
|
||||
"options": {
|
||||
"baseHref": "/oggui/",
|
||||
"localize": false,
|
||||
"localize": true,
|
||||
"aot": true,
|
||||
"outputPath": "dist/og-webconsole",
|
||||
"index": "src/index.html",
|
||||
|
@ -35,23 +41,20 @@
|
|||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "src/locale",
|
||||
"output": "/locale"
|
||||
}
|
||||
],
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "src/locale",
|
||||
"output": "/locale"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/custom-theme.scss",
|
||||
"src/styles.css",
|
||||
"node_modules/ngx-toastr/toastr.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"rfdc"
|
||||
]
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
@ -63,7 +66,7 @@
|
|||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "7kb",
|
||||
"maximumWarning": "4kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
],
|
||||
|
@ -73,6 +76,16 @@
|
|||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": false
|
||||
},
|
||||
"es": {
|
||||
"localize": [
|
||||
"es-ES"
|
||||
]
|
||||
},
|
||||
"en": {
|
||||
"localize": [
|
||||
"en-US"
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
|
@ -91,16 +104,29 @@
|
|||
},
|
||||
"development": {
|
||||
"buildTarget": "ogWebconsole:build:development"
|
||||
},
|
||||
"es": {
|
||||
"buildTarget": "ogWebconsole:build:es"
|
||||
},
|
||||
"en": {
|
||||
"buildTarget": "ogWebconsole:build:en"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@ngx-env/builder:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "ogWebconsole:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@ngx-env/builder:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
"zone.js/testing",
|
||||
"@angular/localize/init"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
|
@ -120,4 +146,4 @@
|
|||
"cli": {
|
||||
"analytics": "95fac95c-8936-41a8-8c9c-1fae82fe6912"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,26 +3,30 @@ import { RouterModule, Routes } from '@angular/router';
|
|||
import { MainLayoutComponent } from './layout/main-layout/main-layout.component';
|
||||
import { AuthLayoutComponent } from './layout/auth-layout/auth-layout.component';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { DashboardComponent } from './components/dashboard/dashboard.component';
|
||||
import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component';
|
||||
import { AdminComponent } from './components/admin/admin.component';
|
||||
import { UsersComponent } from './components/admin/users/users/users.component';
|
||||
import { RolesComponent } from './components/admin/roles/roles/roles.component';
|
||||
import { GroupsComponent } from './components/groups/groups.component';
|
||||
import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.component';
|
||||
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
|
||||
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
|
||||
import { OgbootStatusComponent } from "./components/ogboot/ogboot-status/ogboot-status.component";
|
||||
import {OgbootStatusComponent} from "./components/ogboot/ogboot-status/ogboot-status.component";
|
||||
import { CalendarComponent } from "./components/calendar/calendar.component";
|
||||
import { CommandsComponent } from './components/commands/main-commands/commands.component';
|
||||
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
|
||||
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
|
||||
import { TaskLogsComponent } from './components/task-logs/task-logs.component';
|
||||
import { SoftwareComponent } from "./components/software/software.component";
|
||||
import { SoftwareProfileComponent } from "./components/software-profile/software-profile.component";
|
||||
import { OperativeSystemComponent } from "./components/operative-system/operative-system.component";
|
||||
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
|
||||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import {SoftwareComponent} from "./components/software/software.component";
|
||||
import {SoftwareProfileComponent} from "./components/software-profile/software-profile.component";
|
||||
import {OperativeSystemComponent} from "./components/operative-system/operative-system.component";
|
||||
import {
|
||||
PartitionAssistantComponent
|
||||
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
|
||||
import { RepositoriesComponent } from "./components/repositories/repositories.component";
|
||||
import {RepositoriesComponent} from "./components/repositories/repositories.component";
|
||||
import {
|
||||
CreateClientImageComponent
|
||||
} from "./components/groups/components/client-main-view/create-image/create-image.component";
|
||||
|
@ -32,52 +36,49 @@ import {
|
|||
import {
|
||||
MainRepositoryViewComponent
|
||||
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
||||
import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component";
|
||||
import { MenusComponent } from "./components/menus/menus.component";
|
||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import { StatusComponent } from "./components/ogdhcp/status/status.component";
|
||||
import {
|
||||
RunScriptAssistantComponent
|
||||
} from "./components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component";
|
||||
import { roleGuard } from './guards/role.guard';
|
||||
import { LogoutGuard } from './guards/logout.guard';
|
||||
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
|
||||
import {MenusComponent} from "./components/menus/menus.component";
|
||||
import {OgDhcpSubnetsComponent} from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import {StatusComponent} from "./components/ogdhcp/status/status.component";
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||
{
|
||||
path: '', component: MainLayoutComponent,
|
||||
{ path: '', component: MainLayoutComponent,
|
||||
children: [
|
||||
{ path: 'users', component: UsersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'env-vars', component: EnvVarsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'roles', component: RolesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'admin', component: AdminComponent },
|
||||
{ path: 'users', component: UsersComponent },
|
||||
{ path: 'env-vars', component: EnvVarsComponent },
|
||||
{ path: 'user-groups', component: RolesComponent },
|
||||
{ path: 'groups', component: GroupsComponent },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'pxe', component: PxeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'ogdhcp-status', component: StatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/run-script', component: RunScriptAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'repositories', component: RepositoriesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'software', component: SoftwareComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'software-profiles', component: SoftwareProfileComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'operative-systems', component: OperativeSystemComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'menus', component: MenusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent },
|
||||
{ path: 'pxe', component: PxeComponent },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent },
|
||||
{ path: 'ogdhcp-status', component: StatusComponent },
|
||||
{ path: 'commands', component: CommandsComponent },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent },
|
||||
{ path: 'calendars', component: CalendarComponent },
|
||||
{ path: 'clients/deploy-image', component: DeployImageComponent },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
|
||||
{ path: 'clients/:id', component: ClientMainViewComponent },
|
||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
|
||||
{ path: 'images', component: ImagesComponent },
|
||||
{ path: 'repositories', component: RepositoriesComponent },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||
{ path: 'software', component: SoftwareComponent },
|
||||
{ path: 'software-profiles', component: SoftwareProfileComponent },
|
||||
{ path: 'operative-systems', component: OperativeSystemComponent },
|
||||
{ path: 'menus', component: MenusComponent },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
component: AuthLayoutComponent,
|
||||
children: [
|
||||
{ path: 'login', component: LoginComponent, canActivate: [LogoutGuard] },
|
||||
{ path: 'login', component: LoginComponent },
|
||||
],
|
||||
},
|
||||
{ path: '**', component: PageNotFoundComponent },
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, LOCALE_ID, APP_INITIALIZER } from '@angular/core';
|
||||
import { ConfigService } from './services/config.service';
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
|
@ -17,6 +16,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AdminComponent } from './components/admin/admin.component';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
|
@ -66,7 +66,7 @@ import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.co
|
|||
import { CreatePXEImageComponent } from './components/ogboot/pxe-images/create-image/create-image/create-image.component';
|
||||
import { InfoImageComponent } from './components/ogboot/pxe-images/info-image/info-image/info-image.component';
|
||||
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
|
||||
import { CreatePxeTemplateComponent } from './components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component';
|
||||
import { CreatePxeTemplateComponent } from './components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component';
|
||||
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
|
||||
import { MatExpansionPanel, MatExpansionPanelDescription, MatExpansionPanelTitle } from "@angular/material/expansion";
|
||||
import { OgbootStatusComponent } from './components/ogboot/ogboot-status/ogboot-status.component';
|
||||
|
@ -86,8 +86,9 @@ import { CreateCommandGroupComponent } from './components/commands/commands-grou
|
|||
import { DetailCommandGroupComponent } from './components/commands/commands-groups/detail-command-group/detail-command-group.component';
|
||||
import { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component';
|
||||
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-task.component';
|
||||
import { TaskLogsComponent } from './components/task-logs/task-logs.component';
|
||||
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import { CreateImageComponent } from './components/images/create-image/create-image.component';
|
||||
import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component';
|
||||
|
@ -100,7 +101,7 @@ import { OperativeSystemComponent } from './components/operative-system/operativ
|
|||
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 { 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 { 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';
|
||||
|
@ -116,7 +117,8 @@ import { CreateMultipleClientComponent } from './components/groups/shared/client
|
|||
import { ExportImageComponent } from './components/images/export-image/export-image.component';
|
||||
import { ImportImageComponent } from "./components/repositories/import-image/import-image.component";
|
||||
import { LoadingComponent } from './shared/loading/loading.component';
|
||||
import { InputDialogComponent } from './components/task-logs/input-dialog/input-dialog.component';
|
||||
import { RepositoryImagesComponent } from './components/repositories/repository-images/repository-images.component';
|
||||
import { InputDialogComponent } from './components/commands/commands-task/task-logs/input-dialog/input-dialog.component';
|
||||
import { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
|
||||
import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
|
||||
import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||
|
@ -127,44 +129,10 @@ import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-
|
|||
import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component';
|
||||
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component';
|
||||
import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component';
|
||||
import { ConvertImageComponent } from './components/repositories/convert-image/convert-image.component';
|
||||
import {NgOptimizedImage, 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';
|
||||
import { ClientDetailsComponent } from './components/groups/shared/client-details/client-details.component';
|
||||
import { PartitionTypeOrganizatorComponent } from './components/groups/shared/partition-type-organizator/partition-type-organizator.component';
|
||||
import { CreateTaskScheduleComponent } from './components/commands/commands-task/create-task-schedule/create-task-schedule.component';
|
||||
import { ShowTaskScheduleComponent } from './components/commands/commands-task/show-task-schedule/show-task-schedule.component';
|
||||
import { ShowTaskScriptComponent } from './components/commands/commands-task/show-task-script/show-task-script.component';
|
||||
import { CreateTaskScriptComponent } from './components/commands/commands-task/create-task-script/create-task-script.component';
|
||||
import { ViewParametersModalComponent } from './components/commands/commands-task/show-task-script/view-parameters-modal/view-parameters-modal.component';
|
||||
import { OutputDialogComponent } from './components/task-logs/output-dialog/output-dialog.component';
|
||||
import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs/client-task-logs.component';
|
||||
import { BootSoPartitionComponent } from './components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component';
|
||||
import { RemoveCacheImageComponent } from './components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component';
|
||||
import { ChangeParentComponent } from './components/groups/shared/change-parent/change-parent.component';
|
||||
import { SoftwareProfilePartitionComponent } from './components/commands/main-commands/execute-command/software-profile-partition/software-profile-partition.component';
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
}
|
||||
|
||||
export function initializeApp(configService: ConfigService) {
|
||||
return () => configService.loadConfig();
|
||||
}
|
||||
|
||||
registerLocaleData(localeEs, 'es-ES');
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
@ -173,6 +141,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
HeaderComponent,
|
||||
SidebarComponent,
|
||||
LoginComponent,
|
||||
AdminComponent,
|
||||
MainLayoutComponent,
|
||||
UsersComponent,
|
||||
RolesComponent,
|
||||
|
@ -214,6 +183,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
TaskLogsComponent,
|
||||
ServerInfoDialogComponent,
|
||||
StatusComponent,
|
||||
ClientMainViewComponent,
|
||||
ImagesComponent,
|
||||
CreateImageComponent,
|
||||
PartitionAssistantComponent,
|
||||
|
@ -225,7 +195,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
CreateOperativeSystemComponent,
|
||||
ShowTemplateContentComponent,
|
||||
RepositoriesComponent,
|
||||
ManageRepositoryComponent,
|
||||
CreateRepositoryComponent,
|
||||
ExecuteCommandComponent,
|
||||
ExecuteCommandOuComponent,
|
||||
DeployImageComponent,
|
||||
|
@ -238,34 +208,12 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
ExportImageComponent,
|
||||
ImportImageComponent,
|
||||
LoadingComponent,
|
||||
RepositoryImagesComponent,
|
||||
InputDialogComponent,
|
||||
ManageOrganizationalUnitComponent,
|
||||
BackupImageComponent,
|
||||
ShowClientsComponent,
|
||||
OperationResultDialogComponent,
|
||||
ConvertImageComponent,
|
||||
GlobalStatusComponent,
|
||||
ShowMonoliticImagesComponent,
|
||||
StatusTabComponent,
|
||||
ConvertImageToVirtualComponent,
|
||||
RunScriptAssistantComponent,
|
||||
SaveScriptComponent,
|
||||
EditImageComponent,
|
||||
ShowGitImagesComponent,
|
||||
RenameImageComponent,
|
||||
ClientDetailsComponent,
|
||||
PartitionTypeOrganizatorComponent,
|
||||
CreateTaskScheduleComponent,
|
||||
ShowTaskScheduleComponent,
|
||||
ShowTaskScriptComponent,
|
||||
CreateTaskScriptComponent,
|
||||
ViewParametersModalComponent,
|
||||
OutputDialogComponent,
|
||||
ClientTaskLogsComponent,
|
||||
BootSoPartitionComponent,
|
||||
RemoveCacheImageComponent,
|
||||
ChangeParentComponent,
|
||||
SoftwareProfilePartitionComponent
|
||||
OperationResultDialogComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
@ -314,7 +262,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
progressAnimation: 'increasing',
|
||||
closeButton: true
|
||||
}
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger, NgOptimizedImage
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
|
@ -325,16 +273,8 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
useClass: CustomInterceptor,
|
||||
multi: true
|
||||
},
|
||||
{ provide: LOCALE_ID, useValue: 'es-ES' },
|
||||
provideAnimationsAsync(),
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
ConfigService,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeApp,
|
||||
deps: [ConfigService],
|
||||
multi: true
|
||||
}
|
||||
provideHttpClient(withInterceptorsFromDi())
|
||||
],
|
||||
})
|
||||
export class AppModule { }
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/* Estilos del contenedor para centrar los botones */
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Estilos del contenedor de cada botón y texto */
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
/* Estilos del texto debajo de los botones */
|
||||
span{
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Media query para hacer los botones responsive */
|
||||
@media (max-width: 900px) {
|
||||
button {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
button {
|
||||
height: 90px;
|
||||
width: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
button {
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
span{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<div class="container">
|
||||
<button class="action-button" routerLink="/users">
|
||||
<mat-icon>group</mat-icon>
|
||||
<span>{{ 'labelUsers' | translate }}</span>
|
||||
</button>
|
||||
<button class="action-button" routerLink="/user-groups">
|
||||
<mat-icon>admin_panel_settings</mat-icon>
|
||||
<span>{{ 'labelRoles' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
|
@ -0,0 +1,48 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AdminComponent } from './admin.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
describe('AdminComponent', () => {
|
||||
let component: AdminComponent;
|
||||
let fixture: ComponentFixture<AdminComponent>;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AdminComponent],
|
||||
imports: [
|
||||
RouterTestingModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
TranslateModule.forRoot()
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('debería crear el componente', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('debería renderizar dos botones', () => {
|
||||
const buttons = fixture.nativeElement.querySelectorAll('button');
|
||||
expect(buttons.length).toBe(2);
|
||||
});
|
||||
|
||||
it('debería tener un botón con routerLink a "/users"', () => {
|
||||
const button = fixture.nativeElement.querySelector('button[routerLink="/users"]');
|
||||
expect(button).toBeTruthy();
|
||||
expect(button.querySelector('mat-icon').textContent.trim()).toBe('group');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
templateUrl: './admin.component.html',
|
||||
styleUrl: './admin.component.css'
|
||||
})
|
||||
|
||||
export class AdminComponent {
|
||||
|
||||
}
|
||||
|
|
@ -16,20 +16,12 @@
|
|||
<ng-container matColumnDef="value">
|
||||
<mat-header-cell *matHeaderCellDef> Valor </mat-header-cell>
|
||||
<mat-cell *matCellDef="let variable">
|
||||
<!-- Si es booleano, usamos checkbox -->
|
||||
<mat-checkbox *ngIf="isBoolean(variable.value)"
|
||||
[checked]="variable.value === 'true'"
|
||||
(change)="variable.value = $event.checked ? 'true' : 'false'">
|
||||
</mat-checkbox>
|
||||
|
||||
<!-- Si no es booleano, usamos input -->
|
||||
<mat-form-field *ngIf="!isBoolean(variable.value)" class="value-input">
|
||||
<mat-form-field class="value-input">
|
||||
<input matInput [(ngModel)]="variable.value" />
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
|
@ -38,4 +30,4 @@
|
|||
<button class="action-button" (click)="loadEnvVars()">Recargar</button>
|
||||
<button class="submit-button" (click)="saveEnvVars()">Guardar Cambios</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -13,29 +13,24 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../users/users/data.service';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('EnvVarsComponent', () => {
|
||||
let component: EnvVarsComponent;
|
||||
let fixture: ComponentFixture<EnvVarsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [EnvVarsComponent],
|
||||
declarations: [EnvVarsComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
BrowserAnimationsModule,
|
||||
MatTableModule,
|
||||
MatTableModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
|
@ -52,10 +47,6 @@ describe('EnvVarsComponent', () => {
|
|||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-env-vars',
|
||||
|
@ -9,17 +8,16 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrl: './env-vars.component.css'
|
||||
})
|
||||
export class EnvVarsComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
envVars: { name: string; value: string }[] = [];
|
||||
displayedColumns: string[] = ['name', 'value'];
|
||||
private apiUrl: string;
|
||||
|
||||
private apiUrl = `${this.baseUrl}/env-vars`;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
this.apiUrl = `${this.configService.apiUrl}/env-vars`;
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadEnvVars();
|
||||
|
@ -31,15 +29,12 @@ export class EnvVarsComponent {
|
|||
this.envVars = Object.entries(response.vars).map(([name, value]) => ({ name, value }));
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error al cargar las variables de entorno:', err);
|
||||
this.toastService.error('No se pudieron cargar las variables de entorno.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isBoolean(value: string): boolean {
|
||||
return value === 'true' || value === 'false';
|
||||
}
|
||||
|
||||
saveEnvVars(): void {
|
||||
const vars = this.envVars.reduce((acc, variable) => {
|
||||
acc[variable.name] = variable.value;
|
||||
|
|
|
@ -4,7 +4,6 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {DataService} from "../data.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-role-modal',
|
||||
|
@ -12,9 +11,9 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./add-role-modal.component.css']
|
||||
})
|
||||
export class AddRoleModalComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
roleForm: FormGroup<any>;
|
||||
roleId: string | null = null;
|
||||
baseUrl: string;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<AddRoleModalComponent>,
|
||||
|
@ -22,10 +21,8 @@ export class AddRoleModalComponent {
|
|||
private http: HttpClient,
|
||||
private fb: FormBuilder,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.roleForm = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
superAdmin: [false],
|
||||
|
|
|
@ -2,19 +2,15 @@ import { Injectable } from '@angular/core';
|
|||
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
private apiUrl = `${this.baseUrl}/user-groups?page=1&itemsPerPage=1000`;
|
||||
|
||||
constructor(private http: HttpClient, private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/user-groups?page=1&itemsPerPage=1000`;
|
||||
}
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getUserGroups(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
||||
const params = new HttpParams({ fromObject: filters });
|
||||
|
|
|
@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material/dialog';
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from './data.service';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { of } from 'rxjs';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatLabel } from '@angular/material/form-field';
|
||||
|
@ -20,14 +20,12 @@ describe('RolesComponent', () => {
|
|||
let mockHttpClient: jasmine.SpyObj<HttpClient>;
|
||||
let mockToastrService: jasmine.SpyObj<ToastrService>;
|
||||
let mockDataService: jasmine.SpyObj<DataService>;
|
||||
let mockConfigService: jasmine.SpyObj<ConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
|
||||
const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']);
|
||||
const toastrServiceSpy = jasmine.createSpyObj('ToastrService', ['success', 'error']);
|
||||
const dataServiceSpy = jasmine.createSpyObj('DataService', ['getRoles']);
|
||||
const configServiceSpy = jasmine.createSpyObj('ConfigService', [], { apiUrl: 'http://mock-api-url' });
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RolesComponent, LoadingComponent],
|
||||
|
@ -37,8 +35,7 @@ describe('RolesComponent', () => {
|
|||
{ provide: MatDialog, useValue: matDialogSpy },
|
||||
{ provide: HttpClient, useValue: httpClientSpy },
|
||||
{ provide: ToastrService, useValue: toastrServiceSpy },
|
||||
{ provide: DataService, useValue: dataServiceSpy },
|
||||
{ provide: ConfigService, useValue: configServiceSpy }
|
||||
{ provide: DataService, useValue: dataServiceSpy }
|
||||
]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
@ -50,7 +47,6 @@ describe('RolesComponent', () => {
|
|||
mockHttpClient = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
|
||||
mockToastrService = TestBed.inject(ToastrService) as jasmine.SpyObj<ToastrService>;
|
||||
mockDataService = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
|
||||
mockConfigService = TestBed.inject(ConfigService) as jasmine.SpyObj<ConfigService>;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
|
|
|
@ -7,7 +7,6 @@ import { DataService } from "./data.service";
|
|||
import { PageEvent } from "@angular/material/paginator";
|
||||
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { AddRoleModalComponent } from './add-role-modal/add-role-modal.component';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-roles',
|
||||
|
@ -15,7 +14,7 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./roles.component.css']
|
||||
})
|
||||
export class RolesComponent implements OnInit {
|
||||
baseUrl: string = this.configService.apiUrl;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
filters: { [key: string]: string } = {};
|
||||
loading: boolean = false;
|
||||
|
@ -49,8 +48,7 @@ export class RolesComponent implements OnInit {
|
|||
public dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService
|
||||
private toastService: ToastrService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
|
|
|
@ -4,7 +4,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|||
import { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { DataService } from "../data.service";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
interface UserGroup {
|
||||
'@id': string;
|
||||
|
@ -18,7 +17,7 @@ interface UserGroup {
|
|||
styleUrls: ['./add-user-modal.component.css']
|
||||
})
|
||||
export class AddUserModalComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
@Output() userAdded = new EventEmitter<void>();
|
||||
@Output() userEdited = new EventEmitter<void>();
|
||||
userForm: FormGroup<any>;
|
||||
|
@ -39,10 +38,8 @@ export class AddUserModalComponent implements OnInit {
|
|||
private http: HttpClient,
|
||||
private fb: FormBuilder,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.userForm = this.fb.group({
|
||||
username: ['', Validators.required],
|
||||
password: ['', Validators.required],
|
||||
|
|
|
@ -3,19 +3,15 @@ import { Injectable } from '@angular/core';
|
|||
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
private apiUrl = `${this.baseUrl}/users?page=1&itemsPerPage=1000`;
|
||||
|
||||
constructor(private http: HttpClient, private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/users?page=1&itemsPerPage=1000`;
|
||||
}
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getUsers(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
||||
const params = new HttpParams({ fromObject: filters });
|
||||
|
|
|
@ -7,7 +7,6 @@ import { ToastrService } from 'ngx-toastr';
|
|||
import { of } from 'rxjs';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
class MockToastrService {
|
||||
success() {}
|
||||
|
@ -19,11 +18,6 @@ describe('UsersComponent', () => {
|
|||
let fixture: ComponentFixture<UsersComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [UsersComponent],
|
||||
imports: [
|
||||
|
@ -34,7 +28,6 @@ describe('UsersComponent', () => {
|
|||
],
|
||||
providers: [
|
||||
{ provide: ToastrService, useClass: MockToastrService },
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA], // Ignorar elementos desconocidos
|
||||
}).compileComponents();
|
||||
|
|
|
@ -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 { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { DataService } from "./data.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-users',
|
||||
|
@ -13,8 +13,7 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./users.component.css']
|
||||
})
|
||||
export class UsersComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
filters: { [key: string]: string } = {};
|
||||
loading: boolean = false;
|
||||
|
@ -51,15 +50,14 @@ export class UsersComponent implements OnInit {
|
|||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
|
||||
private apiUrl = `${this.baseUrl}/users`;
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private configService: ConfigService,
|
||||
private http: HttpClient,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/users`;
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.search();
|
||||
|
|
|
@ -16,18 +16,12 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
|||
import { JoyrideModule, JoyrideService } from 'ngx-joyride';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { LoadingComponent } from '../../shared/loading/loading.component';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('CalendarComponent', () => {
|
||||
let component: CalendarComponent;
|
||||
let fixture: ComponentFixture<CalendarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CalendarComponent, LoadingComponent],
|
||||
imports: [
|
||||
|
@ -47,9 +41,6 @@ describe('CalendarComponent', () => {
|
|||
JoyrideModule.forRoot(),
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import { PageEvent } from "@angular/material/paginator";
|
|||
import { CreateCalendarComponent } from "./create-calendar/create-calendar.component";
|
||||
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-calendar',
|
||||
|
@ -17,8 +16,7 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrl: './calendar.component.css'
|
||||
})
|
||||
export class CalendarComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
images: { downloadUrl: string; name: string; uuid: string }[] = [];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
length: number = 0;
|
||||
|
@ -54,18 +52,15 @@ export class CalendarComponent implements OnInit {
|
|||
}
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
private apiUrl = `${this.baseUrl}/remote-calendars`;
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
private joyrideService: JoyrideService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/remote-calendars`;
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
|
@ -170,7 +165,7 @@ export class CalendarComponent implements OnInit {
|
|||
this.joyrideService.startTour({
|
||||
steps: ['titleStep', 'addButtonStep', 'searchStep', 'tableStep', 'actionsStep'],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,12 +22,7 @@
|
|||
|
||||
.time-fields {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.hour-fields {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
gap: 15px; /* Espacio entre los campos */
|
||||
}
|
||||
|
||||
.time-field {
|
||||
|
@ -39,74 +34,4 @@
|
|||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.custom-text {
|
||||
font-style: italic;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
.weekday-toggle-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 40px 0 40px 0;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.weekday-toggle {
|
||||
flex: 1 1 calc(14.28% - 10px);
|
||||
padding: 10px 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f5f5f5;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.weekday-toggle.selected {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
|
||||
.availability-summary {
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
padding: 12px 16px;
|
||||
margin-top: 16px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
color: #0d47a1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.unavailability-summary {
|
||||
background-color: #ffebee;
|
||||
border-left: 4px solid #d32f2f;
|
||||
padding: 12px 16px;
|
||||
margin-top: 16px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
color: #b71c1c;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
|
@ -1,26 +1,24 @@
|
|||
<h2 mat-dialog-title>{{ isEditMode ? ('editCalendar' | translate) : ('addCalendar' | translate) }}</h2>
|
||||
<mat-dialog-content class="form-container">
|
||||
<mat-checkbox [(ngModel)]="isRemoteAvailable">
|
||||
<mat-slide-toggle [(ngModel)]="isRemoteAvailable" class="example-margin">
|
||||
{{ 'remoteAvailability' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-divider style="margin: 10px 0;"></mat-divider>
|
||||
</mat-slide-toggle>
|
||||
|
||||
<div *ngIf="!isRemoteAvailable" class="form-group">
|
||||
<mat-label>{{ 'selectWeekDays' | translate }}</mat-label>
|
||||
<p class="custom-text"> (Los dias y horas seleccionados se marcarán como aula no disponible para remote pc.) </p>
|
||||
<div class="weekday-toggle-group full-width">
|
||||
<button
|
||||
*ngFor="let day of weekDays"
|
||||
type="button"
|
||||
class="weekday-toggle"
|
||||
[class.selected]="busyWeekDays[day]"
|
||||
(click)="busyWeekDays[day] = !busyWeekDays[day]">
|
||||
{{ day.slice(0, 3) }}
|
||||
</button>
|
||||
<div class="row">
|
||||
<div class="col-md-6 checkbox-group">
|
||||
<mat-checkbox *ngFor="let day of weekDays.slice(0, (weekDays.length / 2) + 1)" [(ngModel)]="busyWeekDays[day]">
|
||||
{{ day }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-md-6 checkbox-group">
|
||||
<mat-checkbox *ngFor="let day of weekDays.slice(weekDays.length / 2 + 1)" [(ngModel)]="busyWeekDays[day]">
|
||||
{{ day }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="time-fields">
|
||||
<mat-form-field appearance="fill" class="time-field">
|
||||
<mat-label>{{ 'startTime' | translate }}</mat-label>
|
||||
|
@ -32,24 +30,12 @@
|
|||
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="unavailability-summary" *ngIf="busyFromHour && busyToHour && busyWeekDays && getSelectedDays().length">
|
||||
<mat-icon style="width: 50px;" color="warn">block</mat-icon>
|
||||
<span class="summary-text">
|
||||
El aula estará <strong>no disponible</strong> para Remote PC los días:
|
||||
<strong>{{ getSelectedDays().join(', ') }}</strong> de
|
||||
<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRemoteAvailable" class="form-group">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'reasonLabel' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="availableReason" placeholder="{{ 'reasonPlaceholder' | translate }}" [required]="isRemoteAvailable">
|
||||
<mat-hint>Razón por la cual el aula SI está disponible para su uso en Remote PC</mat-hint>
|
||||
</mat-form-field>
|
||||
<div class="time-fields">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
|
@ -67,32 +53,6 @@
|
|||
<mat-datepicker #picker2></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="hour-fields">
|
||||
<mat-form-field appearance="fill" class="time-field">
|
||||
<mat-label>{{ 'startTime' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="busyFromHour" type="time" placeholder="{{ 'startTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="time-field">
|
||||
<mat-label>{{ 'endTime' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="availability-summary" *ngIf="availableFromDate && availableToDate">
|
||||
<mat-icon color="primary" style="width: 50px;">info</mat-icon>
|
||||
<span class="summary-text">
|
||||
El aula estará <strong>disponible</strong> para reserva desde el
|
||||
<strong>{{ availableFromDate | date:'fullDate' }}</strong> hasta el
|
||||
<strong>{{ availableToDate | date:'fullDate' }}</strong>
|
||||
<span *ngIf="busyFromHour && busyToHour">
|
||||
en el horario de <strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Component, Inject } from '@angular/core';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-calendar-rule',
|
||||
|
@ -10,7 +9,7 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrl: './create-calendar-rule.component.css'
|
||||
})
|
||||
export class CreateCalendarRuleComponent {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
name: string = '';
|
||||
remoteCalendarRules: any[] = [];
|
||||
weekDays: string[] = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
|
||||
|
@ -30,23 +29,20 @@ export class CreateCalendarRuleComponent {
|
|||
constructor(
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService,
|
||||
public dialogRef: MatDialogRef<CreateCalendarRuleComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.calendarId = this.data.calendar
|
||||
if (this.data) {
|
||||
this.isEditMode = true;
|
||||
this.availableFromDate = this.data.rule ? this.data.rule.availableFromDate : null;
|
||||
this.availableToDate = this.data.rule ? this.data.rule.availableToDate : null;
|
||||
this.isRemoteAvailable = this.data.rule ? this.data.rule.isRemoteAvailable : false;
|
||||
this.availableReason = this.data.rule ? this.data.rule.availableReason : null;
|
||||
this.busyFromHour = this.data.rule ? this.data.rule.busyFromHour : null;
|
||||
this.busyToHour = this.data.rule ? this.data.rule.busyToHour : null;
|
||||
this.availableFromDate = this.data.rule? this.data.rule.availableFromDate : null;
|
||||
this.availableToDate = this.data.rule? this.data.rule.availableToDate : null;
|
||||
this.isRemoteAvailable = this.data.rule? this.data.rule.isRemoteAvailable : false;
|
||||
this.availableReason = this.data.rule? this.data.rule.availableReason : null;
|
||||
this.busyFromHour = this.data.rule? this.data.rule.busyFromHour : null;
|
||||
this.busyToHour = this.data.rule? this.data.rule.busyToHour : null;
|
||||
if (this.data.rule && this.data.rule.busyWeekDays) {
|
||||
this.busyWeekDays = this.data.rule.busyWeekDays.reduce((acc: {
|
||||
[x: string]: boolean;
|
||||
|
@ -64,8 +60,8 @@ export class CreateCalendarRuleComponent {
|
|||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
getSelectedDays(): string[] {
|
||||
return Object.keys(this.busyWeekDays || {}).filter(day => this.busyWeekDays[day]);
|
||||
toggleAdditionalForm(): void {
|
||||
this.showAdditionalForm = !this.showAdditionalForm;
|
||||
}
|
||||
|
||||
getSelectedDaysIndices() {
|
||||
|
@ -74,11 +70,6 @@ export class CreateCalendarRuleComponent {
|
|||
.filter(index => index !== -1);
|
||||
}
|
||||
|
||||
convertDateToLocalISO(date: Date): string {
|
||||
const adjustedDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
return adjustedDate.toISOString();
|
||||
}
|
||||
|
||||
submitRule(): void {
|
||||
this.getSelectedDaysIndices()
|
||||
const selectedDaysArray = Object.keys(this.busyWeekDays).map((day, index) => this.busyWeekDays[index]);
|
||||
|
@ -88,8 +79,8 @@ export class CreateCalendarRuleComponent {
|
|||
busyWeekDays: this.selectedDaysIndices,
|
||||
busyFromHour: this.busyFromHour,
|
||||
busyToHour: this.busyToHour,
|
||||
availableFromDate: this.availableFromDate ? this.convertDateToLocalISO(this.availableFromDate) : null,
|
||||
availableToDate: this.availableToDate ? this.convertDateToLocalISO(this.availableToDate) : null,
|
||||
availableFromDate: this.availableFromDate,
|
||||
availableToDate: this.availableToDate,
|
||||
isRemoteAvailable: this.isRemoteAvailable,
|
||||
availableReason: this.availableReason
|
||||
};
|
||||
|
@ -98,7 +89,7 @@ export class CreateCalendarRuleComponent {
|
|||
this.http.put(`${this.baseUrl}${this.ruleId}`, formData)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Calendar rule updated successfully');
|
||||
this.toastService.success('Calendar updated successfully');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
|
@ -110,7 +101,7 @@ export class CreateCalendarRuleComponent {
|
|||
this.http.post(`${this.baseUrl}/remote-calendar-rules`, formData)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Calendar rule created successfully');
|
||||
this.toastService.success('Calendar created successfully');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
.time-fields {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
gap: 15px; /* Espacio entre los campos */
|
||||
}
|
||||
|
||||
.time-field {
|
||||
|
@ -34,25 +34,24 @@
|
|||
|
||||
.list-item-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: flex-start; /* Alinea el contenido al inicio */
|
||||
justify-content: space-between; /* Espacio entre los textos y los íconos */
|
||||
width: 100%; /* Asegúrate de que el contenido ocupe todo el ancho */
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex-grow: 1;
|
||||
margin-right: 16px;
|
||||
flex-grow: 1; /* Permite que este contenedor ocupe el espacio disponible */
|
||||
margin-right: 16px; /* Espaciado a la derecha para separar de los íconos */
|
||||
margin-left: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: center; /* Alinea los íconos verticalmente */
|
||||
}
|
||||
|
||||
.right-icon {
|
||||
margin-left: 8px;
|
||||
margin-left: 8px; /* Espaciado entre los íconos */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -61,15 +60,4 @@
|
|||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.rule-available {
|
||||
background-color: #e8f5e9;
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
.rule-unavailable {
|
||||
background-color: #ffebee;
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
<input matInput [(ngModel)]="name" required>
|
||||
<mat-icon *ngIf="isEditMode" matSuffix (click)="submitForm()">mode_edit</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div *ngIf="isEditMode" mat-subheader>{{ 'rulesHeader' | translate }}</div>
|
||||
<button class="action-button" *ngIf="isEditMode" (click)="createRule()" style="padding: 10px;">
|
||||
|
@ -18,20 +18,14 @@
|
|||
|
||||
<mat-list *ngIf="isEditMode">
|
||||
<ng-container *ngFor="let rule of remoteCalendarRules;">
|
||||
<mat-list-item
|
||||
[ngClass]="{
|
||||
'rule-available': rule.isRemoteAvailable,
|
||||
'rule-unavailable': !rule.isRemoteAvailable
|
||||
}"
|
||||
>
|
||||
<mat-list-item>
|
||||
<div class="list-item-content">
|
||||
<mat-icon matListItemIcon>event_available</mat-icon>
|
||||
<div class="text-content">
|
||||
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('remotePcStatusAvailable' | translate) : ('remotePcStatusUnavailable' | translate) }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">Días: <strong>{{ rule.busyWeekDaysMap }}</strong></div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">Razón: {{ rule.availableReason }}</div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">Días: <strong>{{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</strong></div>
|
||||
<div matListItemLine>Horario: {{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
|
||||
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('statusAvailable' | translate) : ('statusUnavailable' | translate) }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyWeekDaysMap }}</div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">{{ rule.availableReason }} | {{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</div>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
<button mat-icon-button color="primary" class="right-icon" (click)="createRule(rule)">
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dialog";
|
||||
import { CreateCalendarRuleComponent } from "../create-calendar-rule/create-calendar-rule.component";
|
||||
import { DataService } from "../data.service";
|
||||
import { DeleteModalComponent } from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {CreateCalendarRuleComponent} from "../create-calendar-rule/create-calendar-rule.component";
|
||||
import {DataService} from "../data.service";
|
||||
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-calendar',
|
||||
|
@ -13,7 +12,7 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrl: './create-calendar.component.css'
|
||||
})
|
||||
export class CreateCalendarComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
name: string = '';
|
||||
remoteCalendarRules: any[] = [];
|
||||
isEditMode: boolean = false;
|
||||
|
@ -23,14 +22,11 @@ export class CreateCalendarComponent implements OnInit {
|
|||
constructor(
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService,
|
||||
public dialogRef: MatDialogRef<CreateCalendarComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
public dialog: MatDialog,
|
||||
private dataService: DataService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
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 { catchError, map } from 'rxjs/operators';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
private apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=1000`;
|
||||
|
||||
constructor(private http: HttpClient, private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=1000`;
|
||||
}
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getRemoteCalendars(filters: { [key: string]: string }): Observable<any[]> {
|
||||
const params = new HttpParams({ fromObject: filters });
|
||||
|
|
|
@ -33,6 +33,15 @@
|
|||
<ng-container *ngIf="column.columnDef !== 'commands'">
|
||||
{{ column.cell(commandGroup) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'commands'" joyrideStep="viewCommandsStep" text="{{ 'viewCommandsStepText' | translate }}">
|
||||
<button class="action-button" [matMenuTriggerFor]="menu">{{ 'viewCommands' | translate }}</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of commandGroup.commands">
|
||||
{{ command.name }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/
|
|||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-commands-groups',
|
||||
|
@ -16,8 +15,7 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./commands-groups.component.css']
|
||||
})
|
||||
export class CommandsGroupsComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
filters: { [key: string]: string | boolean } = {};
|
||||
length: number = 0;
|
||||
|
@ -49,12 +47,10 @@ export class CommandsGroupsComponent implements OnInit {
|
|||
}
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
private apiUrl = `${this.baseUrl}/command-groups`;
|
||||
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private configService: ConfigService, private joyrideService: JoyrideService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-groups`;
|
||||
}
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
|
@ -124,17 +120,17 @@ export class CommandsGroupsComponent implements OnInit {
|
|||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleStep',
|
||||
'addCommandGroupStep',
|
||||
'searchStep',
|
||||
'tableStep',
|
||||
'viewCommandsStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
'titleStep',
|
||||
'addCommandGroupStep',
|
||||
'searchStep',
|
||||
'tableStep',
|
||||
'viewCommandsStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-command-group',
|
||||
|
@ -11,25 +10,21 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./create-command-group.component.css']
|
||||
})
|
||||
export class CreateCommandGroupComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
availableCommands: any[] = [];
|
||||
selectedCommands: any[] = [];
|
||||
groupName: string = '';
|
||||
enabled: boolean = true;
|
||||
editing: boolean = false;
|
||||
loading: boolean = false;
|
||||
private apiUrl: string;
|
||||
private apiUrl = `${this.baseUrl}/commands`;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private dialogRef: MatDialogRef<CreateCommandGroupComponent>,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/commands`;
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadAvailableCommands();
|
||||
|
|
|
@ -3,7 +3,6 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-detail-command-group',
|
||||
|
@ -11,7 +10,7 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./detail-command-group.component.css']
|
||||
})
|
||||
export class DetailCommandGroupComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
form!: FormGroup;
|
||||
clients: any[] = [];
|
||||
showClientSelect = false;
|
||||
|
@ -22,12 +21,9 @@ export class DetailCommandGroupComponent implements OnInit {
|
|||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private dialogRef: MatDialogRef<DetailCommandGroupComponent>,
|
||||
private fb: FormBuilder,
|
||||
private configService: ConfigService,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.fb.group({
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
|
@ -25,28 +23,36 @@
|
|||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let task">
|
||||
<ng-container *ngIf="column.columnDef !== 'management'">
|
||||
{{ column.cell(task) }}
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="taskid">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'idColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.id }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'management'">
|
||||
<button class="action-button" (click)="openShowScheduleDialog(task)"> Programaciones</button>
|
||||
<button class="action-button" style="margin-left: 0.5vw;" (click)="openShowScriptDialog(task)">Acciones</button>
|
||||
</ng-container>
|
||||
</td>
|
||||
<ng-container matColumnDef="notes">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'infoColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.notes }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'createdByColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.createdBy }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="scheduledDate">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'executionDateColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.dateTime | date:'short' }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="enabled">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'statusColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.enabled ? ('enabled' | translate) : ('disabled' | translate) }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let task" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<button mat-icon-button color="primary" (click)="manageScheduleAction(task)">
|
||||
<mat-icon>watch</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="manageScriptAction(task)">
|
||||
<mat-icon>code-blocks</mat-icon>
|
||||
<button mat-icon-button color="info" (click)="viewTaskDetails(task)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="editTask(task)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
|
|
|
@ -13,16 +13,11 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { LoadingComponent } from '../../../shared/loading/loading.component';
|
||||
import { JoyrideModule, JoyrideService, JoyrideStepService } from 'ngx-joyride';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('CommandsTaskComponent', () => {
|
||||
let component: CommandsTaskComponent;
|
||||
let fixture: ComponentFixture<CommandsTaskComponent>;
|
||||
let mockConfigService: jasmine.SpyObj<ConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const configServiceSpy = jasmine.createSpyObj('ConfigService', [], { apiUrl: 'http://mock-api-url' });
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
|
@ -38,13 +33,8 @@ describe('CommandsTaskComponent', () => {
|
|||
JoyrideModule.forRoot(),
|
||||
],
|
||||
declarations: [CommandsTaskComponent, LoadingComponent],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configServiceSpy }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
mockConfigService = TestBed.inject(ConfigService) as jasmine.SpyObj<ConfigService>;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -6,14 +6,6 @@ import { CreateTaskComponent } from './create-task/create-task.component';
|
|||
import { DetailTaskComponent } from './detail-task/detail-task.component';
|
||||
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {CreateTaskScheduleComponent} from "./create-task-schedule/create-task-schedule.component";
|
||||
import {ShowClientsComponent} from "../../ogdhcp/show-clients/show-clients.component";
|
||||
import {Subnet} from "../../ogdhcp/og-dhcp-subnets.component";
|
||||
import {ShowTaskScheduleComponent} from "./show-task-schedule/show-task-schedule.component";
|
||||
import {ShowTaskScriptComponent} from "./show-task-script/show-task-script.component";
|
||||
import {CreateTaskScriptComponent} from "./create-task-script/create-task-script.component";
|
||||
import {DatePipe} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-commands-task',
|
||||
|
@ -21,34 +13,19 @@ import {DatePipe} from "@angular/common";
|
|||
styleUrls: ['./commands-task.component.css']
|
||||
})
|
||||
export class CommandsTaskComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
tasks: any[] = [];
|
||||
filters: { [key: string]: string | boolean } = {};
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 10;
|
||||
page: number = 1;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
|
||||
columns = [
|
||||
{ columnDef: 'id', header: 'ID', cell: (task: any) => task.id },
|
||||
{ columnDef: 'name', header: 'Nombre de tarea', cell: (task: any) => task.name },
|
||||
{ columnDef: 'organizationalUnit', header: 'Ámbito', cell: (task: any) => task.organizationalUnit.name },
|
||||
{ columnDef: 'management', header: 'Gestiones', cell: (task: any) => task.schedules },
|
||||
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss', 'UTC') },
|
||||
{ columnDef: 'createdBy', header: 'Creado por', cell: (task: any) => task.createdBy },
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'name', 'organizationalUnit', 'management', 'nextExecution', 'createdBy', 'actions'];
|
||||
displayedColumns: string[] = ['taskid', 'notes', 'name', 'scheduledDate', 'enabled', 'actions'];
|
||||
loading: boolean = false;
|
||||
private apiUrl: string;
|
||||
private apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
private joyrideService: JoyrideService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
}
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTasks();
|
||||
|
@ -74,25 +51,24 @@ export class CommandsTaskComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
viewTaskDetails(task: any): void {
|
||||
this.dialog.open(DetailTaskComponent, {
|
||||
width: '800px',
|
||||
data: { task },
|
||||
}).afterClosed().subscribe(() => this.loadTasks());
|
||||
}
|
||||
|
||||
openCreateTaskModal(): void {
|
||||
this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
})
|
||||
}).afterClosed().subscribe(() => this.loadTasks());
|
||||
}
|
||||
|
||||
editTask(task: any): void {
|
||||
this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
data: { task },
|
||||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
})
|
||||
}).afterClosed().subscribe(() => this.loadTasks());
|
||||
}
|
||||
|
||||
deleteTask(task: any): void {
|
||||
|
@ -114,66 +90,12 @@ export class CommandsTaskComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
manageScheduleAction(task: any): void {
|
||||
this.dialog.open(CreateTaskScheduleComponent, {
|
||||
width: '800px',
|
||||
data: { task },
|
||||
}).afterClosed().subscribe( result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
manageScriptAction(task: any): void {
|
||||
this.dialog.open(CreateTaskScriptComponent, {
|
||||
width: '900px',
|
||||
data: { task },
|
||||
}).afterClosed().subscribe( result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex + 1;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
openShowScheduleDialog(commandTask: any) {
|
||||
const dialogRef = this.dialog.open(ShowTaskScheduleComponent, {
|
||||
width: '85vw',
|
||||
height: '85vh',
|
||||
maxWidth: '85vw',
|
||||
maxHeight: '85vh',
|
||||
data: { commandTask: commandTask }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openShowScriptDialog(commandTask: any) {
|
||||
const dialogRef = this.dialog.open(ShowTaskScriptComponent, {
|
||||
width: '85vw',
|
||||
height: '85vh',
|
||||
maxWidth: '85vw',
|
||||
maxHeight: '85vh',
|
||||
data: { commandTask: commandTask }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
|
@ -188,5 +110,5 @@ export class CommandsTaskComponent implements OnInit {
|
|||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
.dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.task-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-time {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.w-half {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.weekday-toggle-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.weekday-toggle {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #f5f5f5;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.weekday-toggle.selected {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
.month-toggle-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.month-toggle-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.month-toggle {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
min-width: 48px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.month-toggle.selected {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: #f9fafb;
|
||||
border-left: 5px solid #3f51b5;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.summary-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
padding: 12px 16px;
|
||||
margin-top: 16px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
color: #0d47a1;
|
||||
line-height: 1.4;
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
<h2 mat-dialog-title class="dialog-title">Programar accion</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form">
|
||||
|
||||
<mat-form-field appearance="fill" class="w-full">
|
||||
<mat-label>Repetición</mat-label>
|
||||
<mat-select formControlName="recurrenceType">
|
||||
<mat-option *ngFor="let type of recurrenceTypes" [value]="type">{{ type | titlecase }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="w-full" *ngIf="form.get('recurrenceType')?.value === 'none'">
|
||||
<mat-label>Fecha de ejecución</mat-label>
|
||||
<input matInput [matDatepicker]="oneTimePicker" formControlName="executionDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="oneTimePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #oneTimePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="w-full">
|
||||
<mat-label>Hora</mat-label>
|
||||
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Mostrar solo si no es 'none' -->
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
|
||||
<label>Días de la semana:</label>
|
||||
<div class="weekday-toggle-group">
|
||||
<button
|
||||
*ngFor="let day of weekDays"
|
||||
type="button"
|
||||
class="weekday-toggle"
|
||||
[class.selected]="selectedDays[day]"
|
||||
(click)="toggleDay(day)">
|
||||
{{ day.slice(0, 3) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selección de meses -->
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
|
||||
<label>Meses:</label>
|
||||
<div class="month-toggle-row" *ngFor="let row of monthRows">
|
||||
<button
|
||||
*ngFor="let month of row"
|
||||
type="button"
|
||||
class="month-toggle"
|
||||
[class.selected]="selectedMonths[month]"
|
||||
(click)="toggleMonth(month)">
|
||||
{{ month.slice(0, 3) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rango de fechas -->
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
|
||||
<mat-form-field appearance="fill" class="w-half">
|
||||
<mat-label>Desde</mat-label>
|
||||
<input matInput [matDatepicker]="fromPicker" formControlName="initDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="w-half">
|
||||
<mat-label>Hasta</mat-label>
|
||||
<input matInput [matDatepicker]="toPicker" formControlName="endDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-checkbox formControlName="enabled">Activar tarea</mat-checkbox>
|
||||
|
||||
<mat-card *ngIf="summaryText" class="summary-card">
|
||||
<mat-icon color="primary" style="width: 50px;">info</mat-icon>
|
||||
<span class="summary-text">
|
||||
{{ summaryText }}
|
||||
</span>
|
||||
</mat-card>
|
||||
</form>
|
||||
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" >{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,100 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreateTaskScheduleComponent } from './create-task-schedule.component';
|
||||
import {LoadingComponent} from "../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule} from "@angular/common/http/testing";
|
||||
import {ToastrModule} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule, TranslateService} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {MatDatepickerModule} from "@angular/material/datepicker";
|
||||
import {MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
import {
|
||||
DateAdapter,
|
||||
MAT_DATE_FORMATS,
|
||||
MAT_NATIVE_DATE_FORMATS,
|
||||
MatNativeDateModule,
|
||||
provideNativeDateAdapter
|
||||
} from "@angular/material/core";
|
||||
import {MatCheckboxModule} from "@angular/material/checkbox";
|
||||
|
||||
describe('CreateTaskScheduleComponent', () => {
|
||||
let component: CreateTaskScheduleComponent;
|
||||
let fixture: ComponentFixture<CreateTaskScheduleComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CreateTaskScheduleComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatTreeModule,
|
||||
MatDatepickerModule,
|
||||
MatButtonToggleModule,
|
||||
MatNativeDateModule,
|
||||
MatCheckboxModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: MAT_NATIVE_DATE_FORMATS },
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreateTaskScheduleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,203 +0,0 @@
|
|||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {FormBuilder, FormGroup} from "@angular/forms";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-task-schedule',
|
||||
templateUrl: './create-task-schedule.component.html',
|
||||
styleUrl: './create-task-schedule.component.css'
|
||||
})
|
||||
export class CreateTaskScheduleComponent implements OnInit{
|
||||
form: FormGroup;
|
||||
baseUrl: string;
|
||||
apiUrl: string;
|
||||
recurrenceTypes = ['none', 'custom'];
|
||||
weekDays: string[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
isSingleDateSelected: boolean = true;
|
||||
monthsList: string[] = [
|
||||
'january', 'february', 'march', 'april', 'may', 'june',
|
||||
'july', 'august', 'september', 'october', 'november', 'december'
|
||||
];
|
||||
|
||||
monthRows: string[][] = [];
|
||||
editing: boolean = false;
|
||||
selectedMonths: { [key: string]: boolean } = {};
|
||||
selectedDays: { [key: string]: boolean } = {};
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
public dialogRef: MatDialogRef<CreateTaskScheduleComponent>,
|
||||
private http: HttpClient,
|
||||
private toastr: ToastrService,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-task-schedules`;
|
||||
this.form = this.fb.group({
|
||||
executionDate: [new Date()],
|
||||
executionTime: ['08:00'],
|
||||
recurrenceType: ['none'],
|
||||
recurrenceDetails: this.fb.group({
|
||||
daysOfWeek: [[]],
|
||||
months: this.fb.control([]),
|
||||
initDate: [null],
|
||||
endDate: [null]
|
||||
}),
|
||||
enabled: [true]
|
||||
});
|
||||
|
||||
if (this.data.schedule) {
|
||||
this.editing = true;
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
this.form.get('recurrenceType')?.valueChanges.subscribe((value) => {
|
||||
if (value === 'none') {
|
||||
this.form.get('recurrenceDetails')?.disable();
|
||||
} else {
|
||||
this.form.get('recurrenceDetails')?.enable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.monthRows = [
|
||||
this.monthsList.slice(0, 6),
|
||||
this.monthsList.slice(6, 12)
|
||||
];
|
||||
}
|
||||
|
||||
loadData(): void {
|
||||
this.http.get<any>(`${this.baseUrl}${this.data.schedule['@id']}`).subscribe(
|
||||
(data) => {
|
||||
const formattedExecutionTime = this.formatExecutionTime(data.executionTime);
|
||||
|
||||
this.form.patchValue({
|
||||
executionDate: data.executionDate,
|
||||
executionTime: formattedExecutionTime,
|
||||
recurrenceType: data.recurrenceType,
|
||||
recurrenceDetails: {
|
||||
...data.recurrenceDetails,
|
||||
initDate: data.recurrenceDetails.initDate || null,
|
||||
endDate: data.recurrenceDetails.endDate || null,
|
||||
daysOfWeek: data.recurrenceDetails.daysOfWeek || [],
|
||||
months: data.recurrenceDetails.months || []
|
||||
},
|
||||
enabled: data.enabled
|
||||
});
|
||||
this.selectedDays = data.recurrenceDetails.daysOfWeek.reduce((acc: any, day: string) => {
|
||||
acc[day] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
this.selectedMonths = data.recurrenceDetails.months.reduce((acc: any, month: string) => {
|
||||
acc[month] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading schedule data', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
formatExecutionTime(time: string | Date): string {
|
||||
const date = (time instanceof Date) ? time : new Date(time);
|
||||
if (isNaN(date.getTime())) {
|
||||
console.error('Invalid execution time:', time);
|
||||
return '';
|
||||
}
|
||||
return date.toISOString().substring(11, 16);
|
||||
}
|
||||
|
||||
convertDateToLocalISO(date: Date): string {
|
||||
date = new Date(date);
|
||||
const adjustedDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
return adjustedDate.toISOString();
|
||||
}
|
||||
|
||||
|
||||
onSubmit() {
|
||||
const formData = this.form.value;
|
||||
|
||||
const payload: any = {
|
||||
commandTask: this.data.task['@id'],
|
||||
executionDate: formData.recurrenceType === 'none' ? this.convertDateToLocalISO(formData.executionDate) : null,
|
||||
executionTime: formData.executionTime,
|
||||
recurrenceType: formData.recurrenceType,
|
||||
recurrenceDetails: {
|
||||
...formData.recurrenceDetails,
|
||||
initDate: formData.recurrenceDetails?.initDate || null,
|
||||
endDate: formData.recurrenceDetails?.endDate || null,
|
||||
daysOfWeek: formData.recurrenceDetails?.daysOfWeek || [],
|
||||
months: formData.recurrenceDetails?.months || []
|
||||
},
|
||||
enabled: formData.enabled
|
||||
}
|
||||
|
||||
if (this.editing) {
|
||||
const taskId = this.data.task.uuid;
|
||||
this.http.patch<any>(`${this.baseUrl}${this.data.schedule['@id']}`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastr.success('Programacion de tarea actualizada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error('Error al actualizar la tarea');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.http.post<any>(this.apiUrl, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastr.success('Programacion de tarea creada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error('Error al crear la tarea');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
get summaryText(): string {
|
||||
const recurrence = this.form.get('recurrenceType')?.value;
|
||||
const start = this.form.get('recurrenceType')?.value === 'none' ? this.form.get('executionDate')?.value : this.form.get('recurrenceDetails.initDate')?.value;
|
||||
const end = this.form.get('recurrenceType')?.value === 'none' ? this.form.get('executionDate')?.value : this.form.get('recurrenceDetails.endDate')?.value;
|
||||
const time = this.form.get('executionTime')?.value;
|
||||
const days = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
|
||||
const months = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
|
||||
|
||||
if (recurrence === 'none') {
|
||||
return `Esta acción se ejecutará una sola vez el ${ this.formatDate(start)} a las ${time}.`;
|
||||
}
|
||||
|
||||
return `Esta acción se ejecutará todos los ${days.join(', ')} de ${months.join(', ')}, desde el ${this.formatDate(start)} hasta el ${this.formatDate(end)} a las ${time}.`;
|
||||
}
|
||||
|
||||
formatDate(date: string | Date): string {
|
||||
const realDate = (date instanceof Date) ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat('es-ES', { dateStyle: 'long' }).format(realDate);
|
||||
}
|
||||
|
||||
toggleDay(day: string) {
|
||||
this.selectedDays[day] = !this.selectedDays[day];
|
||||
const days = Object.keys(this.selectedDays).filter(d => this.selectedDays[d]);
|
||||
this.form.get('recurrenceDetails.daysOfWeek')?.setValue(days);
|
||||
}
|
||||
|
||||
toggleMonth(month: string) {
|
||||
this.selectedMonths[month] = !this.selectedMonths[month];
|
||||
const months = Object.keys(this.selectedMonths).filter(m => this.selectedMonths[m]);
|
||||
this.form.get('recurrenceDetails.months')?.setValue(months);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,284 +0,0 @@
|
|||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.task-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
.script-selector-card {
|
||||
margin: 20px 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.toggle-options {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
<h2 mat-dialog-title class="dialog-title">Añadir acción a: {{ data.task?.name }}</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<div class="task-form">
|
||||
|
||||
<div class="toggle-options">
|
||||
<mat-button-toggle-group [(ngModel)]="commandType" exclusive>
|
||||
<mat-button-toggle value="new">
|
||||
<mat-icon>edit</mat-icon> Nuevo Script
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="existing">
|
||||
<mat-icon>storage</mat-icon> Script Guardado
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'new'" class="new-command-container">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Orden de ejecucion </mat-label>
|
||||
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Ingrese el script</mat-label>
|
||||
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
|
||||
</mat-form-field>
|
||||
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'existing'">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Seleccione script a ejecutar</mat-label>
|
||||
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
|
||||
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Orden de ejecucion </mat-label>
|
||||
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
|
||||
</mat-form-field>
|
||||
|
||||
<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 parámetros:</h3>
|
||||
<div *ngFor="let paramName of parameterNames">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ paramName }}</mat-label>
|
||||
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" >{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,89 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreateTaskScriptComponent } from './create-task-script.component';
|
||||
import {GroupsComponent} from "../../../groups/groups.component";
|
||||
import {ExecuteCommandComponent} from "../../main-commands/execute-command/execute-command.component";
|
||||
import {LoadingComponent} from "../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule} from "@angular/common/http/testing";
|
||||
import {ToastrModule} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule, TranslateService} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
|
||||
describe('CreateTaskScriptComponent', () => {
|
||||
let component: CreateTaskScriptComponent;
|
||||
let fixture: ComponentFixture<CreateTaskScriptComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CreateTaskScriptComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatButtonToggleModule,
|
||||
MatTreeModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreateTaskScriptComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,138 +0,0 @@
|
|||
import {Component, EventEmitter, Inject, OnInit, 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 {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {
|
||||
SaveScriptComponent
|
||||
} from "../../../groups/components/client-main-view/run-script-assistant/save-script/save-script.component";
|
||||
import {FormBuilder, FormGroup} from "@angular/forms";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-task-script',
|
||||
templateUrl: './create-task-script.component.html',
|
||||
styleUrl: './create-task-script.component.css'
|
||||
})
|
||||
export class CreateTaskScriptComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
baseUrl: string;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
errorMessage = '';
|
||||
loading: boolean = false;
|
||||
scripts: any[] = [];
|
||||
scriptContent: string = "";
|
||||
parameters: any = {};
|
||||
commandType: string = 'existing';
|
||||
selectedScript: any = null;
|
||||
newScript: string = '';
|
||||
executionOrder: Number = 0;
|
||||
selection = new SelectionModel(true, []);
|
||||
parameterNames: string[] = Object.keys(this.parameters);
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<CreateTaskScriptComponent>,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
private router: Router,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.loadScripts()
|
||||
this.form = this.fb.group({
|
||||
content: [''],
|
||||
order: [''],
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
||||
commandTask: this.data.task['@id'],
|
||||
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||
order: this.executionOrder,
|
||||
type: 'run-script',
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Tarea creada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
|
@ -6,24 +6,19 @@
|
|||
padding: 20px;
|
||||
}
|
||||
|
||||
.select-task {
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
margin-bottom: 16px;
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
mat-form-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
@ -31,35 +26,3 @@ mat-form-field {
|
|||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
background-color: #f9f9f9;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.summary-block {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.date-time-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
|
|
@ -1,83 +1,106 @@
|
|||
<h2 mat-dialog-title class="dialog-title">
|
||||
{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}
|
||||
</h2>
|
||||
<h2 mat-dialog-title class="dialog-title">{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<form [formGroup]="taskForm" class="task-form">
|
||||
<mat-dialog-content>
|
||||
|
||||
<!-- Toggle entre crear o añadir -->
|
||||
<mat-radio-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode">
|
||||
<mat-radio-button value="create">Crear tarea</mat-radio-button>
|
||||
<mat-radio-button value="add">Introducir en tarea existente</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
<!-- Selección de tarea existente -->
|
||||
<div *ngIf="taskMode === 'add'" class="select-task">
|
||||
<h3 class="section-title">Información</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione una tarea</mat-label>
|
||||
<mat-select [(ngModel)]="selectedExistingTask" name="existingTask">
|
||||
<mat-option *ngFor="let task of existingTasks" [value]="task">{{ task.name }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-label>Información</mat-label>
|
||||
<textarea matInput formControlName="notes" placeholder="Ingresa tus notas aquí"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">{{ 'informationSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Orden de ejecución</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[(ngModel)]="executionOrder"
|
||||
name="executionOrder"
|
||||
min="1"
|
||||
placeholder="Introduce el orden"
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de nueva tarea -->
|
||||
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" placeholder="{{ 'nameLabel' | translate }}">
|
||||
<mat-error *ngIf="taskForm.get('name')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'notesLabel' | translate }}</mat-label>
|
||||
<mat-label>{{ 'informationLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="notes" placeholder="{{ 'notesPlaceholder' | translate }}"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">{{ 'commandSelectionSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Ámbito</mat-label>
|
||||
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)">
|
||||
<mat-option value="organizational-unit">Unidad Organizativa</mat-option>
|
||||
<mat-option value="classrooms-group">Grupo de aulas</mat-option>
|
||||
<mat-option value="classroom">Aulas</mat-option>
|
||||
<mat-option value="clients-group">Grupos de clientes</mat-option>
|
||||
<mat-label>{{ 'selectCommandsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="commandGroup" (selectionChange)="onCommandGroupChange()">
|
||||
<mat-option *ngFor="let group of availableCommandGroups" [value]="group.uuid">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('commandGroup')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
<mat-label>{{ 'selectIndividualCommandsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="extraCommands" multiple>
|
||||
<mat-option *ngFor="let command of availableIndividualCommands" [value]="command.uuid">
|
||||
{{ command.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate">
|
||||
¿Quieres programar la tarea al finalizar su creación?
|
||||
</mat-checkbox>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<h3 class="section-title">{{ 'executionDateTimeSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'executionDateLabel' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="date" placeholder="{{ 'selectDatePlaceholder' | translate }}">
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
<mat-error *ngIf="taskForm.get('date')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button
|
||||
class="submit-button"
|
||||
(click)="taskMode === 'create' ? saveTask() : addToExistingTask()"
|
||||
>
|
||||
{{ 'buttonSave' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'executionTimeLabel' | translate }}</mat-label>
|
||||
<input matInput type="time" formControlName="time" placeholder="{{ 'selectTimePlaceholder' | translate }}">
|
||||
<mat-error *ngIf="taskForm.get('time')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">{{ 'destinationSelectionSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectOrganizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectClassroomLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
|
||||
<mat-option *ngFor="let child of selectedUnitChildren" [value]="child['@id']">
|
||||
{{ child.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectClientsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
|
||||
{{ 'selectAllClients' | translate }}
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
|
||||
{{ client.name }} ({{ client.ip }})
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Clientes</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
|
||||
Seleccionar todos
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
|
||||
{{ client.name }} ({{ client.ip }})
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
</form>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="submit-button" (click)="saveTask()">{{ 'buttonSave' | translate }}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {of} from "rxjs";
|
||||
import {startWith, switchMap} from "rxjs/operators";
|
||||
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-task',
|
||||
|
@ -14,193 +10,176 @@ import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-s
|
|||
styleUrls: ['./create-task.component.css']
|
||||
})
|
||||
export class CreateTaskComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
taskForm: FormGroup;
|
||||
availableCommandGroups: any[] = [];
|
||||
selectedGroupCommands: any[] = [];
|
||||
availableIndividualCommands: any[] = [];
|
||||
apiUrl: string;
|
||||
apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
editing: boolean = false;
|
||||
availableOrganizationalUnits: any[] = [];
|
||||
clients: any[] = [];
|
||||
allOrganizationalUnits: any[] = [];
|
||||
loading: boolean = false;
|
||||
taskMode: 'create' | 'add' = 'create';
|
||||
existingTasks: any[] = [];
|
||||
selectedExistingTask: string | null = null;
|
||||
executionOrder: number | null = null;
|
||||
selectedUnitChildren: any[] = [];
|
||||
selectedClients: any[] = [];
|
||||
selectedClientIds: Set<string> = new Set();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService,
|
||||
private toastr: ToastrService,
|
||||
private dialog: MatDialog,
|
||||
public dialogRef: MatDialogRef<CreateTaskComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
this.taskForm = this.fb.group({
|
||||
scope: [ this.data?.scope ? this.data.scope : '', Validators.required],
|
||||
name: ['', Validators.required],
|
||||
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null, Validators.required],
|
||||
commandGroup: ['', Validators.required],
|
||||
extraCommands: [[]],
|
||||
date: ['', Validators.required],
|
||||
time: ['', Validators.required],
|
||||
notes: [''],
|
||||
scheduleAfterCreate: [false]
|
||||
organizationalUnit: ['', Validators.required],
|
||||
selectedChild: [''],
|
||||
selectedClients: [[]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
const observables = [
|
||||
this.loadCommandGroups(),
|
||||
this.loadIndividualCommands(),
|
||||
this.loadOrganizationalUnits(),
|
||||
this.startUnitsFilter(),
|
||||
this.loadTasks()
|
||||
];
|
||||
|
||||
Promise.all(observables).then(() => {
|
||||
|
||||
if (this.data.task) {
|
||||
this.editing = true;
|
||||
this.loadData().then(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
} else {
|
||||
this.loading = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
this.loadCommandGroups();
|
||||
this.loadIndividualCommands();
|
||||
this.loadOrganizationalUnits();
|
||||
if (this.data && this.data.task) {
|
||||
this.editing = true;
|
||||
this.loadTaskData(this.data.task);
|
||||
}
|
||||
}
|
||||
|
||||
loadData(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.baseUrl}${this.data.task['@id']}`).subscribe(
|
||||
(data) => {
|
||||
this.taskForm.patchValue({
|
||||
name: data.name,
|
||||
scope: data.scope,
|
||||
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
|
||||
notes: data.notes,
|
||||
});
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los datos de la tarea');
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
loadTasks(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.apiUrl}?page=1&itemsPerPage=100`).subscribe(
|
||||
(data) => {
|
||||
this.existingTasks = data['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar las tareas existentes');
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onScopeChange(scope: string): void {
|
||||
this.filterUnits(scope).subscribe(filteredUnits => {
|
||||
this.availableOrganizationalUnits = filteredUnits;
|
||||
this.taskForm.get('organizationalUnit')?.setValue('');
|
||||
});
|
||||
}
|
||||
|
||||
startUnitsFilter(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.taskForm.get('scope')?.valueChanges.pipe(
|
||||
startWith(this.taskForm.get('scope')?.value),
|
||||
switchMap((value) => this.filterUnits(value))
|
||||
).subscribe(filteredUnits => {
|
||||
this.availableOrganizationalUnits = filteredUnits;
|
||||
resolve();
|
||||
}, error => {
|
||||
this.toastr.error('Error al filtrar las unidades organizacionales');
|
||||
reject(error);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
filterUnits(value: string) {
|
||||
const filtered = this.allOrganizationalUnits.filter(unit => unit.type === value);
|
||||
return of(filtered);
|
||||
}
|
||||
|
||||
loadCommandGroups(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
loadCommandGroups(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/command-groups`).subscribe(
|
||||
(data) => {
|
||||
this.availableCommandGroups = data['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los grupos de comandos');
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadIndividualCommands(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.baseUrl}/commands`).subscribe(
|
||||
(data) => {
|
||||
this.availableIndividualCommands = data['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los comandos individuales');
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
loadIndividualCommands(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/commands`).subscribe(
|
||||
(data) => {
|
||||
this.availableIndividualCommands = data['hydra:member'];
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los comandos individuales');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadOrganizationalUnits(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=100`).subscribe(
|
||||
(data) => {
|
||||
this.allOrganizationalUnits = data['hydra:member'];
|
||||
this.availableOrganizationalUnits = [...this.allOrganizationalUnits];
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar las unidades organizacionales');
|
||||
reject(error);
|
||||
loadOrganizationalUnits(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
|
||||
(data) => {
|
||||
this.availableOrganizationalUnits = data['hydra:member'].filter((unit: any) => unit['type'] === 'organizational-unit');
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar las unidades organizacionales');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadTaskData(task: any): void {
|
||||
this.taskForm.patchValue({
|
||||
commandGroup: task.commandGroup ? task.commandGroup['@id'] : '',
|
||||
extraCommands: task.commands ? task.commands.map((cmd: any) => cmd['@id']) : [],
|
||||
date: task.dateTime ? task.dateTime.split('T')[0] : '',
|
||||
time: task.dateTime ? task.dateTime.split('T')[1].slice(0, 5) : '',
|
||||
notes: task.notes || '',
|
||||
organizationalUnit: task.organizationalUnit ? task.organizationalUnit['@id'] : ''
|
||||
});
|
||||
|
||||
if (task.commandGroup) {
|
||||
this.selectedGroupCommands = task.commandGroup.commands;
|
||||
}
|
||||
}
|
||||
|
||||
private collectClassrooms(unit: any): any[] {
|
||||
let classrooms = [];
|
||||
if (unit.type === 'classroom') {
|
||||
classrooms.push(unit);
|
||||
}
|
||||
if (unit.children && unit.children.length > 0) {
|
||||
for (let child of unit.children) {
|
||||
classrooms = classrooms.concat(this.collectClassrooms(child));
|
||||
}
|
||||
}
|
||||
return classrooms;
|
||||
}
|
||||
|
||||
onOrganizationalUnitChange(): void {
|
||||
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
|
||||
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
|
||||
|
||||
if (selectedUnit) {
|
||||
this.selectedUnitChildren = this.collectClassrooms(selectedUnit);
|
||||
} else {
|
||||
this.selectedUnitChildren = [];
|
||||
}
|
||||
|
||||
this.taskForm.patchValue({ selectedChild: '', selectedClients: [] });
|
||||
this.selectedClients = [];
|
||||
this.selectedClientIds.clear();
|
||||
}
|
||||
|
||||
onChildChange(): void {
|
||||
const selectedChildId = this.taskForm.get('selectedChild')?.value;
|
||||
|
||||
if (!selectedChildId) {
|
||||
this.selectedClients = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}${selectedChildId}`.replace(/([^:]\/)\/+/g, '$1');
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
(data) => {
|
||||
if (Array.isArray(data.clients) && data.clients.length > 0) {
|
||||
this.selectedClients = data.clients;
|
||||
} else {
|
||||
this.selectedClients = [];
|
||||
this.toastr.warning('El aula seleccionada no tiene clientes.');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this.taskForm.patchValue({ selectedClients: [] });
|
||||
this.selectedClientIds.clear();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los detalles del aula seleccionada');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
addToExistingTask() {
|
||||
if (!this.selectedExistingTask) {
|
||||
this.toastr.error('Debes seleccionar una tarea existente.');
|
||||
return;
|
||||
toggleSelectAll() {
|
||||
const allSelected = this.areAllSelected();
|
||||
if (allSelected) {
|
||||
this.selectedClientIds.clear();
|
||||
} else {
|
||||
this.selectedClients.forEach(client => this.selectedClientIds.add(client.uuid));
|
||||
}
|
||||
|
||||
if (this.executionOrder == null || this.executionOrder < 1) {
|
||||
this.toastr.error('Debes introducir un orden de ejecución válido (mayor que 0).');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
taskId: this.selectedExistingTask,
|
||||
executionOrder: this.executionOrder
|
||||
};
|
||||
|
||||
this.toastr.success('Tarea actualizada con éxito');
|
||||
this.dialogRef.close(data);
|
||||
this.taskForm.get('selectedClients')!.setValue(Array.from(this.selectedClientIds));
|
||||
}
|
||||
|
||||
areAllSelected(): boolean {
|
||||
return this.selectedClients.length > 0 && this.selectedClients.every(client => this.selectedClientIds.has(client.uuid));
|
||||
}
|
||||
|
||||
onCommandGroupChange(): void {
|
||||
const selectedGroupId = this.taskForm.get('commandGroup')?.value;
|
||||
this.http.get<any>(`${this.baseUrl}/command-groups/${selectedGroupId}`).subscribe(
|
||||
(data) => {
|
||||
this.selectedGroupCommands = data.commands;
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los comandos del grupo seleccionado');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
saveTask(): void {
|
||||
if (this.taskForm.invalid) {
|
||||
|
@ -209,14 +188,22 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
|
||||
const formData = this.taskForm.value;
|
||||
const dateTime = this.combineDateAndTime(formData.date, formData.time);
|
||||
const selectedCommands = formData.extraCommands && formData.extraCommands.length > 0
|
||||
? formData.extraCommands.map((id: any) => `/commands/${id}`)
|
||||
: null;
|
||||
|
||||
const payload: any = {
|
||||
name: formData.name,
|
||||
scope: formData.scope,
|
||||
organizationalUnit: formData.organizationalUnit,
|
||||
commandGroups: formData.commandGroup ? [`/command-groups/${formData.commandGroup}`] : null,
|
||||
dateTime: dateTime,
|
||||
notes: formData.notes || '',
|
||||
clients: Array.from(this.selectedClientIds).map((uuid: string) => `/clients/${uuid}`),
|
||||
};
|
||||
|
||||
if (selectedCommands) {
|
||||
payload.commands = selectedCommands;
|
||||
}
|
||||
|
||||
if (this.editing) {
|
||||
const taskId = this.data.task.uuid;
|
||||
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
|
||||
|
@ -230,21 +217,9 @@ export class CreateTaskComponent implements OnInit {
|
|||
});
|
||||
} else {
|
||||
this.http.post<any>(this.apiUrl, payload).subscribe({
|
||||
next: response => {
|
||||
next: () => {
|
||||
this.toastr.success('Tarea creada con éxito');
|
||||
this.dialogRef.close(response);
|
||||
if (formData.scheduleAfterCreate) {
|
||||
const dialogRef = this.dialog.open(CreateTaskScheduleComponent, {
|
||||
width: '800px',
|
||||
data: { task: response }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastr.success('Tarea programada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error('Error al crear la tarea');
|
||||
|
@ -253,7 +228,14 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
combineDateAndTime(date: string, time: string): string {
|
||||
const dateObj = new Date(date);
|
||||
const [hours, minutes] = time.split(':').map(Number);
|
||||
dateObj.setHours(hours, minutes, 0);
|
||||
return dateObj.toISOString();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close(false);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.spacing-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex-grow: 1;
|
||||
margin-right: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-icon {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.subnets-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h2 mat-dialog-title>Gestionar programaciones de tareas en {{ data.commandTask?.name }}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
|
||||
text="Busca subredes por nombre para localizar una subred específica rápidamente.">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre del cliente</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder"
|
||||
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear tree search"
|
||||
(click)="filters['name'] = ''; loadData()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchIpStep" text="Busca programaciones por tipo.">
|
||||
<mat-label i18n="@@searchLabel">Buscar por tipo</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['recurrence']" i18n-placeholder="@@searchPlaceholder"
|
||||
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<button *ngIf="filters['ip']" mat-icon-button matSuffix aria-label="Clear tree search"
|
||||
(click)="filters['ip'] = ''; loadData()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="Visualiza y administra las subredes listadas según los filtros aplicados.">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let schedule">
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'recurrenceType'">
|
||||
<mat-chip style="padding: 10px; margin: 5px;">
|
||||
<ng-container *ngIf="column.cell(schedule) === 'none'; else scheduledTemplate">
|
||||
No programado
|
||||
<div style="font-size: 12px;">
|
||||
{{ schedule.executionDate | date }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #scheduledTemplate>
|
||||
Programado
|
||||
<div style="font-size: 12px;">
|
||||
{{ schedule.recurrenceDetails.initDate | date }} → {{ schedule.recurrenceDetails.endDate | date}}
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'executionTime'">
|
||||
{{ schedule.executionTime | date: 'HH:mm' }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef !== 'recurrenceType' && column.columnDef !== 'executionTime' && column.columnDef !== 'enabled'">
|
||||
{{ column.cell(schedule) }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'enabled'">
|
||||
<mat-chip>
|
||||
<ng-container *ngIf="schedule.enabled">
|
||||
Activo
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!schedule.enabled">
|
||||
Inactivo
|
||||
</ng-container>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editSchedule(schedule)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteSchedule(schedule)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep"
|
||||
text="Navega entre las páginas de subredes usando el paginador.">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">Cerrar</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,86 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ShowTaskScheduleComponent } from './show-task-schedule.component';
|
||||
import {LoadingComponent} from "../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule} from "@angular/common/http/testing";
|
||||
import {ToastrModule} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
|
||||
|
||||
describe('ShowTaskScheduleComponent', () => {
|
||||
let component: ShowTaskScheduleComponent;
|
||||
let fixture: ComponentFixture<ShowTaskScheduleComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ShowTaskScheduleComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatTreeModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ShowTaskScheduleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,107 +0,0 @@
|
|||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {Client} from "../../../groups/model/model";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
|
||||
import {DatePipe} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-task-schedule',
|
||||
templateUrl: './show-task-schedule.component.html',
|
||||
styleUrl: './show-task-schedule.component.css'
|
||||
})
|
||||
export class ShowTaskScheduleComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
dataSource = new MatTableDataSource<any>([]);
|
||||
length = 0;
|
||||
itemsPerPage: number = 10;
|
||||
pageSizeOptions: number[] = [5, 10, 20];
|
||||
page = 0;
|
||||
loading: boolean = false;
|
||||
filters: { [key: string]: string } = {};
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
|
||||
columns = [
|
||||
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
|
||||
{ columnDef: 'recurrenceType', header: 'Recurrencia', cell: (schedule: any) => schedule.recurrenceType },
|
||||
{ columnDef: 'time', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm', 'UTC') },
|
||||
{ columnDef: 'daysOfWeek', header: 'Dias de la semana', cell: (schedule: any) => schedule.recurrenceDetails.daysOfWeek },
|
||||
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails.months },
|
||||
{ columnDef: 'enabled', header: 'Activo', cell: (schedule: any) => schedule.enabled }
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'recurrenceType', 'time', 'daysOfWeek', 'months', 'enabled', 'actions'];
|
||||
|
||||
constructor(
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<ShowTaskScheduleComponent>,
|
||||
public dialog: MatDialog,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.baseUrl}/command-task-schedules?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}`, { params: this.filters }).subscribe(
|
||||
(data) => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
editSchedule(schedule: any): void {
|
||||
this.dialog.open(CreateTaskScheduleComponent, {
|
||||
width: '800px',
|
||||
data: { schedule: schedule, task: this.data.commandTask }
|
||||
}).afterClosed().subscribe(() => this.loadData());
|
||||
}
|
||||
|
||||
deleteSchedule(schedule: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: 'tarea programada' }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
|
||||
() => {
|
||||
this.toastService.success('Programación eliminada correctamente');
|
||||
this.loadData();
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
onPageChange(event: any) {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.loadData()
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.spacing-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex-grow: 1;
|
||||
margin-right: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-icon {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.subnets-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h2 mat-dialog-title>Gestionar scripts de tareas en {{ data.commandTask?.name }}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
|
||||
text="Busca subredes por nombre para localizar una subred específica rápidamente.">
|
||||
<mat-label i18n="@@searchLabel">Buscar contenido de script</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder"
|
||||
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear tree search"
|
||||
(click)="filters['name'] = ''; loadData()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="Visualiza y administra las subredes listadas según los filtros aplicados.">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let schedule">
|
||||
<ng-container *ngIf="column.columnDef === 'content'; else checkOtherColumn">
|
||||
<div style="background-color: #f5f5f5; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; white-space: pre-wrap; font-size: 13px;">
|
||||
{{ column.cell(schedule) }}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #checkOtherColumn>
|
||||
<ng-container *ngIf="column.columnDef === 'parameters'; else normalCell">
|
||||
<button mat-stroked-button color="primary" (click)="openParametersModal(schedule.parameters)">
|
||||
Ver parámetros
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #normalCell>
|
||||
{{ column.cell(schedule) }}
|
||||
</ng-template>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
|
||||
<button mat-icon-button color="warn" (click)="deleteTaskScript(schedule)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep"
|
||||
text="Navega entre las páginas de subredes usando el paginador.">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">Cerrar</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,86 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ShowTaskScriptComponent } from './show-task-script.component';
|
||||
import {LoadingComponent} from "../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule} from "@angular/common/http/testing";
|
||||
import {ToastrModule} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {ShowTaskScheduleComponent} from "../show-task-schedule/show-task-schedule.component";
|
||||
|
||||
describe('ShowTaskScriptComponent', () => {
|
||||
let component: ShowTaskScriptComponent;
|
||||
let fixture: ComponentFixture<ShowTaskScriptComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ShowTaskScriptComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatTreeModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ShowTaskScriptComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,104 +0,0 @@
|
|||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
|
||||
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {ViewParametersModalComponent} from "./view-parameters-modal/view-parameters-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-task-script',
|
||||
templateUrl: './show-task-script.component.html',
|
||||
styleUrl: './show-task-script.component.css'
|
||||
})
|
||||
export class ShowTaskScriptComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
dataSource = new MatTableDataSource<any>([]);
|
||||
length = 0;
|
||||
itemsPerPage: number = 10;
|
||||
pageSizeOptions: number[] = [5, 10, 20];
|
||||
page = 0;
|
||||
loading: boolean = false;
|
||||
filters: { [key: string]: string } = {};
|
||||
|
||||
columns = [
|
||||
{ columnDef: 'id', header: 'ID', cell: (client: any) => client.id },
|
||||
{ columnDef: 'order', header: 'Orden', cell: (client: any) => client.order },
|
||||
{ columnDef: 'content', header: 'Script', cell: (client: any) => client.content },
|
||||
{ columnDef: 'type', header: 'Type', cell: (client: any) => client.type },
|
||||
{ columnDef: 'parameters', header: 'Parameters', cell: (client: any) => client.parameters },
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'order', 'type', 'parameters', 'content', 'actions'];
|
||||
|
||||
constructor(
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<ShowTaskScriptComponent>,
|
||||
public dialog: MatDialog,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.baseUrl}/command-task-scripts?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}`, { params: this.filters }).subscribe(
|
||||
(data) => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteTaskScript(schedule: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: 'script de una tarea' }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
|
||||
() => {
|
||||
this.toastService.success('Eliminado correctamente');
|
||||
this.loadData();
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
openParametersModal(parameters: any): void {
|
||||
this.dialog.open(ViewParametersModalComponent, {
|
||||
width: '900px',
|
||||
data: parameters
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: any) {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.loadData()
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
<h2 mat-dialog-title>Parámetros</h2>
|
||||
<mat-dialog-content>
|
||||
<pre>{{ data | json }}</pre>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">Cerrar</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,69 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ViewParametersModalComponent } from './view-parameters-modal.component';
|
||||
import {LoadingComponent} from "../../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule, provideHttpClientTesting} from "@angular/common/http/testing";
|
||||
import {ToastrModule, ToastrService} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormBuilder, FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {InputDialogComponent} from "../../../../task-logs/input-dialog/input-dialog.component";
|
||||
import {provideHttpClient} from "@angular/common/http";
|
||||
|
||||
describe('ViewParametersModalComponent', () => {
|
||||
let component: ViewParametersModalComponent;
|
||||
let fixture: ComponentFixture<ViewParametersModalComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ViewParametersModalComponent],
|
||||
imports: [
|
||||
MatDialogModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
}
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ViewParametersModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,18 +0,0 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
|
||||
@Component({
|
||||
selector: 'app-view-parameters-modal',
|
||||
templateUrl: './view-parameters-modal.component.html',
|
||||
styleUrl: './view-parameters-modal.component.css'
|
||||
})
|
||||
export class ViewParametersModalComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ViewParametersModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' |
|
||||
translate }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="resetFilters()" joyrideStep="resetFiltersStep"
|
||||
text="{{ 'resetFiltersStepText' | translate }}">
|
||||
{{ 'resetFilters' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-select" joyrideStep="clientSelectStep"
|
||||
text="{{ 'clientSelectStepText' | translate }}">
|
||||
<input type="text" matInput [formControl]="clientControl" [matAutocomplete]="clientAuto"
|
||||
placeholder="{{ 'filterClientPlaceholder' | translate }}">
|
||||
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient"
|
||||
(optionSelected)="onOptionClientSelected($event.option.value)">
|
||||
<mat-option *ngFor="let client of filteredClients | async" [value]="client">
|
||||
{{ client.name }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-select" joyrideStep="commandSelectStep"
|
||||
text="{{ 'commandSelectStepText' | translate }}">
|
||||
<input type="text" matInput [formControl]="commandControl" [matAutocomplete]="commandAuto"
|
||||
placeholder="{{ 'filterCommandPlaceholder' | translate }}">
|
||||
<mat-autocomplete #commandAuto="matAutocomplete" [displayWith]="displayFnCommand"
|
||||
(optionSelected)="onOptionCommandSelected($event.option.value)">
|
||||
<mat-option *ngFor="let command of filteredCommands | async" [value]="command">
|
||||
{{ command.name }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-boolean">
|
||||
<mat-label i18n="@@searchLabel">Estado</mat-label>
|
||||
<mat-select [(ngModel)]="filters['status']" (selectionChange)="loadTraces()" placeholder="Seleccionar opción">
|
||||
<mat-option [value]="undefined">Todos</mat-option>
|
||||
<mat-option [value]="'failed'">Fallido</mat-option>
|
||||
<mat-option [value]="'pending'">Pendiente de ejecutar</mat-option>
|
||||
<mat-option [value]="'in-progress'">Ejecutando</mat-option>
|
||||
<mat-option [value]="'success'">Completado con éxito</mat-option>
|
||||
<mat-option [value]="'cancelled'">Cancelado</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="traces" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let trace">
|
||||
|
||||
<ng-container [ngSwitch]="column.columnDef">
|
||||
<ng-container *ngSwitchCase="'status'">
|
||||
<ng-container *ngIf="trace.status === 'in-progress' && trace.progress; else statusChip">
|
||||
<div class="progress-container">
|
||||
<mat-progress-bar class="example-margin" [mode]="mode" [value]="trace.progress" [bufferValue]="bufferValue">
|
||||
</mat-progress-bar>
|
||||
<span>{{trace.progress}}%</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #statusChip>
|
||||
<div class="status-progress-flex">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': trace.status === 'failed',
|
||||
'chip-success': trace.status === 'success',
|
||||
'chip-pending': trace.status === 'pending',
|
||||
'chip-in-progress': trace.status === 'in-progress',
|
||||
'chip-cancelled': trace.status === 'cancelled'
|
||||
}">
|
||||
{{
|
||||
trace.status === 'failed' ? 'Fallido' :
|
||||
trace.status === 'in-progress' ? 'En ejecución' :
|
||||
trace.status === 'success' ? 'Finalizado con éxito' :
|
||||
trace.status === 'pending' ? 'Pendiente de ejecutar' :
|
||||
trace.status === 'cancelled' ? 'Cancelado' :
|
||||
trace.status
|
||||
}}
|
||||
</mat-chip>
|
||||
<button *ngIf="trace.status === 'in-progress' && trace.command === 'deploy-image'"
|
||||
mat-icon-button
|
||||
(click)="cancelTrace(trace)"
|
||||
class="cancel-button"
|
||||
matTooltip="Cancelar transmisión de imagen">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'input'">
|
||||
<button mat-icon-button (click)="openInputModal(trace.input)">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchDefault>
|
||||
{{ column.cell(trace) }}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import {ChangeDetectorRef, Component, OnInit} from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, forkJoin } from 'rxjs';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
@ -7,13 +7,9 @@ import { DatePipe } from '@angular/common';
|
|||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { InputDialogComponent } from "./input-dialog/input-dialog.component";
|
||||
import { ProgressBarMode } from '@angular/material/progress-bar';
|
||||
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { OutputDialogComponent } from "./output-dialog/output-dialog.component";
|
||||
import { TranslationService } from "@services/translation.service";
|
||||
import { COMMAND_TYPES } from '../../shared/constants/command-types';
|
||||
import { ProgressBarMode, MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-task-logs',
|
||||
|
@ -21,8 +17,8 @@ import { COMMAND_TYPES } from '../../shared/constants/command-types';
|
|||
styleUrls: ['./task-logs.component.css']
|
||||
})
|
||||
export class TaskLogsComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
mercureUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
mercureUrl: string = import.meta.env.NG_APP_OGCORE_MERCURE_BASE_URL;
|
||||
traces: any[] = [];
|
||||
groupedTraces: any[] = [];
|
||||
commands: any[] = [];
|
||||
|
@ -36,13 +32,6 @@ export class TaskLogsComponent implements OnInit {
|
|||
mode: ProgressBarMode = 'buffer';
|
||||
progress = 0;
|
||||
bufferValue = 0;
|
||||
today = new Date();
|
||||
|
||||
filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({
|
||||
name: key,
|
||||
value: key,
|
||||
label: COMMAND_TYPES[key]
|
||||
}));
|
||||
|
||||
columns = [
|
||||
{
|
||||
|
@ -53,30 +42,45 @@ export class TaskLogsComponent implements OnInit {
|
|||
{
|
||||
columnDef: 'command',
|
||||
header: 'Comando',
|
||||
cell: (trace: any) => trace.command
|
||||
cell: (trace: any) => `${trace.command}`
|
||||
},
|
||||
{
|
||||
columnDef: 'client',
|
||||
header: 'Cliente',
|
||||
cell: (trace: any) => trace.client?.name
|
||||
header: 'Client',
|
||||
cell: (trace: any) => `${trace.client?.name}`
|
||||
},
|
||||
{
|
||||
columnDef: 'status',
|
||||
header: 'Estado',
|
||||
cell: (trace: any) => trace.status
|
||||
cell: (trace: any) => `${trace.status}`
|
||||
},
|
||||
{
|
||||
columnDef: 'jobId',
|
||||
header: 'Hilo de trabajo',
|
||||
cell: (trace: any) => `${trace.jobId}`
|
||||
},
|
||||
{
|
||||
columnDef: 'input',
|
||||
header: 'Input',
|
||||
cell: (trace: any) => `${trace.input}`
|
||||
},
|
||||
{
|
||||
columnDef: 'output',
|
||||
header: 'Logs',
|
||||
cell: (trace: any) => `${trace.output}`
|
||||
},
|
||||
{
|
||||
columnDef: 'executedAt',
|
||||
header: 'Ejecución',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
header: 'Programación de ejecución',
|
||||
cell: (trace: any) => `${this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'finishedAt',
|
||||
header: 'Finalización',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
cell: (trace: any) => `${this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
},
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef)];
|
||||
|
||||
filters: { [key: string]: string } = {};
|
||||
filteredClients!: Observable<any[]>;
|
||||
|
@ -85,21 +89,16 @@ export class TaskLogsComponent implements OnInit {
|
|||
commandControl = new FormControl();
|
||||
|
||||
constructor(private http: HttpClient,
|
||||
private joyrideService: JoyrideService,
|
||||
private dialog: MatDialog,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
private translationService: TranslationService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.mercureUrl = this.configService.mercureUrl;
|
||||
}
|
||||
private joyrideService: JoyrideService,
|
||||
private dialog: MatDialog,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private toastService: ToastrService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTraces();
|
||||
this.loadCommands();
|
||||
this.loadClients();
|
||||
//this.loadClients();
|
||||
this.filteredCommands = this.commandControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => (typeof value === 'string' ? value : value?.name)),
|
||||
|
@ -143,17 +142,11 @@ export class TaskLogsComponent implements OnInit {
|
|||
}
|
||||
|
||||
|
||||
private _filterClients(value: string): any[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
|
||||
return this.clients.filter(client =>
|
||||
client.name?.toLowerCase().includes(filterValue) ||
|
||||
client.ip?.toLowerCase().includes(filterValue) ||
|
||||
client.mac?.toLowerCase().includes(filterValue)
|
||||
);
|
||||
private _filterClients(name: string): any[] {
|
||||
const filterValue = name.toLowerCase();
|
||||
return this.clients.filter(client => client.name.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
|
||||
private _filterCommands(name: string): any[] {
|
||||
const filterValue = name.toLowerCase();
|
||||
return this.commands.filter(command => command.name.toLowerCase().includes(filterValue));
|
||||
|
@ -163,13 +156,12 @@ export class TaskLogsComponent implements OnInit {
|
|||
return client && client.name ? client.name : '';
|
||||
}
|
||||
|
||||
onOptionCommandSelected(selectedCommand: any): void {
|
||||
this.filters['command'] = selectedCommand.name;
|
||||
this.loadTraces();
|
||||
displayFnCommand(command: any): string {
|
||||
return command && command.name ? command.name : '';
|
||||
}
|
||||
|
||||
onOptionStatusSelected(selectedStatus: any): void {
|
||||
this.filters['status'] = selectedStatus;
|
||||
onOptionCommandSelected(selectedCommand: any): void {
|
||||
this.filters['command.id'] = selectedCommand.id;
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
|
@ -180,19 +172,11 @@ export class TaskLogsComponent implements OnInit {
|
|||
|
||||
openInputModal(inputData: any): void {
|
||||
this.dialog.open(InputDialogComponent, {
|
||||
width: '70vw',
|
||||
height: '60vh',
|
||||
width: '700px',
|
||||
data: { input: inputData }
|
||||
});
|
||||
}
|
||||
|
||||
openOutputModal(outputData: any): void {
|
||||
this.dialog.open(OutputDialogComponent, {
|
||||
width: '500px',
|
||||
data: { input: outputData }
|
||||
});
|
||||
}
|
||||
|
||||
cancelTrace(trace: any): void {
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
|
@ -216,20 +200,10 @@ export class TaskLogsComponent implements OnInit {
|
|||
loadTraces(): void {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/traces?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`;
|
||||
const params: any = { ...this.filters };
|
||||
const params = { ...this.filters };
|
||||
if (params['status'] === undefined) {
|
||||
delete params['status'];
|
||||
}
|
||||
|
||||
if (params['startDate']) {
|
||||
params['executedAt[after]'] = this.datePipe.transform(params['startDate'], 'yyyy-MM-dd');
|
||||
delete params['startDate'];
|
||||
}
|
||||
if (params['endDate']) {
|
||||
params['executedAt[before]'] = this.datePipe.transform(params['endDate'], 'yyyy-MM-dd');
|
||||
delete params['endDate'];
|
||||
}
|
||||
|
||||
this.http.get<any>(url, { params }).subscribe(
|
||||
(data) => {
|
||||
this.traces = data['hydra:member'];
|
||||
|
@ -249,6 +223,7 @@ export class TaskLogsComponent implements OnInit {
|
|||
this.http.get<any>(`${this.baseUrl}/commands?&page=1&itemsPerPage=10000`).subscribe(
|
||||
response => {
|
||||
this.commands = response['hydra:member'];
|
||||
console.log(this.commands);
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
|
@ -260,24 +235,30 @@ export class TaskLogsComponent implements OnInit {
|
|||
|
||||
loadClients() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.baseUrl}/clients?page=1&itemsPerPage=10000`).subscribe(
|
||||
this.http.get<any>(`${this.baseUrl}/clients?&page=1&itemsPerPage=10000`).subscribe(
|
||||
response => {
|
||||
this.clients = response['hydra:member'];
|
||||
this.loading = false;
|
||||
const clientIds = response['hydra:member'].map((client: any) => client['@id']);
|
||||
const clientDetailsRequests: Observable<any>[] = clientIds.map((id: string) => this.http.get<any>(`${this.baseUrl}${id}`));
|
||||
forkJoin(clientDetailsRequests).subscribe(
|
||||
(clients: any[]) => {
|
||||
this.clients = clients;
|
||||
this.loading = false;
|
||||
},
|
||||
(error: any) => {
|
||||
console.error('Error fetching client details:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
},
|
||||
error => {
|
||||
(error: any) => {
|
||||
console.error('Error fetching clients:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
resetFilters(clientSearchCommandInput: any, clientSearchStatusInput: any, clientSearchClientInput: any) {
|
||||
resetFilters() {
|
||||
this.loading = true;
|
||||
clientSearchCommandInput.value = null;
|
||||
clientSearchStatusInput.value = null;
|
||||
clientSearchClientInput.value = null;
|
||||
this.filters = {};
|
||||
this.loadTraces();
|
||||
}
|
||||
|
@ -299,21 +280,6 @@ export class TaskLogsComponent implements OnInit {
|
|||
}));
|
||||
}
|
||||
|
||||
onDateFilterChange(): void {
|
||||
const start = this.filters['startDate'];
|
||||
const end = this.filters['endDate'];
|
||||
|
||||
if (!start || !end) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (start && end && start > end) {
|
||||
this.toastService.warning('La fecha de inicio no puede ser mayor que la fecha de fin');
|
||||
return;
|
||||
}
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
|
@ -321,39 +287,14 @@ export class TaskLogsComponent implements OnInit {
|
|||
this.loadTraces();
|
||||
}
|
||||
|
||||
translateCommand(command: string): string {
|
||||
return this.translationService.getCommandTranslation(command);
|
||||
}
|
||||
|
||||
clearCommandFilter(event: Event, clientSearchCommandInput: any): void {
|
||||
event.stopPropagation();
|
||||
delete this.filters['command'];
|
||||
clientSearchCommandInput.value = null;
|
||||
this.loadTraces()
|
||||
}
|
||||
|
||||
clearStatusFilter(event: Event, clientSearchStatusInput: any): void {
|
||||
event.stopPropagation();
|
||||
delete this.filters['status'];
|
||||
clientSearchStatusInput.value = null;
|
||||
this.loadTraces()
|
||||
}
|
||||
|
||||
clearClientFilter(event: Event, clientSearchClientInput: any): void {
|
||||
event.stopPropagation();
|
||||
delete this.filters['client.id'];
|
||||
clientSearchClientInput.value = null;
|
||||
this.loadTraces()
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'tracesTitleStep',
|
||||
'titleStep',
|
||||
'resetFiltersStep',
|
||||
'filtersStep',
|
||||
'tracesProgressStep',
|
||||
'tracesInfoStep',
|
||||
'clientSelectStep',
|
||||
'commandSelectStep',
|
||||
'tableStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
|
@ -20,18 +20,12 @@ import { NgxChartsModule } from '@swimlane/ngx-charts';
|
|||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { JoyrideModule } from 'ngx-joyride';
|
||||
import { LoadingComponent } from '../../../shared/loading/loading.component';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('CommandsComponent', () => {
|
||||
let component: CommandsComponent;
|
||||
let fixture: ComponentFixture<CommandsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CommandsComponent, LoadingComponent],
|
||||
imports: [
|
||||
|
@ -59,8 +53,7 @@ describe('CommandsComponent', () => {
|
|||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} }
|
||||
]
|
||||
|
||||
}).compileComponents();
|
||||
|
|
|
@ -7,7 +7,7 @@ import { CreateCommandComponent } from './create-command/create-command.componen
|
|||
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ExecuteCommandComponent } from './execute-command/execute-command.component';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
|
||||
@Component({
|
||||
|
@ -16,8 +16,7 @@ import { JoyrideService } from 'ngx-joyride';
|
|||
styleUrls: ['./commands.component.css']
|
||||
})
|
||||
export class CommandsComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
filters: { [key: string]: string | boolean } = {};
|
||||
length: number = 0;
|
||||
|
@ -49,12 +48,10 @@ export class CommandsComponent implements OnInit {
|
|||
}
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
private apiUrl = `${this.baseUrl}/commands`;
|
||||
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService, private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/commands`;
|
||||
}
|
||||
private joyrideService: JoyrideService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
|
@ -85,14 +82,14 @@ export class CommandsComponent implements OnInit {
|
|||
|
||||
openCreateCommandModal(): void {
|
||||
this.dialog.open(CreateCommandComponent, {
|
||||
width: '800px',
|
||||
width: '600px',
|
||||
}).afterClosed().subscribe(() => this.search());
|
||||
}
|
||||
|
||||
editCommand(event: MouseEvent, command: any): void {
|
||||
event.stopPropagation();
|
||||
this.dialog.open(CreateCommandComponent, {
|
||||
width: '800px',
|
||||
width: '600px',
|
||||
data: command['@id']
|
||||
}).afterClosed().subscribe(() => this.search());
|
||||
}
|
||||
|
|
|
@ -57,15 +57,4 @@
|
|||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.checkbox-with-hint {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
margin-left: 40px;
|
||||
}
|
||||
}
|
|
@ -13,13 +13,7 @@
|
|||
|
||||
<div class="checkbox-group">
|
||||
<mat-checkbox formControlName="readOnly">{{ 'readOnlyLabel' | 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 @.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
|
|
|
@ -13,18 +13,12 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
|
|||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('CreateCommandComponent', () => {
|
||||
let component: CreateCommandComponent;
|
||||
let fixture: ComponentFixture<CreateCommandComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CreateCommandComponent],
|
||||
imports: [
|
||||
|
@ -51,8 +45,7 @@ describe('CreateCommandComponent', () => {
|
|||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
|
|
@ -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 { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from "../data.service";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
import {DataService} from "../data.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-command',
|
||||
templateUrl: './create-command.component.html',
|
||||
styleUrls: ['./create-command.component.css']
|
||||
})
|
||||
export class CreateCommandComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
export class CreateCommandComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
createCommandForm: FormGroup<any>;
|
||||
private apiUrl = `${this.baseUrl}/commands`;
|
||||
commandId: string | null = null;
|
||||
|
||||
constructor(
|
||||
|
@ -21,16 +21,13 @@ export class CreateCommandComponent implements OnInit{
|
|||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<CreateCommandComponent>,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
private dataService: DataService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.createCommandForm = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
script: [''],
|
||||
readOnly: [false],
|
||||
parameters: [false],
|
||||
enabled: [true],
|
||||
comments: [''],
|
||||
});
|
||||
|
@ -45,12 +42,12 @@ export class CreateCommandComponent implements OnInit{
|
|||
load(): void {
|
||||
this.dataService.getCommand(this.data).subscribe({
|
||||
next: (response) => {
|
||||
console.log(response);
|
||||
this.createCommandForm = this.fb.group({
|
||||
name: [response.name, Validators.required],
|
||||
notes: [response.notes],
|
||||
script: [response.script],
|
||||
readOnly: [response.readOnly],
|
||||
parameters: [response.parameters],
|
||||
enabled: [response.enabled],
|
||||
});
|
||||
this.commandId = response['@id'];
|
||||
|
@ -75,7 +72,6 @@ export class CreateCommandComponent implements OnInit{
|
|||
readOnly: this.createCommandForm.value.readOnly,
|
||||
enabled: this.createCommandForm.value.enabled,
|
||||
comments: this.createCommandForm.value.comments,
|
||||
parameters: this.createCommandForm.value.parameters,
|
||||
};
|
||||
|
||||
if (this.commandId) {
|
||||
|
@ -86,6 +82,7 @@ export class CreateCommandComponent implements OnInit{
|
|||
},
|
||||
(error) => {
|
||||
this.toastService.error(error['error']['hydra:description']);
|
||||
console.error('Error al editar el comando', error);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
@ -96,6 +93,7 @@ export class CreateCommandComponent implements OnInit{
|
|||
},
|
||||
(error) => {
|
||||
this.toastService.error(error['error']['hydra:description']);
|
||||
console.error('Error al añadir comando', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
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 { catchError, map } from 'rxjs/operators';
|
||||
|
||||
|
@ -8,16 +8,10 @@ import { catchError, map } from 'rxjs/operators';
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
private apiUrl = `${this.baseUrl}/commands?page=1&itemsPerPage=1000`;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/commands?page=1&itemsPerPage=1000`;
|
||||
}
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getCommands(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
||||
const params = new HttpParams({ fromObject: filters });
|
||||
|
|
|
@ -4,7 +4,6 @@ import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dial
|
|||
import { CreateCommandComponent } from '../create-command/create-command.component';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-command-detail',
|
||||
|
@ -12,7 +11,7 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./command-detail.component.css']
|
||||
})
|
||||
export class CommandDetailComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
form!: FormGroup;
|
||||
clients: any[] = [];
|
||||
showClientSelect = false;
|
||||
|
@ -21,15 +20,12 @@ export class CommandDetailComponent implements OnInit {
|
|||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private configService: ConfigService,
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<CommandDetailComponent>,
|
||||
private dialog: MatDialog,
|
||||
private toastService: ToastrService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.fb.group({
|
||||
|
@ -56,7 +52,7 @@ export class CommandDetailComponent implements OnInit {
|
|||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success('Comando editado');
|
||||
this.toastService.success('Comando editado' );
|
||||
this.data.command = result;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selected-client {
|
||||
background-color: #a0c2e5 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
<h2 mat-dialog-title> Seleccionar particion para arrancar SO</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<div *ngIf="!loading" class="select-container">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Clientes </mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Listado de clientes para arrancar un SO
|
||||
<mat-icon>desktop_windows</mat-icon>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="toggleSelectAll()">
|
||||
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of data.clients" class="client-item">
|
||||
<div class="client-card"
|
||||
(click)="client.status === 'og-live' && toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}" >
|
||||
|
||||
<img
|
||||
[src]="'assets/images/computer_' + client.status + '.svg'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name | slice:0:20 }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
|
||||
<mat-radio-button [value]="client"
|
||||
color="primary"
|
||||
[disabled]="!client.selected"
|
||||
(click)="$event.stopPropagation()">
|
||||
Modelo
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div *ngIf="!loading" class="partition-table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group
|
||||
[(ngModel)]="selectedPartition"
|
||||
[disabled]="!row.operativeSystem"
|
||||
>
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
|
||||
</div>
|
|
@ -1,82 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BootSoPartitionComponent } from './boot-so-partition.component';
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatCheckboxModule } from "@angular/material/checkbox";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatMenuModule } from "@angular/material/menu";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { MatTableModule } from "@angular/material/table";
|
||||
import { MatSelectModule } from "@angular/material/select";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { ToastrModule, ToastrService } from "ngx-toastr";
|
||||
import { TranslateModule } from "@ngx-translate/core";
|
||||
import { DataService } from "../../data.service";
|
||||
import { provideHttpClient } from "@angular/common/http";
|
||||
import { provideHttpClientTesting } from "@angular/common/http/testing";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
|
||||
describe('BootSoPartitionComponent', () => {
|
||||
let component: BootSoPartitionComponent;
|
||||
let fixture: ComponentFixture<BootSoPartitionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [BootSoPartitionComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
MatExpansionModule,
|
||||
MatMenuModule,
|
||||
BrowserAnimationsModule,
|
||||
MatTableModule,
|
||||
MatDividerModule,
|
||||
MatSelectModule,
|
||||
MatIconModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {
|
||||
clients: []
|
||||
}
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BootSoPartitionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,154 +0,0 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-boot-so-partition',
|
||||
templateUrl: './boot-so-partition.component.html',
|
||||
styleUrl: './boot-so-partition.component.css'
|
||||
})
|
||||
export class BootSoPartitionComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
selectedPartition: any = null;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
clientId: string | null = null;
|
||||
selectedClients: any[] = [];
|
||||
selectedModelClient: any = null;
|
||||
filteredPartitions: any[] = [];
|
||||
allSelected: boolean = false;
|
||||
clientData: any[] = [];
|
||||
loading: boolean = false;
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'diskNumber',
|
||||
header: 'Disco',
|
||||
cell: (partition: any) => partition.diskNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionNumber',
|
||||
header: 'Particion',
|
||||
cell: (partition: any) => partition.partitionNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'size',
|
||||
header: 'Tamaño',
|
||||
cell: (partition: any) => `${partition.size} MB`
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionCode',
|
||||
header: 'Tipo de partición',
|
||||
cell: (partition: any) => partition.partitionCode
|
||||
},
|
||||
{
|
||||
columnDef: 'filesystem',
|
||||
header: 'Sistema de ficheros',
|
||||
cell: (partition: any) => partition.filesystem
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => partition.operativeSystem?.name
|
||||
}
|
||||
];
|
||||
|
||||
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: { clients: any },
|
||||
private dialogRef: MatDialogRef<BootSoPartitionComponent>,
|
||||
private configService: ConfigService,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.clientId = this.data.clients?.length ? this.data.clients[0]['@id'] : null;
|
||||
|
||||
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === 'og-live') {
|
||||
client.selected = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedClients = this.data.clients.filter(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
);
|
||||
|
||||
this.selectedModelClient = this.data.clients.find(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
) || null;
|
||||
|
||||
if (this.selectedModelClient) {
|
||||
this.loadPartitions(this.selectedModelClient);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
loadPartitions(client: any) {
|
||||
const url = `${this.baseUrl}${client.uuid}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleClientSelection(client: any) {
|
||||
client.selected = !client.selected;
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
updateSelectedClients() {
|
||||
this.selectedClients = this.data.clients.filter(
|
||||
(client: { selected: boolean; state: string }) => client.selected && client.state === "og-live"
|
||||
);
|
||||
|
||||
if (!this.selectedClients.includes(this.selectedModelClient)) {
|
||||
this.selectedModelClient = null;
|
||||
this.filteredPartitions = [];
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === "og-live") {
|
||||
client.selected = this.allSelected;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.loading = true;
|
||||
this.http.post(`${this.baseUrl}/clients/server/boot-client`, {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
partition: this.selectedPartition['@id']
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,17 +8,11 @@
|
|||
[matMenuTriggerFor]="commandMenu">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
|
||||
<button mat-menu-item *ngSwitchCase="'menu-item'" [matMenuTriggerFor]="commandMenu" [disabled]="disabled">
|
||||
<mat-icon>{{ icon }}</mat-icon>
|
||||
<span>{{ buttonText }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<mat-menu #commandMenu="matMenu">
|
||||
<button mat-menu-item [disabled]="command.disabled
|
||||
|| (command.slug === 'create-image' && clientData.length > 1)" *ngFor="let command of arrayCommands"
|
||||
(click)="onCommandSelect(command.slug)">
|
||||
{{ command.translationKey | translate }}
|
||||
<button mat-menu-item [disabled]="command.disabled || (command.slug === 'create-image' && clientData.length > 1)"
|
||||
*ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
|
||||
{{ command.name }}
|
||||
</button>
|
||||
</mat-menu>
|
|
@ -1,4 +1,5 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ExecuteCommandComponent } from './execute-command.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
|
@ -16,18 +17,12 @@ import { ToastrModule, ToastrService } from 'ngx-toastr';
|
|||
import { DataService } from '../data.service';
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatMenu, MatMenuModule} from "@angular/material/menu";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('ExecuteCommandComponent', () => {
|
||||
let component: ExecuteCommandComponent;
|
||||
let fixture: ComponentFixture<ExecuteCommandComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ExecuteCommandComponent],
|
||||
imports: [
|
||||
|
@ -59,8 +54,7 @@ describe('ExecuteCommandComponent', () => {
|
|||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
}
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
|
|
@ -1,13 +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 { Router } from "@angular/router";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { BootSoPartitionComponent } from "./boot-so-partition/boot-so-partition.component";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { RemoveCacheImageComponent } from "./remove-cache-image/remove-cache-image.component";
|
||||
import { AuthService } from '@services/auth.service';
|
||||
import {SoftwareProfilePartitionComponent} from "./software-profile-partition/software-profile-partition.component";
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import {Router} from "@angular/router";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-command',
|
||||
|
@ -15,118 +11,41 @@ import {SoftwareProfilePartitionComponent} from "./software-profile-partition/so
|
|||
styleUrls: ['./execute-command.component.css']
|
||||
})
|
||||
export class ExecuteCommandComponent implements OnInit {
|
||||
@Input() runScriptContext: any = null;
|
||||
@Input() clientState: string = 'off';
|
||||
@Input() clientData: any[] = [];
|
||||
@Input() buttonType: 'icon' | 'text' | 'menu-item' = 'icon';
|
||||
@Input() buttonType: 'icon' | 'text' = 'icon';
|
||||
@Input() buttonText: string = 'Ejecutar Comandos';
|
||||
@Input() icon: string = 'terminal';
|
||||
@Input() disabled: boolean = false;
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
loading: boolean = true;
|
||||
|
||||
arrayCommands: any[] = [
|
||||
{ translationKey: 'executeCommands.powerOn', slug: 'power-on', disabled: false },
|
||||
{ translationKey: 'executeCommands.powerOff', slug: 'power-off', disabled: false },
|
||||
{ translationKey: 'executeCommands.reboot', slug: 'reboot', disabled: false },
|
||||
{ translationKey: 'executeCommands.login', slug: 'login', disabled: true },
|
||||
{ translationKey: 'executeCommands.createImage', slug: 'create-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.deployImage', slug: 'deploy-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.deleteImageCache', slug: 'remove-cache-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.partition', slug: 'partition', disabled: false },
|
||||
{ translationKey: 'executeCommands.softwareInventory', slug: 'software-inventory', disabled: false },
|
||||
{ translationKey: 'executeCommands.hardwareInventory', slug: 'hardware-inventory', disabled: true },
|
||||
{ translationKey: 'executeCommands.runScript', slug: 'run-script', disabled: false },
|
||||
{name: 'Enceder', slug: 'power-on', disabled: false},
|
||||
{name: 'Apagar', slug: 'power-off', disabled: false},
|
||||
{name: 'Reiniciar', slug: 'reboot', disabled: false},
|
||||
{name: 'Iniciar Sesión', slug: 'login', disabled: true},
|
||||
{name: 'Crear imagen', slug: 'create-image', disabled: false},
|
||||
{name: 'Clonar/desplegar imagen', slug: 'deploy-image', disabled: false},
|
||||
{name: 'Eliminar Imagen Cache', slug: 'delete-image-cache', disabled: true},
|
||||
{name: 'Particionar y Formatear', slug: 'partition', disabled: false},
|
||||
{name: 'Inventario Software', slug: 'software-inventory', disabled: true},
|
||||
{name: 'Inventario Hardware', slug: 'hardware-inventory', disabled: true},
|
||||
{name: 'Ejecutar script', slug: 'run-script', disabled: true},
|
||||
];
|
||||
|
||||
client: any = {};
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
public auth: AuthService,
|
||||
private dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private fb: FormBuilder,
|
||||
private router: Router,
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.clientData = this.clientData || [];
|
||||
const allowed = this.getAllowedCommandsByRole();
|
||||
this.arrayCommands = this.arrayCommands.filter(c => allowed.includes(c.slug));
|
||||
|
||||
this.updateCommandStates();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.updateCommandStates();
|
||||
}
|
||||
|
||||
private getAllowedCommandsByRole(): string[] {
|
||||
const role = this.auth.userCategory;
|
||||
|
||||
const permissions: Record<string, string[]> = {
|
||||
'super-admin': ['*'],
|
||||
'ou-admin': ['*'],
|
||||
'ou-operator': [
|
||||
'power-on',
|
||||
'power-off',
|
||||
'reboot',
|
||||
'login',
|
||||
'deploy-image',
|
||||
'software-inventory',
|
||||
'hardware-inventory',
|
||||
'remove-cache-image',
|
||||
'partition'
|
||||
],
|
||||
'ou-minimal': [
|
||||
'power-on',
|
||||
'power-off'
|
||||
]
|
||||
};
|
||||
|
||||
const allowed = permissions[role] || [];
|
||||
return allowed.includes('*') ? this.arrayCommands.map(c => c.slug) : allowed;
|
||||
}
|
||||
|
||||
|
||||
private updateCommandStates(): void {
|
||||
let states: string[] = [];
|
||||
|
||||
if (this.clientData.length > 0) {
|
||||
states = this.clientData.map(client => client.status);
|
||||
} else if (this.clientState) {
|
||||
states = [this.clientState];
|
||||
}
|
||||
|
||||
const allOffOrDisconnected = states.every(state => state === 'off' || state === 'disconnected');
|
||||
const allSameState = states.every(state => state === states[0]);
|
||||
const multipleClients = this.clientData.length > 1;
|
||||
|
||||
this.arrayCommands = this.arrayCommands.map(command => {
|
||||
if (allOffOrDisconnected) {
|
||||
command.disabled = command.slug !== 'power-on';
|
||||
} else if (allSameState) {
|
||||
if (states[0] === 'off' || states[0] === 'disconnected') {
|
||||
command.disabled = command.slug !== 'power-on';
|
||||
} else {
|
||||
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-image', 'partition', 'run-script', 'software-inventory'].includes(command.slug);
|
||||
}
|
||||
} else {
|
||||
if (command.slug === 'create-image'|| command.slug === 'software-inventory') {
|
||||
command.disabled = multipleClients;
|
||||
} else if (
|
||||
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'remove-cache-image', 'run-script'].includes(command.slug)
|
||||
) {
|
||||
command.disabled = false;
|
||||
} else {
|
||||
command.disabled = true;
|
||||
}
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
|
||||
onCommandSelect(action: any): void {
|
||||
|
@ -142,14 +61,6 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
this.openDeployImageAssistant();
|
||||
}
|
||||
|
||||
if (action === 'run-script') {
|
||||
this.openRunScriptAssistant();
|
||||
}
|
||||
|
||||
if (action === 'login') {
|
||||
this.loginClient();
|
||||
}
|
||||
|
||||
if (action === 'reboot') {
|
||||
this.rebootClient();
|
||||
}
|
||||
|
@ -161,18 +72,6 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
if (action === 'power-on') {
|
||||
this.powerOnClient();
|
||||
}
|
||||
|
||||
if (action === 'remove-cache-image') {
|
||||
this.removeImageCache();
|
||||
}
|
||||
|
||||
if (action === 'hardware-inventory') {
|
||||
this.hardwareInventory();
|
||||
}
|
||||
|
||||
if (action === 'software-inventory') {
|
||||
this.softwareInventory();
|
||||
}
|
||||
}
|
||||
|
||||
rebootClient(): void {
|
||||
|
@ -183,108 +82,11 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loginClient(): void {
|
||||
const clientDataToSend = this.clientData.map(client => ({
|
||||
name: client.name,
|
||||
mac: client.mac,
|
||||
uuid: '/clients/' + client.uuid,
|
||||
status: client.status,
|
||||
partitions: client.partitions,
|
||||
firmwareType: client.firmwareType,
|
||||
ip: client.ip
|
||||
}));
|
||||
|
||||
const dialogRef = this.dialog.open(BootSoPartitionComponent, {
|
||||
width: '70vw',
|
||||
height: 'auto',
|
||||
data: { clients: clientDataToSend }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success('Petición de arranque de SO enviada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeImageCache(): void {
|
||||
const clientDataToSend = this.clientData.map(client => ({
|
||||
name: client.name,
|
||||
mac: client.mac,
|
||||
uuid: '/clients/' + client.uuid,
|
||||
status: client.status,
|
||||
partitions: client.partitions,
|
||||
firmwareType: client.firmwareType,
|
||||
ip: client.ip
|
||||
}));
|
||||
|
||||
const dialogRef = this.dialog.open(RemoveCacheImageComponent, {
|
||||
width: '70vw',
|
||||
height: 'auto',
|
||||
data: { clients: clientDataToSend }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success('Petición de borrado de caché de imagen enviada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hardwareInventory(): void {
|
||||
if (this.clientData.length === 0) {
|
||||
this.toastService.error('No hay clientes seleccionados');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = this.clientData[0].uuid;
|
||||
|
||||
this.http.post(`${this.baseUrl}/clients/server/${clientId}/hardware-inventory`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Inventario de hardware actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
softwareInventory(): void {
|
||||
if (this.clientData.length === 0) {
|
||||
this.toastService.error('No hay clientes seleccionados');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientDataToSend = {
|
||||
clientId: this.clientData[0].uuid,
|
||||
name: this.clientData[0].name,
|
||||
mac: this.clientData[0].mac,
|
||||
status: this.clientData[0].status,
|
||||
partitions: this.clientData[0].partitions,
|
||||
firmwareType: this.clientData[0].firmwareType,
|
||||
ip: this.clientData[0].ip
|
||||
}
|
||||
|
||||
const clientId = this.clientData[0].uuid;
|
||||
|
||||
const dialogRef = this.dialog.open(SoftwareProfilePartitionComponent, {
|
||||
width: '70vw',
|
||||
height: 'auto',
|
||||
data: { client: clientDataToSend }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
powerOnClient(): void {
|
||||
this.http.post(`${this.baseUrl}/image-repositories/wol`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
|
@ -293,7 +95,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
this.toastService.success('Petición de encendido enviada correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -306,27 +108,16 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
this.toastService.success('Petición de apagado enviada correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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'], {
|
||||
queryParams: {
|
||||
clientData: JSON.stringify(clientDataToSend),
|
||||
runScriptContext: JSON.stringify(this.runScriptContext)
|
||||
}
|
||||
state: { clientData: this.clientData },
|
||||
}).then(r => {
|
||||
console.log('Navigated to partition assistant with data:', this.clientData);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -337,38 +128,10 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}
|
||||
|
||||
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'], {
|
||||
queryParams: {
|
||||
clientData: JSON.stringify(clientDataToSend),
|
||||
runScriptContext: JSON.stringify(this.runScriptContext)
|
||||
}
|
||||
state: { clientData: this.clientData },
|
||||
}).then(r => {
|
||||
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),
|
||||
runScriptContext: JSON.stringify(this.runScriptContext)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selected-client {
|
||||
background-color: #a0c2e5 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
<h2 mat-dialog-title> Seleccionar imagen para eliminar de la cache</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<div *ngIf="!loading" class="select-container">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Clientes </mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Listado de clientes para arrancar un SO
|
||||
<mat-icon>desktop_windows</mat-icon>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="toggleSelectAll()">
|
||||
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of data.clients" class="client-item">
|
||||
<div class="client-card"
|
||||
(click)="client.status === 'og-live' && toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}" >
|
||||
|
||||
<img
|
||||
[src]="'assets/images/computer_' + client.status + '.svg'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name | slice:0:20 }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
|
||||
<mat-radio-button [value]="client"
|
||||
color="primary"
|
||||
[disabled]="!client.selected"
|
||||
(click)="$event.stopPropagation()">
|
||||
Modelo
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div *ngIf="!loading" class="partition-table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar imagen</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group
|
||||
[(ngModel)]="selectedPartition"
|
||||
[disabled]="!row.operativeSystem"
|
||||
>
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef !== 'size' && column.columnDef !== 'operativeSystem'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'operativeSystem'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.operativeSystem?.name }} </span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.image?.name}} </span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
|
||||
</div>
|
|
@ -1,92 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RemoveCacheImageComponent } from './remove-cache-image.component';
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatCheckboxModule } from "@angular/material/checkbox";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatMenuModule } from "@angular/material/menu";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { MatTableModule } from "@angular/material/table";
|
||||
import { MatSelectModule } from "@angular/material/select";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { ToastrModule, ToastrService } from "ngx-toastr";
|
||||
import { TranslateModule } from "@ngx-translate/core";
|
||||
import { DataService } from "../../data.service";
|
||||
import { provideHttpClient } from "@angular/common/http";
|
||||
import { provideHttpClientTesting } from "@angular/common/http/testing";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
|
||||
describe('RemoveCacheImageComponent', () => {
|
||||
let component: RemoveCacheImageComponent;
|
||||
let fixture: ComponentFixture<RemoveCacheImageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RemoveCacheImageComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatExpansionModule,
|
||||
BrowserAnimationsModule,
|
||||
MatTableModule,
|
||||
MatDividerModule,
|
||||
MatSelectModule,
|
||||
MatRadioModule,
|
||||
MatIconModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {
|
||||
clients: [
|
||||
{
|
||||
'@id': '/clients/1',
|
||||
uuid: 'client-uuid-1',
|
||||
selected: false,
|
||||
status: 'og-live',
|
||||
state: 'og-live'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RemoveCacheImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,154 +0,0 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-remove-cache-image',
|
||||
templateUrl: './remove-cache-image.component.html',
|
||||
styleUrl: './remove-cache-image.component.css'
|
||||
})
|
||||
export class RemoveCacheImageComponent {
|
||||
baseUrl: string;
|
||||
selectedPartition: any = null;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
clientId: string | null = null;
|
||||
selectedClients: any[] = [];
|
||||
selectedModelClient: any = null;
|
||||
filteredPartitions: any[] = [];
|
||||
allSelected: boolean = false;
|
||||
clientData: any[] = [];
|
||||
loading: boolean = false;
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'diskNumber',
|
||||
header: 'Disco',
|
||||
cell: (partition: any) => partition.diskNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionNumber',
|
||||
header: 'Particion',
|
||||
cell: (partition: any) => partition.partitionNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'size',
|
||||
header: 'Tamaño',
|
||||
cell: (partition: any) => `${partition.size} MB`
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionCode',
|
||||
header: 'Tipo de partición',
|
||||
cell: (partition: any) => partition.partitionCode
|
||||
},
|
||||
{
|
||||
columnDef: 'filesystem',
|
||||
header: 'Sistema de ficheros',
|
||||
cell: (partition: any) => partition.filesystem
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => partition.operativeSystem?.name
|
||||
}
|
||||
];
|
||||
|
||||
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: { clients: any },
|
||||
private dialogRef: MatDialogRef<RemoveCacheImageComponent>,
|
||||
private configService: ConfigService,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.clientId = this.data.clients?.length ? this.data.clients[0]['@id'] : null;
|
||||
|
||||
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === 'og-live') {
|
||||
client.selected = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedClients = this.data.clients.filter(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
);
|
||||
|
||||
this.selectedModelClient = this.data.clients.find(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
) || null;
|
||||
|
||||
if (this.selectedModelClient) {
|
||||
this.loadPartitions(this.selectedModelClient);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
loadPartitions(client: any) {
|
||||
const url = `${this.baseUrl}${client.uuid}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0 && partition.image;
|
||||
});
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleClientSelection(client: any) {
|
||||
client.selected = !client.selected;
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
updateSelectedClients() {
|
||||
this.selectedClients = this.data.clients.filter(
|
||||
(client: { selected: boolean; state: string }) => client.selected && client.state === "og-live"
|
||||
);
|
||||
|
||||
if (!this.selectedClients.includes(this.selectedModelClient)) {
|
||||
this.selectedModelClient = null;
|
||||
this.filteredPartitions = [];
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === "og-live") {
|
||||
client.selected = this.allSelected;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.loading = true;
|
||||
this.http.post(`${this.baseUrl}/clients/server/remove-cache-image`, {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
partition: this.selectedPartition['@id']
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selected-client {
|
||||
background-color: #a0c2e5 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
<h2 mat-dialog-title> Seleccionar partición para inventariar</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div *ngIf="!loading" class="partition-table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar imagen</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group
|
||||
[(ngModel)]="selectedPartition"
|
||||
>
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef !== 'size' && column.columnDef !== 'operativeSystem'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'operativeSystem'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.operativeSystem?.name }} </span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.image?.name}} </span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue