Compare commits
3 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
3d62f51cff | |
|
|
c4e110c23e | |
|
|
c83b4b1705 |
|
|
@ -7,5 +7,3 @@ debian/*.substvars
|
||||||
debian/*.log
|
debian/*.log
|
||||||
debian/.debhelper/
|
debian/.debhelper/
|
||||||
debian/files
|
debian/files
|
||||||
.venv/
|
|
||||||
.vscode/
|
|
||||||
|
|
|
||||||
769
CHANGELOG.md
769
CHANGELOG.md
|
|
@ -1,782 +1,20 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [1.26.0] - 2026-11-04
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Restricción Declarativa de Comandos de Plugins según Estado de Cliente y SO Activo (`PluginService`, `PluginCommand`)**:
|
|
||||||
- Incorporadas las propiedades `activeOsOnly`, `excludeClientStatus` y `requireClientStatus` en la interfaz `PluginCommand`.
|
|
||||||
- La directiva `activeOsOnly: true` exige estrictamente que el equipo esté arrancado y en ejecución en uno de los sistemas operativos declarados en `os` (ej. Windows o Linux), impidiendo que se habiliten comandos de escritorio cuando el cliente se encuentra en entorno de mantenimiento `ogLive` o apagado aunque tenga una partición con dicho SO instalada en disco.
|
|
||||||
- Soportada exclusión e inclusión explícita por estado del cliente (`excludeClientStatus: ["og-live"]`, etc.) en `isCommandSupportedForClients`.
|
|
||||||
- En `ExecuteCommandComponent`, evaluado el estado unitario (`clientState`) para garantizar que la disponibilidad de comandos en el menú contextual de cada tarjeta individual responda fielmente al estado en tiempo real.
|
|
||||||
|
|
||||||
## [1.25.1] - 2026-11-01
|
|
||||||
|
|
||||||
### Refactored
|
|
||||||
- **Modularización y Optimización de Arquitectura en `PluginService`**:
|
|
||||||
- Descompuesto el monolito de ~900 líneas en submódulos especializados bajo `src/app/services/plugin/`:
|
|
||||||
- `plugin.types.ts`: Modelos, interfaces tipadas y estructuras de datos (`PluginCommand`, `RoleDefinition`, `PluginBadgeRule`, `PluginManifest`, etc.).
|
|
||||||
- `plugin-state.service.ts`: Manejo reactivo del estado dinámico de plugins en memoria, caché de metadatos contextuales y suscripción a eventos en tiempo real vía Mercure (Server-Sent Events).
|
|
||||||
- `plugin-roles.service.ts`: Comprobación de roles y permisos granulares de usuarios y comandos, jerarquía de acceso y sincronización con `ogCore`.
|
|
||||||
- `plugin-badge.service.ts`: Resolución desacoplada de badges visuales e internacionalización de tooltips dinámicos con metadatos.
|
|
||||||
- Eliminada la duplicación completa (~100 líneas) entre `getClientBadges()` y `getPartitionBadges()`, unificando la lógica de interpolación y renderizado en una función auxiliar común (`buildBadge`).
|
|
||||||
- `PluginService` actúa ahora como una fachada limpia, liviana y 100% retrocompatible (~280 líneas), preservando sin alteraciones todas las firmas públicas y re-exportando los tipos para los componentes consumidores.
|
|
||||||
|
|
||||||
## [1.25.0] - 2026-11-01
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Restricción Declarativa de Comandos de Plugins según Estado (`PluginService`, `ExecuteCommandComponent`)**:
|
|
||||||
- Incorporadas las propiedades opcionales `requireStatus` y `excludeStatus` en la interfaz `PluginCommand`, permitiendo a los plugins condicionar la disponibilidad de sus acciones en función del estado actual de los clientes.
|
|
||||||
- Implementado filtrado reactivo en `isCommandSupportedForClients`: los comandos solo se habilitan si el estado del equipo coincide con los estados requeridos (`requireStatus`) y no figura entre los estados excluidos (`excludeStatus`).
|
|
||||||
- Si un puesto de acceso libre (Aquarium) se encuentra desactivado (`DISABLED`), las opciones de reserva, liberación y renovación quedan deshabilitadas, permitiendo únicamente la acción de activación.
|
|
||||||
- Adaptación dinámica de etiqueta en comandos de conmutación de estado (`ogaquarium-toggle-status`): muestra "Activar puesto" o "Desactivar puesto" según el estado actual cuando se selecciona un único equipo.
|
|
||||||
- Suscripción reactiva a `statusUpdates$` en `ExecuteCommandComponent` para recalcular al instante la disponibilidad de acciones al recibir eventos Mercure en tiempo real.
|
|
||||||
|
|
||||||
## [1.24.7] - 2026-10-31
|
|
||||||
|
|
||||||
### Refactored
|
|
||||||
- **Gestión Nativa por Disco y Partición con Contrato Estandarizado en `PluginService`**:
|
|
||||||
- Eliminada la abstracción innecesaria de `subtarget`, operando directamente con las coordenadas nativas de OpenGnSys (`disk_number` y `partition_number`).
|
|
||||||
- La interfaz de usuario opera de manera 100% pasiva y desacoplada: es el plugin emisor (backend / listener Mercure / endpoint `/status`) quien suministra explícitamente tanto el estado global del cliente (`client_status` / `clientStatus`) como el estado de cada partición (`status` junto a `disk_number` y `partition_number`).
|
|
||||||
- Métodos nativos y directos en `PluginService`: `setPartitionPluginStatus`, `getPartitionPluginStatus`, `clearPartitionPluginStatus` y `getPartitionBadges(client, diskNumber, partitionNumber)`.
|
|
||||||
- Eliminadas las iteraciones o inferencias en `getClientBadges`: el badge general del cliente se consulta directamente en memoria sin realizar bucles de búsqueda sobre particiones ni calcular prioridades arbitrarias.
|
|
||||||
- Soporte completo de metadatos contextuales en tooltips tanto a nivel general del equipo como a nivel de cada partición individual.
|
|
||||||
|
|
||||||
## [1.24.6] - 2026-10-31
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Desacoplamiento Estricto y Sincronización Agnóstica en `PluginService`**:
|
|
||||||
- Eliminadas todas las cadenas hardcodeadas de estados específicos de plugins (`['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING']`, `'FREE'`, `'UNFROZEN'`).
|
|
||||||
- Implementada gestión genérica del ciclo de vida de particiones (`handlePartitionClear` y `syncGlobalStatusFromPartitions`): al recibir un estado `NONE` para una partición, se comprueba dinámicamente si al cliente le quedan otras particiones con estado activo; si no queda ninguna, se purga de forma inmediata el estado global en memoria evitando badges fantasma o residuales.
|
|
||||||
- La resolución de badges a nivel de equipo comprueba directamente contra el catálogo declarado por el plugin en su `manifest.json` (`plugin.badges`), eliminando filtros ad-hoc.
|
|
||||||
- El guardado y borrado de metadatos se gestiona de forma agnóstica según la presencia efectiva de datos contextuales en el payload.
|
|
||||||
|
|
||||||
## [1.24.5] - 2026-10-30
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Soporte Genérico de Tooltips Dinámicos (`PluginBadgeRule.tooltipFields`)**:
|
|
||||||
- Incorporada la especificación declarativa `tooltipFields` en `PluginBadgeRule`, permitiendo a cualquier plugin declarar en su propio manifiesto qué campos de metadatos deben anexarse al tooltip (con `prefix` y `suffix` opcionales).
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Actualización Reactiva en Tiempo Real y Auto-Conexión Mercure (`PluginService`, `groups.component`)**:
|
|
||||||
- La recepción de eventos SSE de Mercure en `PluginService` se ejecuta dentro de `NgZone.run()`, emitiendo a través del flujo observable reactivo `statusUpdates$`.
|
|
||||||
- Suscripción reactiva en `GroupsComponent` a `statusUpdates$`, activando la detección de cambios (`cd.markForCheck(); cd.detectChanges()`) de manera inmediata sin recargar ni salir de la pantalla.
|
|
||||||
- En `groups.component.html` y `client-details.component.html`, la expresión de seguimiento del control de flujo (`@for`) incorpora el tooltip (`track ... + (badge.tooltip || '')`), forzando la actualización instantánea de `[matTooltip]` en el DOM al cambiar los minutos restantes o datos.
|
|
||||||
- Auto-conexión garantizada de Mercure en `PluginService`: si la URL de Mercure se resuelve de forma asíncrona tras la carga de la configuración, se conecta de manera transparente al solicitar estados o badges.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- **Desacoplamiento Total de Plugins en `PluginService`**:
|
|
||||||
- Eliminado todo código ad-hoc y variables específicas de dominio (`dni`, `remaining_minutes`, `student`, `user`, etc.) del motor genérico de `PluginService`. `oggui` pasa a ser 100% agnóstico e independiente de los plugins instalados.
|
|
||||||
- Retirado el temporizador `interval(30000)` en `GroupsComponent`. La arquitectura se mantiene 100% reactiva exclusivamente mediante los eventos push de Mercure (SSE).
|
|
||||||
|
|
||||||
## [1.24.1] - 2026-10-30
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Detección Dinámica de Lenguaje y Resaltado en Editor de Scripts de Plugins (`PluginSettingsComponent`, `PluginService`)**:
|
|
||||||
- Corregido el badge de cabecera del editor de código en `plugin-settings` que mostraba estáticamente `POWERSHELL` incluso en scripts de Bash/Linux (`#!/bin/bash`).
|
|
||||||
- Incorporada la propiedad declarativa `language?: string` en `SchemaField` (`PluginService`), permitiendo a los plugins especificar directamente su sintaxis (ej. `bash`, `powershell`, `sh`).
|
|
||||||
- Detección automática inteligente como respaldo según el atributo `field.language`, el nombre de la clave (`linux`, `bash`, `ps1`, etc.) o el shebang del script (`#!/bin/bash`, `#!/bin/sh`).
|
|
||||||
- Adaptación contextual del motor de coloreado de sintaxis para comandos (`echo`, `mount`, `losetup`, `update-initramfs`, etc.), palabras clave (`if`, `fi`, `then`, `do`, `done`, etc.) y banderas de Linux (`--bind`, `-o`, etc.).
|
|
||||||
|
|
||||||
## [1.24.0] - 2026-10-28
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Control de Acceso y Visibilidad de Funciones de Plugins por Rol (`PluginService`, `PluginSettingsComponent`, `ExecuteCommandComponent`)**:
|
|
||||||
- Incorporada especificación declarativa de roles autorizados (`roles?: string[]`) en la interfaz `PluginCommand`.
|
|
||||||
- Creada matriz interactiva de permisos en `PluginSettingsComponent` dentro de la nueva subpestaña *"Funciones y Roles"*, permitiendo a los administradores activar o revocar gráficamente el acceso a cada comando expuesto por cualquier plugin para cada rol del sistema.
|
|
||||||
- Los roles disponibles se extraen dinámicamente desde `ogcore` (`GET /user-groups`), con respaldo seguro en los roles canónicos de OpenGnSys (`ROLE_SUPER_ADMIN`, `ROLE_ORGANIZATIONAL_UNIT_ADMIN`, `ROLE_ORGANIZATIONAL_UNIT_OPERATOR`, `ROLE_ORGANIZATIONAL_UNIT_MINIMAL`).
|
|
||||||
- La personalización se persiste en la base de datos dentro de `configData.command_roles` de forma 100% desacoplada sin requerir alteraciones en el esquema backend.
|
|
||||||
- En `PluginService.getPluginGroups()`, los comandos disponibles en los menús de acción (`ExecuteCommandComponent`) se filtran reactivamente en función del rol y permisos del usuario autenticado, ocultando totalmente aquellas opciones que no le correspondan (ej. un conserje con perfil básico solo visualiza Reservar, Liberar y Renovar puesto, permaneciendo invisibles las funciones de instalación o mantenimiento).
|
|
||||||
- Total adhesión a la sintaxis moderna de control de flujo Angular (`@if`, `@for`) y diseño modular de estilos CSS sin atributos inline.
|
|
||||||
- Soporte de internacionalización integral (`@ngx-translate`) con catálogos actualizados en español (`es.json`), inglés (`en.json`) y catalán (`cat.json`) para todos los textos, pestañas, cabeceras y notificaciones toast de la configuración de roles.
|
|
||||||
|
|
||||||
## [1.23.0] - 2026-10-27
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Enriquecimiento Contextual de Tooltips en Badges de Plugins (`PluginService`, `groups.component`)**:
|
|
||||||
- Incorporada caché reactiva de metadatos de clientes (`clientPluginMetadataMap`) en `PluginService`, extrayendo automáticamente atributos contextuales proporcionados por cualquier plugin tanto en la consulta inicial (`statusEndpoint`) como en las actualizaciones en tiempo real recibidas por Mercure.
|
|
||||||
- Al renderizar los badges (`getClientBadges`), si existen atributos contextuales como el identificador del alumno/usuario (`dni`, `user`) y el tiempo restante (`remaining_minutes`, `remainingMinutes`), se añaden dinámicamente al texto del tooltip (ej. `"Puesto reservado y en uso (DNI: 44212977P · 58 min restantes)"`), preservando el badge visualmente limpio y compacto (`Ocupado`, `A punto de expirar`).
|
|
||||||
- Limpieza automática y atómica de metadatos cuando el puesto queda libre (`FREE`) o se reinicia (`NONE`), manteniendo sincronizada la consola web en todo momento sin acoplamiento a ningún plugin en concreto.
|
|
||||||
|
|
||||||
## [1.22.0] - 2026-10-27
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Diálogos Dinámicos de Parámetros y Confirmación en Comandos de Plugins (`PluginCommandModalComponent`, `ExecuteCommandComponent`)**:
|
|
||||||
- Incorporada especificación de parámetros interactivos (`params?: CommandParam[]`) y mensajes de confirmación (`confirm?: string; confirmKey?: string`) en la interfaz declarativa `PluginCommand`.
|
|
||||||
- Creado el componente modal genérico `PluginCommandModalComponent` que renderiza dinámicamente formularios reactivos con soporte para campos de texto, números, selectores y conmutadores (`checkbox`), respetando validaciones de obligatoriedad (`required`) e internacionalización con `@ngx-translate`.
|
|
||||||
- En `ExecuteCommandComponent`, al disparar un comando de cualquier plugin que requiera parámetros o confirmación, se abre automáticamente el modal interactivo, inyectando los valores resultantes en el cuerpo de la petición POST enviada al endpoint del plugin de forma 100% desacoplada.
|
|
||||||
- Total adhesión a la sintaxis moderna de control de flujo Angular (`@if`, `@for`, `@switch`) y separación estricta de estilos sin atributos inline.
|
|
||||||
|
|
||||||
## [1.21.1] - 2026-10-27
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Desacoplamiento Total y Agnóstico en Plugins (`PluginService`, `PluginSettingsComponent`)**:
|
|
||||||
- Eliminado cualquier acoplamiento o referencia estática a nombres de plugins en la consola web (`PluginSettingsComponent`, `PluginService` y catálogos globales `locale/*.json`).
|
|
||||||
- El sistema opera de forma 100% agnóstica y dinámica: la presencia de la sub-pestaña "Historial y Registros" responde exclusivamente a la declaración de `recordsEndpoint` en el manifiesto de cualquier plugin.
|
|
||||||
- La internacionalización de plugins se carga dinámicamente en caliente mediante `@ngx-translate` consultando el endpoint `/i18n/{lang}` de cada plugin, respetando la estructura estándar de namespaces anidados como en `ogfreeze`.
|
|
||||||
|
|
||||||
## [1.21.0] - 2026-10-26
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Sub-pestaña Genérica de Historial y Registros en Plugins (`PluginManifest`, `PluginSettingsComponent`)**:
|
|
||||||
- Incorporado soporte en `PluginManifest` para `recordsEndpoint` y `recordsColumns` (`RecordColumn`), permitiendo a cualquier plugin declarar un origen de datos tabular desacoplado.
|
|
||||||
- Añadido selector de sub-pestañas en `plugin-settings` para alternar entre "Configuración" e "Historial y Registros".
|
|
||||||
- Vista de registros completa con buscador en tiempo real (por DNI, puesto o aula), ordenación cronológica, paginación reactiva y contador de resultados.
|
|
||||||
- Soporte de exportación a archivo CSV con descarga directa desde backend (`${recordsEndpoint}/export`) y fallback automático de generación en cliente con BOM UTF-8 para compatibilidad universal.
|
|
||||||
- Renderizado adaptativo de celdas con soporte de badges semánticos para identificadores (DNI), duraciones y motivos de cierre de sesión (`TIMEOUT`, `RELEASED_BY_CONCIERGE`, `DISABLED_BY_ADMIN`).
|
|
||||||
- Total adhesión a la sintaxis moderna de control de flujo Angular (`@if`, `@for`, `@switch`) y estilos reutilizables sin atributos inline.
|
|
||||||
|
|
||||||
## [1.20.0] - 2026-10-26
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Soporte de Estados y Badges a Nivel de Partición en Plugins (`PluginService`, `groups.component`)**:
|
|
||||||
- Incorporada gestión granular de estados por partición (`${plugin}:${clientId}:${disk}:${partition}`).
|
|
||||||
- En eventos Mercure con `status === 'NONE'` en una partición específica, se limpia la clave de la partición y cualquier estado transitorio global obsoleto (`REMOVING`, `CHECKING`, etc.), permitiendo que `getClientBadges` evalúe de inmediato y preserve el badge activo de cualquier otra partición protegida del equipo.
|
|
||||||
- Soporte de invalidación granular por disco y partición en eventos de imagen y particionado.
|
|
||||||
|
|
||||||
## [1.19.0] - 2026-10-23
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Activación y Desactivación Dinámica de Plugins (`PluginService`, `plugins.component`, `plugin-settings.component`)**:
|
|
||||||
- Incorporada la capacidad de activar y desactivar plugins en caliente sin desinstalarlos del sistema.
|
|
||||||
- Al desactivar un plugin, sus comandos contextuales y badges se ocultan automáticamente de las tarjetas y menús de clientes en toda la consola web, preservando íntegra su configuración en base de datos.
|
|
||||||
- En la vista de administración `/plugins`, se integró un interruptor deslizante (`mat-slide-toggle`) en la cabecera de configuración para conmutar entre `Activo` e `Inactivo` con notificación toast y actualización reactiva.
|
|
||||||
- Añadido indicador visual de estado y conteo en la cabecera del panel (`X Activo(s) / Y Instalado(s)`) y badge `Inactivo` en las pestañas de los plugins deshabilitados.
|
|
||||||
|
|
||||||
## [1.18.0] - 2026-10-23
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Desacoplamiento Total de Plugins y Eventos Mercure (`PluginService`, `groups.component`, `execute-command.component`)**:
|
|
||||||
- `PluginService`: Suscripción dinámica y agnóstica al topic genérico de Mercure `plugins`, procesando eventos de cualquier plugin de forma desacoplada y actualizando el estado de los clientes en tiempo real sin hardcoding.
|
|
||||||
- Consulta agnóstica de endpoints de estado (`statusEndpoint`) y reglas de badges (`badges`) declaradas dinámicamente en los manifiestos de los plugins.
|
|
||||||
- Inferencia dinámica de badges contextuales en clientes (`getClientBadges`) mapeados directamente desde la especificación `badges` del manifiesto del plugin.
|
|
||||||
- Limpieza de estilos en `groups.component.css`, adoptando clases semánticas universales (`.badge-info`, `.badge-neutral`, `.badge-pending`, `.badge-error`, `.badge-success`, `.badge-warning`).
|
|
||||||
- **Sistema Estandarizado de Internacionalización Dinámica de Plugins (`PluginService`, `plugins.component`, `plugin-settings.component`, `execute-command.component`)**:
|
|
||||||
- Incorporada la carga e inyección en caliente de traducciones (`i18n`) desde el endpoint `/api/plugins/{name}/i18n/{lang}` de cada plugin directamente en `@ngx-translate`, sin necesidad de alterar los ficheros de idioma en disco ni requerir recompilación.
|
|
||||||
- Soporte completo para claves semánticas (`titleKey`, `descriptionKey`, `labelKey`, `tooltipKey`) en comandos, badges y formularios de configuración de plugins, con reactividad automática ante el cambio de idioma en la cabecera.
|
|
||||||
|
|
||||||
## [1.17.2] - 2026-10-22
|
|
||||||
|
|
||||||
### Revert
|
|
||||||
- **Cancelación de Tareas (`task-logs.component`)**:
|
|
||||||
- Revertida la modificación temporal que permitía seleccionar y cancelar trazas en estado `Enviado` (`sent`). Se mantiene la regla del sistema que solo permite seleccionar y cancelar trazas en estado `pending` o `in-progress`.
|
|
||||||
|
|
||||||
## [1.17.0] - 2026-10-22
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Encolado Inteligente por Sistema Operativo Destino (`execute-command.component`)**:
|
|
||||||
- Al ejecutar comandos de plugins específicos de un sistema operativo (como `ogfreeze` para Windows), si hay equipos apagados, desconectados o actualmente iniciados en un SO diferente (ej. `og-live`), se ofrece la confirmación de encolado (`QueueConfirmationModalComponent`) enviando el metadato `target_os`.
|
|
||||||
- La tarea encolada esperará de forma segura en `ogcore` a que el equipo arranque en el sistema operativo compatible antes de ser despachada.
|
|
||||||
|
|
||||||
## [1.16.0] - 2026-10-22
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Soporte Dinámico para Plugins (`PluginService`, `PluginsComponent`, `PluginSettingsComponent`)**:
|
|
||||||
- Incorporada infraestructura agnóstica para descubrimiento y gestión de plugins desacoplados a través de `/api/plugins`.
|
|
||||||
- Agrupación ordenada de comandos de plugins en submenús dedicados en la consola de comandos (`execute-command`), organizados por plugin para mantener el menú limpio.
|
|
||||||
- Comprobación y filtrado dinámico de compatibilidad por Sistema Operativo (`os: ['windows']`), deshabilitando automáticamente los comandos en puestos con sistemas incompatibles.
|
|
||||||
- Estándar de Contrato y Acciones de Campos para Plugins (`PluginSettingsComponent`): Incorporado soporte para acciones declarativas por campo (`field.action`) ejecutadas contra el backend del plugin mediante el contrato estándar `{ config: ... }` -> `{ data: { [key]: value }, message: "..." }`. Los botones de asistencia (como la generación de scripts) se integran directamente en la cabecera de la caja del script y la lógica reside 100% en el backend del plugin, manteniendo el núcleo de `oggui` completamente agnóstico y desacoplado.
|
|
||||||
- Soporte de insignias de estado (badges) contextuales por cliente en las vistas de mosaico y lista de equipos (`groups.component`).
|
|
||||||
- Nuevo elemento condicional "Plugins" en el menú lateral (`sidebar`), visible únicamente cuando existen plugins activos en el sistema.
|
|
||||||
- Resaltador de sintaxis enriquecido y visor de terminal para scripts de configuración con numeración de líneas, colores semánticos (comentarios en verde itálica, comandos, parámetros, cadenas), botón de copiado rápido y alternancia entre vista coloreada y editor manual.
|
|
||||||
- Comportamiento 100% neutro e idéntico a 1.15.0 si no hay plugins instalados.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Recarga de página en Plugins (`plugins.component`, `plugin-settings.component`)**: Corregido el problema de sincronización reactiva al refrescar directamente (F5) la vista de `/plugins`, añadiendo un estado de carga visual y suscripción a `plugins$` para evitar mostrar prematuramente la pantalla de "No hay plugins instalados".
|
|
||||||
- **Rutas excluidas vacías en ogfreeze (`plugin-settings.component`)**: Corregida la regeneración de scripts cuando el campo de rutas excluidas se deja intencionadamente en blanco; ahora omite de forma limpia el bloque de exclusiones en lugar de forzar los valores predeterminados.
|
|
||||||
- **Ejecución y Validación de Comandos de Plugins (`execute-command.component`)**: Implementado el envío HTTP real de las órdenes de plugins (`POST /api/plugins/...`), validación de estado del SO activo (aviso si están ejecutando Linux u og-live cuando se requiere Windows) y diálogo de confirmación de encolado (`QueueConfirmationModalComponent`) cuando los equipos seleccionados se encuentran apagados o desconectados.
|
|
||||||
|
|
||||||
## [1.15.0] - 2026-10-20
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Ejecución y Encolado Automático en Clientes Offline (`execute-command`, `boot-so-partition`, `queue-confirmation-modal`)**:
|
|
||||||
- Permitida la ejecución de comandos y scripts (incluyendo inicio de sesión en partición / arranque de SO) sobre equipos apagados o grupos con estados mixtos.
|
|
||||||
- El modal de confirmación detecta automáticamente equipos apagados o estados no ejecutables de inmediato, pre-seleccionando la opción de encolado y mostrando avisos visuales informativos (disponible en ES, EN y CA).
|
|
||||||
- Unificada y estandarizada la matriz de estados de todos los comandos en la consola de comandos.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Selección de Clientes en Unidades Organizativas (`groups.component.html`)**: Solucionado el problema al invocar comandos o tareas desde el menú contextual de un Aula / OU donde solo se seleccionaban los equipos mostrados en la página actual de la tabla en lugar de todos los clientes pertenecientes a la OU.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **Refactorización Angular (`boot-so-partition`)**: Migradas directivas estructurales (`*ngIf`, `*ngFor`) a la sintaxis moderna de control de flujo (`@if`, `@for`) y extraídos los estilos inline a clases CSS reutilizables.
|
|
||||||
|
|
||||||
## [1.14.4] - 2026-10-09
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **UI/UX (Componentes Genéricos)**: Solucionado un error en los componentes de añadir equipos a una subred (`add-clients-to-subnet`) y configuración de ficheros de arranque PXE (`pxe-boot-files`) introducido en versiones previas. Estos componentes enviaban un identificador URI (ej. `/organizational-units/uuid`) o el propio UUID en sus peticiones de búsqueda (GET) en lugar del ID numérico esperado por la base de datos (Doctrine), lo que impedía listar los equipos de las aulas seleccionadas. Ahora resuelven correctamente el ID numérico de la unidad seleccionada antes de realizar la petición al backend.
|
|
||||||
|
|
||||||
## [1.14.3] - 2026-10-09
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **UI/UX (Componentes Genéricos)**: Solucionado un problema de regresión introducido en la versión 1.14.2 donde múltiples componentes de creación y edición (Crear/Editar Tarea, Usuarios, Aula, Cliente, Crear Múltiples Clientes y Mover Clientes) enviaban un identificador numérico en el payload en lugar del identificador URI (`@id`) esperado por el backend para las relaciones de `organizationalUnit`, provocando errores al intentar guardar. Ahora, estos componentes detectan automáticamente el identificador devuelto por el selector genérico y construyen la URI correcta antes de realizar la petición HTTP.
|
|
||||||
|
|
||||||
## [1.14.2] - 2026-10-06
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **UI/UX (Componentes Genéricos)**: Refactorizado el comportamiento de `app-ou-select` para que devuelva siempre el identificador URI (`@id`) de la unidad organizativa en lugar de su ID numérico. Esto corrige varios errores críticos en cadena (como el error al mover clientes) provocados porque la API de Symfony (API Platform) requiere referencias URI (`/api/organizational_units/X`) para mapear entidades relacionales, en vez de identificadores enteros puros. Adicionalmente, el componente de mover clientes se ha hecho más robusto para prevenir envíos mal formateados.
|
|
||||||
|
|
||||||
## [1.14.1] - 2026-10-06
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **DHCP Subnets**: Solucionado un error al añadir equipos a una subred donde la petición al servidor enviaba la unidad organizativa como "undefined", impidiendo listar los equipos del aula seleccionada.
|
|
||||||
|
|
||||||
## [1.14.0] - 2026-10-05
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **UI/UX**: Añadido buscador en todos los desplegables de selección de Unidad Organizativa gracias al nuevo componente reutilizable `OuSelectComponent`.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **UI/UX**: Estandarizada la selección de Unidades Organizativas en toda la consola para mostrar de forma consistente la ruta jerárquica (breadcrumb) del elemento.
|
|
||||||
- **Refactor**: Extraída la lógica de selección de Unidades Organizativas a un componente genérico (`OuSelectComponent`) para cumplir con el principio DRY.
|
|
||||||
- **Refactor**: Migrada la sintaxis de plantillas (directivas estructurales `*ngIf`, `*ngFor`, `*ngSwitch`) a la nueva sintaxis de control de flujo de Angular 18 (`@if`, `@for`, `@switch`) en los componentes afectados por la refactorización.
|
|
||||||
|
|
||||||
## [1.13.0] - 2026-10-02
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **RemotePC**: Añadido un botón deslizable (toggle) en el panel de configuración de Unidades Organizativas (aulas) para habilitar el reinicio de equipos en lugar de apagado cuando son liberados por UDS.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **UI/UX**: Solucionado un error de compilación y renderizado en `partition-assistant.component.css` causado por la inserción involuntaria de caracteres nulos (UTF-16) ocultos en el código fuente.
|
|
||||||
|
|
||||||
## [1.12.0] - 2026-09-30
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **UI/UX**: Implementada persistencia en localStorage para el ancho del panel lateral del árbol de jerarquías, individualizada por usuario.
|
|
||||||
- **RemotePC**: Añadida la insignia indicadora de conexión remota en la vista de lista (tabla) de clientes para unificar la experiencia con la vista de tarjetas.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **UI/UX**: Refinados los estilos visuales de la insignia de RemotePC (colores sutiles, bordes redondeados y tamaño reducido) para armonizar con el resto de etiquetas (ej: velocidad de red).
|
|
||||||
|
|
||||||
## [1.11.1] - 2026-09-24
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **UI Layout**: Corregido el problema de desbordamiento de barras de desplazamiento (scrollbars) globales forzando un diseño con flexbox en el layout principal de los clientes.
|
|
||||||
- **Jerarquía de Unidades Organizativas**: Se ha reemplazado la visualización del "padre inmediato" por una ruta recursiva completa (ej: Sede / Edificio / Planta / Aula) en los desplegables de aulas de ogDHCP y ogBoot.
|
|
||||||
- **Auto-selección de PXE**: Al elegir una partición destino en la ventana de "Despliegue de Imagen" y seleccionar "Cambiar Plantilla PXE", el sistema auto-seleccionará automáticamente la plantilla correspondiente al disco y partición (ej: `firstDisk_firstPartition`).
|
|
||||||
|
|
||||||
## [1.11.0] - 2026-09-23
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Desplegar Imagen**: Añadidas acciones post-despliegue ("Iniciar Sistema Restaurado" y "Seleccionar Plantilla PXE") que permiten encadenar comandos de configuración de inicio y reinicio directamente tras programar el despliegue.
|
|
||||||
- **Preferencias de Usuario**: Las opciones elegidas para el despliegue (acción post-despliegue y tipo de PXE) se persisten automáticamente en el navegador de manera segura e individual por usuario.
|
|
||||||
|
|
||||||
## [1.10.2] - 2026-09-22
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Crear/Desplegar Imagen**: Eliminada la dirección IP del listado de repositorios en los selectores de despliegue, mostrando únicamente el nombre del repositorio.
|
|
||||||
|
|
||||||
## [1.10.1] - 2026-09-18
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Editor de Menús**: Corregida la previsualización del diseño HTML en entornos con proxy inverso (Nginx). Se delega la resolución de los 'assets' al backend proporcionándole el `baseUrl` completo, asegurando que las imágenes de los iconos se carguen correctamente tanto en la "Vista Global" como al previsualizar desde "Editar HTML".
|
|
||||||
|
|
||||||
## [1.10.0] - 2026-09-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Permisos de Operador (ou-operator)**:
|
|
||||||
- Añadido permiso explícito para que los operadores de aula puedan ejecutar el comando `createImage`.
|
|
||||||
- Restringida la creación de nuevas unidades organizativas de tipo "Centro" para los usuarios con rol de operador.
|
|
||||||
|
|
||||||
## [1.9.0] - 2026-09-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **Visualización y Estructura Organizativa**:
|
|
||||||
- Implementados diferentes colores para las etiquetas de velocidad de las tarjetas de red en la vista de equipos, facilitando su identificación visual.
|
|
||||||
- Añadido un botón de búsqueda interactivo en el árbol organizativo (OUs) que permite ocultar/mostrar los campos de búsqueda para ahorrar espacio visual en pantalla.
|
|
||||||
- Componentes `pxe-boot-files` y `add-clients-to-subnet`: Añadido buscador en los menús desplegables (dropdowns) de selección de unidades organizativas y aulas, facilitando la búsqueda en jerarquías grandes.
|
|
||||||
|
|
||||||
- **Creación de Imágenes**:
|
|
||||||
- Permitida la creación de imágenes (`createImage`) a partir de particiones de datos que no contienen Sistema Operativo (Data partitions).
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Refactorización de plantillas HTML en `groups`, `pxe-boot-files` y `add-clients-to-subnet` para migrar de directivas estructurales legacy (`*ngIf`, `*ngFor`) a la nueva sintaxis de control de flujo de Angular (`@if`, `@for`).
|
|
||||||
## [1.8.3] - 2026-07-31
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Inventario Hardware: Añadida visualización gráfica en la interfaz web para los nuevos campos reportados por los clientes.
|
|
||||||
- Las vistas "Tarjeta" y "Lista" de equipos muestran ahora un indicador ('badge') con la velocidad de la interfaz de red (ej. 1000 Mbps).
|
|
||||||
- El Asistente de Particionado muestra el tipo de disco físico ('SATA', 'NVMe', 'USB', etc.) junto al nombre del disco.
|
|
||||||
|
|
||||||
## [1.8.2] - 2026-07-27
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Task Logs (`task-logs.component`): Consume `output` and `finishedAt` properties from Mercure events to update trace outputs in real-time without reloading the page.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Creación de imágenes (`create-image.component`): Al seleccionar la acción "Actualizar Imagen", ahora se filtran correctamente las imágenes para mostrar solo aquellas que pertenecen al repositorio objetivo asignado al cliente.
|
|
||||||
- Menú de opciones de cliente (`groups.component.html`): Unificado el menú de acciones (tres puntos verticales) usando `matMenuContent`. Se elimina la duplicidad de código que causaba divergencias de comportamiento entre la vista tarjeta y la vista lista. Ahora la opción de "Logs en tiempo real" está siempre disponible en ambas vistas, al igual que los bloqueos al sincronizar.
|
|
||||||
- Detalles de tareas (`OutputDialogComponent`): Ampliado el ancho de la ventana modal de detalles a `70vw` para facilitar la lectura de trazas largas y comandos de OGAgent/OGLive.
|
|
||||||
- Repositorios Git (`show-git-images.component`): Corregido el cierre abrupto de la ventana modal al borrar un repositorio. Ahora, la interfaz recarga la lista de repositorios, permitiendo seguir operando sin tener que volver a abrir el gestor.
|
|
||||||
|
|
||||||
## [1.8.1] - 2026-07-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Formulario de despliegue (`Deploy Image`): Añadido selector de repositorio origen (OG Repository) al menú de despliegue para permitir al usuario elegir desde dónde descargar la imagen.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Modal de partición de arranque (`boot-so-partition`): Refactorizado para usar el componente compartido `app-disk-partition-selector`, eliminando código duplicado y unificando el diseño y comportamiento de la selección de discos y particiones con el resto de modales.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Detalles de tareas (`OutputDialogComponent`): Corregido el formateo de los detalles mostrados desde los agentes (OGAgent) para renderizar el texto en crudo manteniendo los saltos de línea (usando pre-wrap por CSS y <pre> tag), y dándole formato monoespaciado en lugar de intentar forzar un parseo estricto JSON que rompía la salida de comandos.
|
|
||||||
- Formulario de despliegue (`Deploy Image`): Solucionado el problema que ocultaba las comandos (instructions) cuando el despliegue era en modo caché. El código ha sido refactorizado y limpiado usando directivas estructurales estándar de Angular (`*ngIf`, `*ngFor`).
|
|
||||||
- Roles: Permisos actualizados en vistas para reflejar recientes cambios de backend en la gestión de perfiles de software.
|
|
||||||
|
|
||||||
## [1.8.0] - 2026-06-16
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Sistema de permisos por rol en la interfaz gráfica para los cuatro perfiles:
|
|
||||||
`ROLE_SUPER_ADMIN`, `ROLE_ORGANIZATIONAL_UNIT_ADMIN`,
|
|
||||||
`ROLE_ORGANIZATIONAL_UNIT_OPERATOR` y `ROLE_ORGANIZATIONAL_UNIT_MINIMAL`.
|
|
||||||
- `roleGuard`: protección de rutas según rol; ou-operator accede a operaciones
|
|
||||||
de equipos (despliegue, particionado, scripts, imagen); ou-minimal y
|
|
||||||
ou-operator son redirigidos a `/groups` si intentan acceder a rutas de
|
|
||||||
administración.
|
|
||||||
- Botones de creación/edición de infraestructura (DHCP, Boot servers,
|
|
||||||
repositorios, comandos) ocultos para ou-admin con `*ngIf`.
|
|
||||||
- Botones de creación de OU y cliente ocultos para ou-operator y ou-minimal.
|
|
||||||
- Tests unitarios de permisos: `auth.service.spec.ts` (5 roles × métodos de
|
|
||||||
rol), `role.guard.spec.ts` (matriz 26 rutas × 4 roles),
|
|
||||||
`sidebar.component.spec.ts` (visibilidad de ítems por rol).
|
|
||||||
- Infraestructura E2E con Playwright: `playwright.config.ts`, helpers de
|
|
||||||
autenticación y API, y 4 specs de permisos (super-admin, ou-admin,
|
|
||||||
ou-operator, ou-minimal) que verifican sidebar, rutas y botones.
|
|
||||||
- Botón de cerrar en la ventana de plano de aula.
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- La petición de equipos al hacer clic en un aula y la actualización de su
|
|
||||||
estado se ejecutan de forma secuencial en lugar de en paralelo, evitando
|
|
||||||
el loader general mientras se resuelven los estados individuales.
|
|
||||||
- Ficheros de idiomas (es, en, cat) ordenados alfabéticamente; revisión
|
|
||||||
completa del fichero catalán.
|
|
||||||
|
|
||||||
## [1.7.0] - 2026-05-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Visor de logs de disco en las páginas de estado de ogcore, ogboot, ogdhcp
|
|
||||||
y ogrepository: muestra entries del journal en tiempo real sin necesidad
|
|
||||||
de oglog/OpenSearch, con polling cada 5 segundos y modo live/pausa
|
|
||||||
- Soporte de múltiples `SYSLOG_IDENTIFIER` por componente: ogdhcp incluye
|
|
||||||
kea, ogrepository incluye samba, ogboot incluye tftpd-hpa
|
|
||||||
- Host basado en IP del servidor seleccionado para lectura del fichero de
|
|
||||||
journal remoto (`/var/log/journal/remote/<IP>.journal`)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- `StatusLogsComponent`: `@Input() identifier` reemplazado por
|
|
||||||
`@Input() identifiers: string[]`; respuesta parseada como array de entries
|
|
||||||
JSON estructurados con formato `[timestamp] IDENTIFIER: mensaje`
|
|
||||||
|
|
||||||
## [1.6.1] - 2026-05-14
|
|
||||||
### Improved
|
|
||||||
- Modificada recepcion de mensajes de mercure durante la importación
|
|
||||||
|
|
||||||
## [1.6.0] - 2026-05-08
|
|
||||||
### Added
|
|
||||||
- Añadida opción en Administración para importar una base de datos OG 1.1 en la nueva version. Permite subir fichero .sql o .zip o usar parametros de conexión a la base de datos de OG 1.1
|
|
||||||
|
|
||||||
## [1.5.9] - 2026-05-07
|
|
||||||
### Fixed
|
|
||||||
- Asistente de particionado: en discos GPT se elimina el límite de 4 particiones primarias, se oculta la opción de añadir partición lógica/extendida y se suprime el estilo visual de partición lógica (borde amarillo e icono └)
|
|
||||||
### Added
|
|
||||||
- En acciones programadas, la acción "Iniciar Sesión" ahora permite configurar disco y partición de arranque como parámetros
|
|
||||||
|
|
||||||
## [1.5.8] - 2026-05-06
|
|
||||||
### Added
|
|
||||||
- Añadido polling en la vista de logs de disco para refresco automático en tiempo real
|
|
||||||
|
|
||||||
## [1.5.7] - 2026-05-05
|
|
||||||
### Fixed
|
|
||||||
- Modificada vista para eliminar imagen de cache, se muestra la lista de ficheros de imagen contenida en la cache y se borra el fichero
|
|
||||||
|
|
||||||
## [1.5.6] - 2026-05-05
|
|
||||||
### Fixed
|
|
||||||
- Corregida edicion de menus
|
|
||||||
|
|
||||||
## [1.5.5] - 2026-05-04
|
|
||||||
### Fixed
|
|
||||||
- Corregida gestion de oglives
|
|
||||||
|
|
||||||
## [1.5.4] - 2026-04-24
|
|
||||||
### Improved
|
|
||||||
- Modificado script de post instalación debian para añadir a config.json las nuevas variables.
|
|
||||||
|
|
||||||
## [1.5.3] - 2026-04-24
|
|
||||||
### Improved
|
|
||||||
- Corregidas traducciones
|
|
||||||
|
|
||||||
## [1.5.2] - 2026-04-24
|
|
||||||
### Added
|
|
||||||
- Añadido campo resolución en los ajustes de red de aulas y grupos para propagarlo a los equipos.
|
|
||||||
- Rediseño del menú de configuracion de una Unidad Organizativa
|
|
||||||
|
|
||||||
## [1.5.1] - 2026-04-23
|
|
||||||
### Improved
|
|
||||||
- Creadas claves de traducción para idiomas, añadido menu para cambiar de idioma en la barra superior
|
|
||||||
|
|
||||||
## [1.5.0] - 2026-04-21
|
|
||||||
### Added
|
|
||||||
- Añadida gestión de múltiples ogBoot Servers
|
|
||||||
- Si es un ogBoot existente, permite sincronizar su información sobre oglives y plantillas pxe.
|
|
||||||
- En los Ajustes de red de equipos, aulas y grupos, ahora se debe seleccionar servidor ogBoot y luego oglive y plantilla dentro de dicho servidor.
|
|
||||||
|
|
||||||
## [1.4.0] - 2026-04-16
|
|
||||||
### Added
|
|
||||||
- Añadida gestión de múltiples Dhcps
|
|
||||||
- Si el Dhcp tiene ordenadores añadidos en su fichero kea, se pueden sincronizar para añadirlos a la estructura de Grupos actual.
|
|
||||||
|
|
||||||
## [1.3.3] - 2026-04-13
|
|
||||||
### Improved
|
|
||||||
- Mejorada la visualizacion del selector de particiones mostrando el contenido de cache y la imagen asociada a la partición.
|
|
||||||
- Mejorada usabilidad de la funcion updateCache para que no haga falta elegir disco y particion para lanzar el comando.
|
|
||||||
|
|
||||||
## [1.3.2] - 2026-04-13
|
|
||||||
### Added
|
|
||||||
- Añadida opción para restaurar sólo cache o cache y particion.
|
|
||||||
|
|
||||||
## [1.3.1] - 2026-04-09
|
|
||||||
### Added
|
|
||||||
- Añadida información de la fecha de despliegue de una imagen en una particion.
|
|
||||||
|
|
||||||
## [1.3.0] - 2026-03-31
|
|
||||||
### Added
|
|
||||||
- Añadido comando "Refrescar" tanto en un cliente como en un grupo de clientes, de forma que se ejecuta el comando Actualizar en el ogAgent y se actualiza la información de las particiones del cliente y se recarga el menú del ogbrowser.
|
|
||||||
|
|
||||||
## [1.2.7] - 2026-03-31
|
|
||||||
### Added
|
|
||||||
- Rediseño de la plantilla para seleccion de particiones, se ha creado un componente compartido por la vista de crear imagen y deploy de imagenes.
|
|
||||||
|
|
||||||
## [1.2.6] - 2026-03-30
|
|
||||||
### Added
|
|
||||||
- Rediseño de la pantalla detalles de un equipo, en la partición CACHE, se muestra el listado de ficheros que contiene con el tamaño de cada uno.
|
|
||||||
|
|
||||||
## [1.2.5] - 2026-03-27
|
|
||||||
### Fixed
|
|
||||||
- Corregido error 401 Unauthorized en la edición de menús con nombres que contienen la palabra "login". Se ha refinado el interceptor para aplicar el prefijo "Bearer" de forma selectiva mediante validación exacta del path `/auth/login`.
|
|
||||||
|
|
||||||
## [1.2.4] - 2026-03-20
|
|
||||||
### Added
|
|
||||||
- Se ha mejorado el editor de menus para poder editar directamente la plantilla twig en lugar del html renderizado.
|
|
||||||
|
|
||||||
## [1.2.3] - 2026-03-17
|
|
||||||
### Added
|
|
||||||
- Se ha creado una nueva opción para gestionar assets en la pantalla de menus.
|
|
||||||
|
|
||||||
## [1.2.2] - 2026-03-17
|
|
||||||
### Added
|
|
||||||
- Se ha integrado un editor interactivo en texto plano para los menús HTML directamente en la pantalla de edición, permitiendo previsualizar y modificar el código en vivo sin necesidad de resubir archivos. Utiliza la sintaxis moderna `@if/@else` de Angular 17+ para su lógica de renderizado.
|
|
||||||
|
|
||||||
## [1.2.1] - 2026-03-11
|
|
||||||
### Added
|
|
||||||
- Se ha añadido la funcionalidad para poder adjuntar un fichero HTML local en la creación/edición de menús, codificándolo y enviándolo en Base64 de forma transparente.
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- Se ha mejorado la UX integrando el campo de subida de ficheros en el formulario de menús (`create-menu`) utilizando la estética nativa de Angular Material.
|
|
||||||
|
|
||||||
## [1.2.0] - 2025-10-21
|
|
||||||
### Added
|
|
||||||
- Se ha añadido una nueva funcionalidad la cual permite poder borrar varios equipos a la vez.
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- Ahora no se pueden añadir equipos si no hay OUs creadas en el sistema.
|
|
||||||
- Ahora aparecen los logs del cliente en tiempo real en estados mas alla de "og-live".
|
|
||||||
- Se ha añadido el campo "mac" en el ogbrowser
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Se ha corregido un error de sintaxis a la hora de cargar los iframe del ogLog.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [1.1.0] - 2025-10-17
|
|
||||||
### Added
|
|
||||||
- Se han incorporado opciones para poder seleccionar tamaños de pantalla para visualizar en los menu ogBrowser
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- Se han incluido cambios en el particionador que mejoran la experiencia de usuario.
|
|
||||||
- Se ha incluido un parametro en el "config.json" para poder integrar los logs desde el instalador.
|
|
||||||
|
|
||||||
### Fixed.
|
|
||||||
- Se han corrgido los test que no se realizaban correctamente.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [1.0.0] - 2025-10-09
|
|
||||||
### Added
|
|
||||||
- Se ha añadido nuevo readme
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.26.0] - 2025-10-09
|
|
||||||
### Improved
|
|
||||||
- Se han eliminado campos redudantes en la creacion/edicion de cliente
|
|
||||||
- En el particionador, ahora el despleagble del disco se autocompleta en caso de ser 1.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.25.0] - 2025-10-08
|
|
||||||
### Added
|
|
||||||
- Se ha añadido una logica en el particionador, el cual filtra las opciones del sistema de ficheros, segun el tipo de particion seleccionado.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Se ha corregido el componente "estado global" ya que no funcionaba correctamente para el repositorio-
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.24.1] - 2025-10-06
|
|
||||||
### Fixed
|
|
||||||
- Se ha corregido un error en los casos de 2 discos o mas en el asistente de particionado.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.24.0] - 2025-09-23
|
|
||||||
### Added
|
|
||||||
- Se han añadido nuevos componentes de visualizacion de todos los apartados de hardware y hardware profile.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.23.4] - 2025-09-22
|
|
||||||
### Fixed
|
|
||||||
- Se ha arreglado en error a la hora de mandar el ambito de ejecucion en lso asistentes
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.23.3] - 2025-09-10
|
|
||||||
### Improved
|
|
||||||
- Se han realizado mejoras sobre la funcionalidad de tareas programadas.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.23.2] - 2025-09-09
|
|
||||||
### Fixed
|
|
||||||
- Se ha corregido un bug en el asistente de particionado relacionado con las tareas programadas.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.23.1] - 2025-09-09
|
|
||||||
### Fixed
|
|
||||||
- Se ha corregido el formato de la fecha a la hora de cambiar las tareas programadas
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.23.0] - 2025-09-08
|
|
||||||
### Added
|
|
||||||
- Se ha añadido la funcionalidad para crear backups de repositorios GIT.
|
|
||||||
|
|
||||||
## [0.22.2] - 2025-09-05
|
|
||||||
### Improved
|
|
||||||
- Se ha mejorado la UX en el asistente de ejecurcion de scripts.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Se ha corregido en el particionador, el que el tamaño de las particiones EFI no esten fijas a 512 cuando ya haya datos almacenados.
|
|
||||||
- Se ha corregido un bug que hacia que no pasara los clientes seleccionados en el asistente de script, y en las tareas programadas.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.22.1] - 2025-09-05
|
|
||||||
### Improved
|
|
||||||
- Se ha mejorado la experiencia de usuario con el despleable de "tipos de particion" en el asistente de particonado.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.22.0] - 2025-09-03
|
|
||||||
### Added
|
|
||||||
- Nueva ux a la hora de gestionar tareas programadas.
|
|
||||||
- En modificar imagen git, nueva opcion para "forzar push".
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- Nuevo manejo de errores en el modulo de ogRepository.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.21.0] - 2025-08-28
|
|
||||||
### Added
|
|
||||||
- Se ha incluido una integracion con el agente, para generar los scripts que se llaman al particionar.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.20.1] - 2025-08-28
|
|
||||||
### Improved
|
|
||||||
- Se ha mejorado la UX de los asistentes.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Se han corredigo errores en la seccion clientes de los asistentes.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.20.0] - 2025-08-25
|
|
||||||
### Added
|
|
||||||
- Se ha añadido un nuevo boton en "Trazas" para marcar la misma como completada cuando se requiera.
|
|
||||||
- Nuevo estado "ocupado" en las trazas para indicar que el cliente envia un "409" y que ya esta ejecutando una accion
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- Mejorada la interfaz para gestionar las tareas.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.19.0] - 2025-08-06
|
|
||||||
### Added
|
|
||||||
- Se ha añadido un nuevo estado "enviado" para cuando se ejecuten acciones a equipos en estado Windows o Linux
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.18.0] - 2025-08-04
|
|
||||||
### Added
|
|
||||||
- Se ha añadido la posibilidad de visualizar logs en tiempo real de Grafana. Tanto en los componentes como en los clientes.
|
|
||||||
- Se ha añadido la funcionaldad e integracion con OgGit.
|
|
||||||
- En el particionador, se ha añadido una integracion para comprobar los tamaños de las particiones.
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- Sistema de cola de acciones.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.17.0] - 2025-07-15
|
|
||||||
### Added
|
|
||||||
- Se ha añadido la funcionalidad para tagear commits en el apartado de imágenes git.
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- Se ha corregido el particionador, para cuando un equipo es EFI, ahora aparece la primera particion completada.
|
|
||||||
|
|
||||||
|
|
||||||
## [0.16.0] - 2025-06-27
|
|
||||||
### Added
|
|
||||||
- Sistema de logs en tiempo real.
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- Se ha mejorado el comportamiento de algunos filtros en la parte de trazas.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.15.0] - 2025-06-26
|
|
||||||
### Added
|
|
||||||
- Se ha añadido integracion con OgGit. Ahora se pueden crear y desplegar imagenes.
|
|
||||||
- Ahora se pueden gestionar cola de acciones y vaciarlas tanto a nivel de aula como de cliente.
|
|
||||||
- Nuevos componentes que ayudan a la mejora general de la UX
|
|
||||||
- Mejora en el comportamiento de los asistentes.
|
|
||||||
- Se puede cancelar tareas desde la parte de Trazas
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Se ha cambiado la vista de las imagenes de Git
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.14.1] - 2025-06-09
|
|
||||||
### Fixed
|
|
||||||
- Se han corregido los errores en produccion que hacia que no salieran mensajes desde la API correctamente.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.14.0] - 2025-06-02
|
|
||||||
### Added
|
|
||||||
- Se ha añadido funcionalidad de usuarios/roles para separar las vistas segun los permisos.
|
|
||||||
- Nuevo boton de "mover" en la pantalla de grupos, para mover equipos entre aulas y grupos.
|
|
||||||
|
|
||||||
### Improved
|
|
||||||
- Se ha completado la opcion de inicion de sesion y eliminar imagen cache.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.13.1] - 2025-05-23
|
|
||||||
### Changed
|
|
||||||
- Desactivado temporalmente la funcionalidad de ogGit.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [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
|
|
||||||
- Se ha modificado el acceso a Mercure añadiendo nueva variable de entorno.
|
|
||||||
|
|
||||||
---
|
|
||||||
## [0.9.0] - 2025-3-4
|
|
||||||
### Added
|
|
||||||
- Integracion con Mercure. Subscriber tanto en "Trazas" con en "Clientes".
|
|
||||||
- Nueva funcionalidad para checkear la integridad de una imagen. Boton en apartado "imagenes" dentro del repositorio.
|
|
||||||
- Centralizacion de estilos.
|
|
||||||
- Nueva funcionalidad para realizar backup de imágenes.
|
|
||||||
- Botón para cancelar despliegues de imagenes. Aparece en "trazas" tan solo para los comendos "deploy" y para el estado "en progreso".
|
|
||||||
|
|
||||||
### 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
|
## [0.7.0] - 2024-12-10
|
||||||
|
|
||||||
### Refactored
|
### Refactored
|
||||||
- Refactored the group screen, removing the separate tabs for clients, advanced search, and organizational units.
|
- Refactored the group screen, removing the separate tabs for clients, advanced search, and organizational units.
|
||||||
- Added support for partitioning functionality in the client detail view.
|
- Added support for partitioning functionality in the client detail view.
|
||||||
|
|
||||||
---
|
|
||||||
## [0.6.1] - 2024-11-19
|
## [0.6.1] - 2024-11-19
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
- Introduced a new automatic sync mode for the ogdhcp and ogBoot components.
|
- Introduced a new automatic sync mode for the ogdhcp and ogBoot components.
|
||||||
- Improve test coverage.
|
- Improve test coverage.
|
||||||
- New view for clients inside the classroom on the main page.
|
- New view for clients inside the classroom on the main page.
|
||||||
|
|
||||||
---
|
|
||||||
## [0.6.0] - 2024-11-19
|
## [0.6.0] - 2024-11-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Added functionality to execute actions from the menu in the general groups screen.
|
- Added functionality to execute actions from the menu in the general groups screen.
|
||||||
- Displayed the selected center on the general screen for better context.
|
- Displayed the selected center on the general screen for better context.
|
||||||
|
|
@ -802,4 +40,3 @@
|
||||||
### Fixed
|
### Fixed
|
||||||
- Resolved an issue that prevented editing software profiles correctly.
|
- Resolved an issue that prevented editing software profiles correctly.
|
||||||
- Fixed a bug where newly created commands failed to execute in the commands section.
|
- Fixed a bug where newly created commands failed to execute in the commands section.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,289 +0,0 @@
|
||||||
# Documentación Funcional - OgWebconsole
|
|
||||||
|
|
||||||
## Descripción General
|
|
||||||
|
|
||||||
**OgWebconsole** es una aplicación web desarrollada en Angular 18 que proporciona una interfaz de gestión integral para sistemas de computación distribuida, especialmente diseñada para entornos educativos y laboratorios. La aplicación permite administrar clientes, unidades organizativas, comandos, imágenes, calendarios y configuraciones de red de manera centralizada.
|
|
||||||
|
|
||||||
**URL de Acceso:** https://localhost:4200/
|
|
||||||
|
|
||||||
## Arquitectura del Sistema
|
|
||||||
|
|
||||||
### Tecnologías Utilizadas
|
|
||||||
- **Frontend:** Angular 18 con Angular Material
|
|
||||||
- **Autenticación:** JWT (JSON Web Tokens)
|
|
||||||
- **Internacionalización:** ngx-translate (Español/Inglés)
|
|
||||||
- **Gráficos:** ngx-charts
|
|
||||||
- **Notificaciones:** ngx-toastr
|
|
||||||
- **Tours Guiados:** ngx-joyride
|
|
||||||
|
|
||||||
### Sistema de Roles
|
|
||||||
La aplicación implementa un sistema jerárquico de roles con diferentes niveles de acceso:
|
|
||||||
|
|
||||||
1. **Super Admin** (`super-admin`): Acceso completo a todas las funcionalidades
|
|
||||||
2. **OU Admin** (`ou-admin`): Administrador de unidad organizativa
|
|
||||||
3. **OU Operator** (`ou-operator`): Operador de unidad organizativa
|
|
||||||
4. **OU Minimal** (`ou-minimal`): Acceso mínimo de solo lectura
|
|
||||||
5. **User** (`user`): Usuario básico
|
|
||||||
|
|
||||||
## Módulos Principales
|
|
||||||
|
|
||||||
### 1. Gestión de Grupos y Unidades Organizativas
|
|
||||||
|
|
||||||
**Ruta:** `/groups`
|
|
||||||
|
|
||||||
**Descripción:** Módulo central para la gestión de la estructura organizativa jerárquica.
|
|
||||||
|
|
||||||
**Funcionalidades:**
|
|
||||||
- Visualización en árbol de unidades organizativas (Centros, Aulas, Grupos)
|
|
||||||
- Gestión de clientes individuales y masiva
|
|
||||||
- Filtros avanzados por nombre, IP, MAC y estado
|
|
||||||
- Vista de lista y tarjeta para clientes
|
|
||||||
- Estadísticas en tiempo real de clientes (Online, Offline, Ocupados)
|
|
||||||
|
|
||||||
**Casos de Uso:**
|
|
||||||
- **Crear Estructura Organizativa:** Un administrador puede crear centros, aulas y grupos de clientes
|
|
||||||
- **Gestionar Clientes:** Añadir, editar, eliminar y mover clientes entre unidades organizativas
|
|
||||||
- **Monitoreo de Estado:** Visualizar el estado en tiempo real de todos los clientes conectados
|
|
||||||
- **Búsqueda Avanzada:** Localizar clientes específicos por múltiples criterios
|
|
||||||
|
|
||||||
### 2. Administración de Usuarios y Roles
|
|
||||||
|
|
||||||
**Ruta:** `/users`, `/roles`, `/env-vars`
|
|
||||||
|
|
||||||
**Descripción:** Gestión completa del sistema de usuarios y permisos.
|
|
||||||
|
|
||||||
**Funcionalidades:**
|
|
||||||
- CRUD de usuarios con asignación de roles
|
|
||||||
- Gestión de roles con permisos granulares
|
|
||||||
- Variables de entorno del sistema
|
|
||||||
- Cambio de contraseñas
|
|
||||||
|
|
||||||
**Casos de Uso:**
|
|
||||||
- **Gestión de Usuarios:** Crear cuentas para administradores y operadores
|
|
||||||
- **Control de Acceso:** Asignar roles específicos según responsabilidades
|
|
||||||
- **Configuración del Sistema:** Modificar variables de entorno críticas
|
|
||||||
|
|
||||||
### 3. Gestión de Comandos
|
|
||||||
|
|
||||||
**Rutas:** `/commands`, `/commands-groups`, `/commands-task`, `/commands-logs`
|
|
||||||
|
|
||||||
**Descripción:** Sistema completo de ejecución y programación de comandos.
|
|
||||||
|
|
||||||
**Funcionalidades:**
|
|
||||||
- Creación y edición de comandos personalizados
|
|
||||||
- Agrupación de comandos para ejecución masiva
|
|
||||||
- Programación de tareas con calendario
|
|
||||||
- Trazabilidad completa de ejecuciones
|
|
||||||
- Logs detallados de todas las operaciones
|
|
||||||
|
|
||||||
**Comandos Predefinidos:**
|
|
||||||
- Encender/Apagar equipos
|
|
||||||
- Reiniciar sistemas
|
|
||||||
- Crear/Desplegar imágenes
|
|
||||||
- Particionar y formatear discos
|
|
||||||
- Inventarios de software y hardware
|
|
||||||
- Ejecución de scripts personalizados
|
|
||||||
|
|
||||||
**Casos de Uso:**
|
|
||||||
- **Mantenimiento Programado:** Ejecutar tareas de mantenimiento en horarios específicos
|
|
||||||
- **Despliegue Masivo:** Aplicar configuraciones a múltiples clientes simultáneamente
|
|
||||||
- **Monitoreo de Ejecución:** Seguir el progreso de comandos en tiempo real
|
|
||||||
- **Auditoría:** Revisar historial completo de operaciones realizadas
|
|
||||||
|
|
||||||
### 4. Gestión de Imágenes y Repositorios
|
|
||||||
|
|
||||||
**Rutas:** `/images`, `/repositories`, `/pxe-images`
|
|
||||||
|
|
||||||
**Descripción:** Administración de imágenes del sistema y repositorios.
|
|
||||||
|
|
||||||
**Funcionalidades:**
|
|
||||||
- Gestión de imágenes OgLive
|
|
||||||
- Repositorios Git y monolíticos
|
|
||||||
- Plantillas PXE para arranque de red
|
|
||||||
- Importación y exportación de imágenes
|
|
||||||
- Conversión entre formatos de imagen
|
|
||||||
|
|
||||||
**Casos de Uso:**
|
|
||||||
- **Despliegue de Sistemas:** Crear y distribuir imágenes de sistemas operativos
|
|
||||||
- **Gestión de Versiones:** Control de versiones de imágenes mediante Git
|
|
||||||
- **Configuración de Arranque:** Configurar plantillas PXE para diferentes tipos de arranque
|
|
||||||
- **Backup y Restauración:** Crear copias de seguridad de configuraciones
|
|
||||||
|
|
||||||
### 5. Configuración de Red y DHCP
|
|
||||||
|
|
||||||
**Rutas:** `/subnets`, `/ogdhcp-status`
|
|
||||||
|
|
||||||
**Descripción:** Gestión de configuración de red y servicios DHCP.
|
|
||||||
|
|
||||||
**Funcionalidades:**
|
|
||||||
- Configuración de subredes
|
|
||||||
- Gestión de clientes DHCP
|
|
||||||
- Monitoreo del estado del servicio DHCP
|
|
||||||
- Configuración de parámetros de red
|
|
||||||
|
|
||||||
**Casos de Uso:**
|
|
||||||
- **Configuración de Red:** Definir subredes y rangos de IP
|
|
||||||
- **Gestión de Clientes:** Asignar direcciones IP automáticamente
|
|
||||||
- **Monitoreo de Red:** Supervisar el estado de los servicios de red
|
|
||||||
|
|
||||||
### 6. Gestión de Calendarios
|
|
||||||
|
|
||||||
**Ruta:** `/calendars`
|
|
||||||
|
|
||||||
**Descripción:** Sistema de calendarios para control de disponibilidad de equipos.
|
|
||||||
|
|
||||||
**Funcionalidades:**
|
|
||||||
- Creación de calendarios personalizados
|
|
||||||
- Configuración de horarios de disponibilidad
|
|
||||||
- Excepciones y reglas especiales
|
|
||||||
- Integración con Remote PC
|
|
||||||
|
|
||||||
**Casos de Uso:**
|
|
||||||
- **Control de Acceso:** Definir horarios de uso de laboratorios
|
|
||||||
- **Mantenimiento Programado:** Bloquear equipos durante mantenimiento
|
|
||||||
- **Gestión de Recursos:** Optimizar el uso de recursos informáticos
|
|
||||||
|
|
||||||
### 7. Gestión de Software y Hardware
|
|
||||||
|
|
||||||
**Rutas:** `/software`, `/software-profiles`, `/operative-systems`, `/hardware`, `/hardware-profiles`, `/hardware-types`
|
|
||||||
|
|
||||||
**Descripción:** Administración de configuraciones de software y hardware.
|
|
||||||
|
|
||||||
**Funcionalidades:**
|
|
||||||
- Catálogo de software disponible
|
|
||||||
- Perfiles de software para diferentes entornos
|
|
||||||
- Configuraciones de sistemas operativos
|
|
||||||
- Gestión de hardware y perfiles de hardware
|
|
||||||
- Tipos de hardware y configuraciones específicas
|
|
||||||
|
|
||||||
**Casos de Uso:**
|
|
||||||
- **Configuración de Entornos:** Crear perfiles específicos para diferentes tipos de laboratorios
|
|
||||||
- **Gestión de Software:** Mantener catálogo actualizado de software disponible
|
|
||||||
- **Configuración de Hardware:** Definir configuraciones óptimas para diferentes tipos de equipos
|
|
||||||
|
|
||||||
### 8. Estado Global del Sistema
|
|
||||||
|
|
||||||
**Ruta:** `/global-status`
|
|
||||||
|
|
||||||
**Descripción:** Dashboard centralizado con métricas del sistema.
|
|
||||||
|
|
||||||
**Funcionalidades:**
|
|
||||||
- Uso de CPU y RAM en tiempo real
|
|
||||||
- Estado de servicios críticos
|
|
||||||
- Estadísticas de equipos conectados
|
|
||||||
- Monitoreo de procesos del sistema
|
|
||||||
|
|
||||||
**Casos de Uso:**
|
|
||||||
- **Monitoreo del Sistema:** Supervisar el rendimiento general del sistema
|
|
||||||
- **Detección de Problemas:** Identificar rápidamente fallos o sobrecargas
|
|
||||||
- **Planificación de Recursos:** Analizar tendencias de uso para planificación
|
|
||||||
|
|
||||||
## Flujos de Trabajo Principales
|
|
||||||
|
|
||||||
### 1. Configuración Inicial del Sistema
|
|
||||||
|
|
||||||
1. **Acceso como Super Admin**
|
|
||||||
2. **Configuración de Variables de Entorno** (`/env-vars`)
|
|
||||||
3. **Creación de Roles y Usuarios** (`/roles`, `/users`)
|
|
||||||
4. **Configuración de Red** (`/subnets`)
|
|
||||||
5. **Creación de Estructura Organizativa** (`/groups`)
|
|
||||||
|
|
||||||
### 2. Gestión Diaria de Clientes
|
|
||||||
|
|
||||||
1. **Acceso a Gestión de Grupos** (`/groups`)
|
|
||||||
2. **Selección de Unidad Organizativa**
|
|
||||||
3. **Visualización de Clientes**
|
|
||||||
4. **Ejecución de Comandos** (individual o masivo)
|
|
||||||
5. **Monitoreo de Estado** en tiempo real
|
|
||||||
|
|
||||||
### 3. Despliegue de Imágenes
|
|
||||||
|
|
||||||
1. **Preparación de Imagen** (`/images` o `/repositories`)
|
|
||||||
2. **Configuración de Plantilla PXE** (`/pxe-templates`)
|
|
||||||
3. **Selección de Clientes Objetivo** (`/groups`)
|
|
||||||
4. **Ejecución de Despliegue** (comando `deployImage`)
|
|
||||||
5. **Verificación de Resultados** (`/commands-logs`)
|
|
||||||
|
|
||||||
### 4. Mantenimiento Programado
|
|
||||||
|
|
||||||
1. **Creación de Tarea** (`/commands-task`)
|
|
||||||
2. **Selección de Comandos** (`/commands` o `/commands-groups`)
|
|
||||||
3. **Programación de Ejecución** (fecha y hora)
|
|
||||||
4. **Selección de Clientes** (`/groups`)
|
|
||||||
5. **Monitoreo de Ejecución** (`/commands-logs`)
|
|
||||||
|
|
||||||
## Características Técnicas Avanzadas
|
|
||||||
|
|
||||||
### 1. Sistema de Autenticación y Autorización
|
|
||||||
- Autenticación basada en JWT
|
|
||||||
- Guards de ruta para control de acceso
|
|
||||||
- Roles jerárquicos con permisos granulares
|
|
||||||
- Sesiones persistentes con refresh automático
|
|
||||||
|
|
||||||
### 2. Interfaz de Usuario
|
|
||||||
- Diseño responsive con Angular Material
|
|
||||||
- Tours guiados integrados (ngx-joyride)
|
|
||||||
- Internacionalización completa (ES/EN)
|
|
||||||
- Notificaciones en tiempo real
|
|
||||||
- Modales y diálogos contextuales
|
|
||||||
|
|
||||||
### 3. Gestión de Estado
|
|
||||||
- Servicios centralizados para cada módulo
|
|
||||||
- Interceptores HTTP para manejo de errores
|
|
||||||
- Caché inteligente de datos
|
|
||||||
- Actualizaciones en tiempo real
|
|
||||||
|
|
||||||
### 4. Monitoreo y Logging
|
|
||||||
- Trazabilidad completa de operaciones
|
|
||||||
- Logs detallados de ejecución
|
|
||||||
- Métricas de rendimiento
|
|
||||||
- Exportación de datos (CSV)
|
|
||||||
|
|
||||||
## Casos de Uso Específicos por Rol
|
|
||||||
|
|
||||||
### Super Admin
|
|
||||||
- Configuración completa del sistema
|
|
||||||
- Gestión de todos los usuarios y roles
|
|
||||||
- Acceso a todas las funcionalidades
|
|
||||||
- Configuración de variables de entorno
|
|
||||||
- Monitoreo global del sistema
|
|
||||||
|
|
||||||
### OU Admin
|
|
||||||
- Gestión de unidades organizativas asignadas
|
|
||||||
- Creación y gestión de clientes
|
|
||||||
- Ejecución de comandos y tareas
|
|
||||||
- Gestión de imágenes y repositorios
|
|
||||||
- Configuración de calendarios
|
|
||||||
|
|
||||||
### OU Operator
|
|
||||||
- Ejecución de comandos predefinidos
|
|
||||||
- Monitoreo de estado de clientes
|
|
||||||
- Visualización de logs y trazas
|
|
||||||
- Gestión básica de clientes
|
|
||||||
|
|
||||||
### OU Minimal
|
|
||||||
- Solo visualización de información
|
|
||||||
- Sin permisos de modificación
|
|
||||||
- Acceso limitado a funcionalidades de consulta
|
|
||||||
|
|
||||||
## Consideraciones de Seguridad
|
|
||||||
|
|
||||||
1. **Autenticación Robusta:** Sistema JWT con refresh tokens
|
|
||||||
2. **Control de Acceso:** Guards de ruta y validación de roles
|
|
||||||
3. **Validación de Datos:** Validación tanto en frontend como backend
|
|
||||||
4. **Auditoría:** Logs completos de todas las operaciones
|
|
||||||
5. **HTTPS:** Comunicación segura en toda la aplicación
|
|
||||||
|
|
||||||
## Escalabilidad y Rendimiento
|
|
||||||
|
|
||||||
1. **Paginación:** Implementada en todas las listas grandes
|
|
||||||
2. **Lazy Loading:** Carga diferida de componentes
|
|
||||||
3. **Caché Inteligente:** Reducción de llamadas al servidor
|
|
||||||
4. **Optimización de Consultas:** Filtros y búsquedas eficientes
|
|
||||||
5. **Responsive Design:** Adaptable a diferentes dispositivos
|
|
||||||
|
|
||||||
## Conclusión
|
|
||||||
|
|
||||||
OgWebconsole es una solución integral para la gestión de laboratorios de computación que combina facilidad de uso con potentes funcionalidades de administración. Su arquitectura modular y sistema de roles permite adaptarse a diferentes entornos y necesidades organizativas, proporcionando una herramienta centralizada para la gestión eficiente de recursos informáticos distribuidos.
|
|
||||||
|
|
||||||
La aplicación está diseñada para ser intuitiva para usuarios finales mientras proporciona las herramientas avanzadas necesarias para administradores de sistemas, haciendo que sea una solución ideal para entornos educativos, laboratorios de investigación y cualquier organización que requiera gestión centralizada de múltiples equipos informáticos.
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
@Library('jenkins-shared-library') _
|
|
||||||
pipeline {
|
|
||||||
agent {
|
|
||||||
label 'jenkins-slave'
|
|
||||||
}
|
|
||||||
environment {
|
|
||||||
DEBIAN_FRONTEND = 'noninteractive'
|
|
||||||
DEFAULT_DEV_NAME = 'Opengnsys Team'
|
|
||||||
DEFAULT_DEV_EMAIL = 'opengnsys@qindel.com'
|
|
||||||
}
|
|
||||||
options {
|
|
||||||
skipDefaultCheckout()
|
|
||||||
}
|
|
||||||
parameters {
|
|
||||||
string(name: 'DEV_NAME', defaultValue: '', description: 'Nombre del desarrollador')
|
|
||||||
string(name: 'DEV_EMAIL', defaultValue: '', description: 'Email del desarrollador')
|
|
||||||
}
|
|
||||||
stages {
|
|
||||||
stage('Prepare Workspace') {
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
env.BUILD_DIR = "${WORKSPACE}/oggui"
|
|
||||||
sh "mkdir -p ${env.BUILD_DIR}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Checkout') {
|
|
||||||
steps {
|
|
||||||
dir("${env.BUILD_DIR}") {
|
|
||||||
checkout scm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Generate Changelog') {
|
|
||||||
when {
|
|
||||||
expression {
|
|
||||||
return env.TAG_NAME != null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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('Generate Changelog (Devel)') {
|
|
||||||
when {
|
|
||||||
branch 'devel'
|
|
||||||
}
|
|
||||||
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, "devel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage ('Publish to Debian Repository (Devel)') {
|
|
||||||
when {
|
|
||||||
branch 'devel'
|
|
||||||
}
|
|
||||||
agent { label 'debian-repo' }
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
def versionPattern = "-${env.BUILD_NUMBER}~devel"
|
|
||||||
publicarEnAptly('/var/tmp/opengnsys/debian-repo/oggui', 'devel', versionPattern)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
success {
|
|
||||||
script {
|
|
||||||
if (env.BRANCH_NAME == 'main') {
|
|
||||||
build job: 'Aptly publish nightly repository',
|
|
||||||
wait: false,
|
|
||||||
parameters: [
|
|
||||||
string(name: 'TRIGGERED_BY', value: "${env.JOB_NAME}-${env.BUILD_NUMBER}")
|
|
||||||
]
|
|
||||||
}
|
|
||||||
if (env.BRANCH_NAME == 'devel') {
|
|
||||||
build job: 'Aptly publish develop repository',
|
|
||||||
wait: false,
|
|
||||||
parameters: [
|
|
||||||
string(name: 'TRIGGERED_BY', value: "${env.JOB_NAME}-${env.BUILD_NUMBER}")
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
always {
|
|
||||||
notifyBuildStatus('opengnsys@qindel.com')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
754
README.md
754
README.md
|
|
@ -1,738 +1,30 @@
|
||||||
# ogGui - OpenGnsys Web Console
|
# ogGui
|
||||||
|
|
||||||
ogGui es una aplicación web frontend desarrollada en **Angular 18** que proporciona una interfaz de usuario completa e intuitiva para la gestión y administración de la herramienta **OpenGnsys**. OpenGnsys es un sistema de clonación y administración masiva de equipos informáticos orientado a entornos educativos y corporativos.
|
ogGui es un proyecto frontend desarrollado en Angular para proporcionar una interfaz de usuario a la herramienta OpenGnsys.
|
||||||
|
|
||||||
## 📋 Tabla de Contenidos
|
## Ambiente de Desarrollo
|
||||||
|
|
||||||
- [Descripción General](#-descripción-general)
|
Este proyecto ha sido desarrollado y probado en el siguiente entorno de desarrollo:
|
||||||
- [Arquitectura Técnica](#-arquitectura-técnica)
|
|
||||||
- [Módulos Principales](#-módulos-principales)
|
|
||||||
- [Análisis de Componentes](#-análisis-de-componentes)
|
|
||||||
- [Servicios y Guards](#-servicios-y-guards)
|
|
||||||
- [Características Destacadas](#-características-destacadas)
|
|
||||||
- [Ambiente de Desarrollo](#-ambiente-de-desarrollo)
|
|
||||||
- [Instalación](#-instalación)
|
|
||||||
- [Configuración](#-configuración)
|
|
||||||
- [Integración con Servicios Externos](#-integración-con-servicios-externos)
|
|
||||||
- [Estructura del Proyecto](#-estructura-del-proyecto)
|
|
||||||
|
|
||||||
## 🎯 Descripción General
|
- Sistema operativo: Ubuntu 22.04.3 LTS (Release: 22.04, Codename: jammy)
|
||||||
|
- Editor de código: Visual Studio Code 1.87.2
|
||||||
|
- Node.js: v22.1.0
|
||||||
|
- npm: 10.7.0
|
||||||
|
- Angular CLI: 12.0.3
|
||||||
|
- Angular Core: 12.0.1
|
||||||
|
- Angular Material: 12.0.2
|
||||||
|
|
||||||
ogGui es la consola web de administración para OpenGnsys que permite a los administradores de sistemas gestionar de manera centralizada:
|
## Instalación
|
||||||
|
|
||||||
- **Clientes y equipos** organizados en aulas y unidades organizativas
|
|
||||||
- **Imágenes del sistema** (creación, despliegue, versionado con Git)
|
|
||||||
- **Tareas y comandos** (programados o inmediatos)
|
|
||||||
- **Configuraciones de red** (DHCP, PXE, arranque)
|
|
||||||
- **Usuarios y permisos** basados en roles
|
|
||||||
- **Hardware y software** (perfiles, inventarios)
|
|
||||||
- **Monitorización en tiempo real** del estado de los equipos
|
|
||||||
|
|
||||||
### Versión Actual
|
|
||||||
**v0.26.0** - Octubre 2025
|
|
||||||
|
|
||||||
## 🏗 Arquitectura Técnica
|
|
||||||
|
|
||||||
### Stack Tecnológico
|
|
||||||
|
|
||||||
| Tecnología | Versión | Propósito |
|
|
||||||
|------------|---------|-----------|
|
|
||||||
| **Angular** | 18.0.0 | Framework principal |
|
|
||||||
| **Angular Material** | 18.0.0 | Componentes UI |
|
|
||||||
| **TypeScript** | 5.4.5 | Lenguaje de programación |
|
|
||||||
| **RxJS** | 7.8.0 | Programación reactiva |
|
|
||||||
| **NGX-Translate** | 16.0.3 | Internacionalización |
|
|
||||||
| **NGX-Charts** | 20.5.0 | Gráficos y visualizaciones |
|
|
||||||
| **NGX-Toastr** | 19.0.0 | Notificaciones |
|
|
||||||
| **Joyride** | 2.5.0 | Tours guiados |
|
|
||||||
| **JWT-Decode** | 4.0.0 | Manejo de autenticación |
|
|
||||||
| **PapaParse** | 5.4.1 | Procesamiento CSV |
|
|
||||||
|
|
||||||
### Arquitectura de Aplicación
|
|
||||||
|
|
||||||
La aplicación sigue el patrón **Model-View-Component** de Angular con:
|
|
||||||
|
|
||||||
1. **Lazy Loading**: Módulos cargados bajo demanda
|
|
||||||
2. **Guards**: Protección de rutas basada en roles
|
|
||||||
3. **Interceptors**: Manejo centralizado de peticiones HTTP
|
|
||||||
4. **Services**: Lógica de negocio compartida
|
|
||||||
5. **Reactive Forms**: Formularios reactivos con validación
|
|
||||||
6. **Material Design**: Diseño consistente y moderno
|
|
||||||
|
|
||||||
### Estructura de Layouts
|
|
||||||
|
|
||||||
```
|
|
||||||
├── AuthLayoutComponent → Layout para autenticación (Login)
|
|
||||||
└── MainLayoutComponent → Layout principal de la aplicación
|
|
||||||
├── HeaderComponent → Barra superior con navegación
|
|
||||||
└── SidebarComponent → Menú lateral colapsable
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🧩 Módulos Principales
|
|
||||||
|
|
||||||
### 1. **Administración** (`admin/`)
|
|
||||||
|
|
||||||
Gestión de usuarios, roles y configuración del sistema.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **UsersComponent**: CRUD de usuarios del sistema
|
|
||||||
- **RolesComponent**: Gestión de roles y permisos
|
|
||||||
- **EnvVarsComponent**: Configuración de variables de entorno
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Sistema de roles jerárquicos (Super Admin, OU Admin, OU Operator, OU Minimal)
|
|
||||||
- Cambio de contraseña
|
|
||||||
- Asignación de unidades organizativas
|
|
||||||
- Gestión de permisos granulares
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. **Grupos y Clientes** (`groups/`)
|
|
||||||
|
|
||||||
Módulo central para la gestión de equipos organizados en aulas y unidades organizativas.
|
|
||||||
|
|
||||||
#### Componentes principales:
|
|
||||||
- **GroupsComponent**: Vista principal de grupos
|
|
||||||
- **ClassroomViewComponent**: Vista de aula con distribución visual
|
|
||||||
- **ClientViewComponent**: Vista detallada de un cliente
|
|
||||||
- **ManageClientComponent**: Creación/edición de clientes
|
|
||||||
- **ShowOrganizationalUnitComponent**: Gestión de OUs
|
|
||||||
- **ClientDetailsComponent**: Detalles técnicos del cliente
|
|
||||||
|
|
||||||
#### Subcomponentes especializados:
|
|
||||||
- **PartitionAssistantComponent**: Asistente de particionado de discos
|
|
||||||
- Soporte para múltiples discos
|
|
||||||
- Tipos de partición (Primaria, Extendida, Lógica, EFI)
|
|
||||||
- Sistemas de archivos (ext4, NTFS, FAT32, etc.)
|
|
||||||
- Validación de tamaños y estructura
|
|
||||||
|
|
||||||
- **CreateClientImageComponent**: Creación de imágenes desde cliente
|
|
||||||
- **DeployImageComponent**: Despliegue de imágenes
|
|
||||||
- **RunScriptAssistantComponent**: Ejecución de scripts con variables
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Vista de árbol jerárquico de OUs
|
|
||||||
- Vista de tarjetas con estado en tiempo real
|
|
||||||
- Filtros avanzados por centro, estado, firmware
|
|
||||||
- Drag & Drop para organización
|
|
||||||
- Estados de cliente: desconectado, ocupado, inicializando, Linux, Windows, etc.
|
|
||||||
- Ejecución masiva de comandos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. **Comandos** (`commands/`)
|
|
||||||
|
|
||||||
Sistema completo de gestión de comandos y tareas.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **CommandsComponent**: Comandos individuales
|
|
||||||
- **CommandsGroupsComponent**: Grupos de comandos secuenciales
|
|
||||||
- **CommandsTaskComponent**: Tareas programadas y scripts
|
|
||||||
|
|
||||||
#### Subcomponentes:
|
|
||||||
- **CreateCommandComponent**: Creador de comandos personalizados
|
|
||||||
- **ExecuteCommandComponent**: Formulario de ejecución
|
|
||||||
- **CreateTaskComponent**: Planificador de tareas
|
|
||||||
- **CreateTaskScheduleComponent**: Tareas programadas con calendario
|
|
||||||
- **CreateTaskScriptComponent**: Tareas basadas en scripts
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Comandos predefinidos (no editables)
|
|
||||||
- Comandos personalizados
|
|
||||||
- Agrupación de comandos secuenciales
|
|
||||||
- Programación con calendarios
|
|
||||||
- Ejecución inmediata o diferida
|
|
||||||
- Variables parametrizables en scripts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. **Repositorios e Imágenes** (`repositories/`, `images/`)
|
|
||||||
|
|
||||||
Gestión completa del ciclo de vida de imágenes del sistema.
|
|
||||||
|
|
||||||
#### Componentes principales:
|
|
||||||
- **RepositoriesComponent**: Lista de repositorios
|
|
||||||
- **MainRepositoryViewComponent**: Vista detallada de repositorio
|
|
||||||
- **ImagesComponent**: Gestión de imágenes
|
|
||||||
|
|
||||||
#### Operaciones con imágenes:
|
|
||||||
- **ManageRepositoryComponent**: Crear/editar repositorios
|
|
||||||
- **ImportImageComponent**: Importar imágenes externas
|
|
||||||
- **ExportImageComponent**: Exportar imágenes
|
|
||||||
- **BackupImageComponent**: Backup de imágenes
|
|
||||||
- **ConvertImageComponent**: Conversión de formatos
|
|
||||||
- **ConvertImageToVirtualComponent**: Conversión a imagen virtual
|
|
||||||
- **EditImageComponent**: Edición de metadatos
|
|
||||||
- **RenameImageComponent**: Renombrado de imágenes
|
|
||||||
|
|
||||||
#### Integración con Git:
|
|
||||||
- **ShowGitCommitsComponent**: Historial de commits
|
|
||||||
- **CreateTagModalComponent**: Etiquetado de versiones
|
|
||||||
- **CreateBranchModalComponent**: Gestión de ramas
|
|
||||||
- **BackupRepositoryModalComponent**: Backup de repositorio Git
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Versionado de imágenes con Git (OgGit)
|
|
||||||
- Imágenes monolíticas y Git
|
|
||||||
- Verificación de integridad
|
|
||||||
- Sincronización con repositorios remotos
|
|
||||||
- Gestión de usuarios SSH y puertos
|
|
||||||
- Cache de imágenes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. **OgBoot - Arranque PXE** (`ogboot/`)
|
|
||||||
|
|
||||||
Configuración del sistema de arranque por red.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **PXEimagesComponent**: Gestión de imágenes PXE (OgLives)
|
|
||||||
- **PxeComponent**: Templates de arranque PXE
|
|
||||||
- **PxeBootFilesComponent**: Archivos de arranque
|
|
||||||
- **OgbootStatusComponent**: Estado del servicio OgBoot
|
|
||||||
|
|
||||||
#### Funcionalidades:
|
|
||||||
- **CreatePXEImageComponent**: Creación de imágenes OgLive
|
|
||||||
- **CreatePxeTemplateComponent**: Templates de configuración
|
|
||||||
- **ShowTemplateContentComponent**: Visualización de templates
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Gestión de unidades de almacenamiento
|
|
||||||
- Precarga de modelos de templates
|
|
||||||
- Sincronización automática
|
|
||||||
- Gestión de BootfileNames
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. **OgDHCP - Servidor DHCP** (`ogdhcp/`)
|
|
||||||
|
|
||||||
Gestión del servidor DHCP integrado.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **OgDhcpSubnetsComponent**: Gestión de subredes
|
|
||||||
- **StatusComponent**: Estado del servicio DHCP
|
|
||||||
- **CreateSubnetComponent**: Creación de subredes
|
|
||||||
- **AddClientsToSubnetComponent**: Asignación de clientes
|
|
||||||
- **ShowClientsComponent**: Búsqueda detallada de clientes
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Sincronización automática
|
|
||||||
- Nombres lógicos para IPs
|
|
||||||
- Asignación masiva de clientes
|
|
||||||
- Búsqueda avanzada de equipos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. **Calendarios** (`calendar/`)
|
|
||||||
|
|
||||||
Sistema de calendarios para programación de tareas.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **CalendarComponent**: Gestión de calendarios
|
|
||||||
- **CreateCalendarComponent**: Creación de calendarios
|
|
||||||
- **CreateCalendarRuleComponent**: Reglas de calendario
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Calendarios con reglas recurrentes
|
|
||||||
- Integración con tareas programadas
|
|
||||||
- Asignación a OUs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. **Trazas y Logs** (`task-logs/`)
|
|
||||||
|
|
||||||
Monitorización de la ejecución de tareas.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **TaskLogsComponent**: Historial de trazas
|
|
||||||
- **ClientTaskLogsComponent**: Logs por cliente
|
|
||||||
- **ClientPendingTasksComponent**: Cola de tareas pendientes
|
|
||||||
- **InputDialogComponent**: Diálogos de entrada
|
|
||||||
- **OutputDialogComponent**: Visualización de salidas
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Logs en tiempo real (Mercure)
|
|
||||||
- Integración con Grafana
|
|
||||||
- Filtros avanzados
|
|
||||||
- Cola de acciones
|
|
||||||
- Cancelación de tareas
|
|
||||||
- Marcado manual de completado
|
|
||||||
- Estados: enviado, en progreso, completado, error, ocupado
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 9. **Software y Perfiles** (`software/`, `software-profile/`)
|
|
||||||
|
|
||||||
Gestión de aplicaciones y perfiles de software.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **SoftwareComponent**: Catálogo de software
|
|
||||||
- **SoftwareProfileComponent**: Perfiles de instalación
|
|
||||||
- **CreateSoftwareComponent**: Alta de aplicaciones
|
|
||||||
- **CreateSoftwareProfileComponent**: Creación de perfiles
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Inventario de software
|
|
||||||
- Perfiles reutilizables
|
|
||||||
- Asignación a particiones
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 10. **Hardware** (`hardware/`, `hardware-profile/`, `hardware-type/`)
|
|
||||||
|
|
||||||
Sistema de gestión de hardware.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **HardwareComponent**: Inventario de hardware
|
|
||||||
- **HardwareProfileComponent**: Perfiles de hardware
|
|
||||||
- **HardwareTypeComponent**: Tipos de hardware
|
|
||||||
- **HardwareCollectionModalComponent**: Colecciones
|
|
||||||
- **ShowDetailsComponent**: Detalles de hardware
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Perfiles de hardware
|
|
||||||
- Tipos y categorías
|
|
||||||
- Inventario automático
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 11. **Sistemas Operativos** (`operative-system/`)
|
|
||||||
|
|
||||||
Gestión de sistemas operativos.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **OperativeSystemComponent**: Lista de SOs
|
|
||||||
- **CreateOperativeSystemComponent**: Alta de SOs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 12. **Menús** (`menus/`)
|
|
||||||
|
|
||||||
Configuración de menús de arranque.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **MenusComponent**: Gestión de menús
|
|
||||||
- **CreateMenuComponent**: Creación de menús
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 13. **Estado Global** (`global-status/`)
|
|
||||||
|
|
||||||
Dashboard de monitorización general.
|
|
||||||
|
|
||||||
#### Componentes:
|
|
||||||
- **GlobalStatusComponent**: Vista general del sistema
|
|
||||||
- **StatusTabComponent**: Pestañas de estado
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Estado de clientes en tiempo real
|
|
||||||
- Estado de repositorios
|
|
||||||
- Gráficos y estadísticas
|
|
||||||
- Integración con Mercure
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Servicios y Guards
|
|
||||||
|
|
||||||
### Servicios Principales
|
|
||||||
|
|
||||||
#### **AuthService** (`services/auth.service.ts`)
|
|
||||||
- Autenticación basada en JWT
|
|
||||||
- Gestión de tokens (access + refresh)
|
|
||||||
- Sistema de roles jerárquicos:
|
|
||||||
- `super-admin`: Acceso total
|
|
||||||
- `ou-admin`: Administrador de OU
|
|
||||||
- `ou-operator`: Operador de OU
|
|
||||||
- `ou-minimal`: Permisos mínimos
|
|
||||||
- Decodificación y validación de tokens
|
|
||||||
- Logout seguro
|
|
||||||
|
|
||||||
#### **ConfigService** (`services/config.service.ts`)
|
|
||||||
- Carga dinámica de configuración desde `config.json`
|
|
||||||
- Variables de entorno:
|
|
||||||
- `apiUrl`: URL del backend
|
|
||||||
- `mercureUrl`: URL del servidor Mercure
|
|
||||||
|
|
||||||
#### **TranslationService** (`services/translation.service.ts`)
|
|
||||||
- Gestión de idiomas (ES/EN)
|
|
||||||
- Integración con NGX-Translate
|
|
||||||
|
|
||||||
### Guards
|
|
||||||
|
|
||||||
#### **roleGuard** (`guards/role.guard.ts`)
|
|
||||||
- Protección de rutas por roles
|
|
||||||
- Verificación de permisos por página
|
|
||||||
- Redirección en caso de acceso no autorizado
|
|
||||||
|
|
||||||
#### **LogoutGuard** (`guards/logout.guard.ts`)
|
|
||||||
- Prevención de acceso a login si ya está autenticado
|
|
||||||
|
|
||||||
### Interceptors
|
|
||||||
|
|
||||||
#### **CustomInterceptor** (`core/services/custom.interceptor.ts`)
|
|
||||||
- Inyección automática de token JWT
|
|
||||||
- Manejo de errores HTTP
|
|
||||||
- Refresh automático de tokens
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Características Destacadas
|
|
||||||
|
|
||||||
### 🔄 Actualización en Tiempo Real
|
|
||||||
- Integración con **Mercure** para notificaciones push
|
|
||||||
- Actualización automática del estado de clientes
|
|
||||||
- Logs en tiempo real
|
|
||||||
|
|
||||||
### 📊 Visualizaciones
|
|
||||||
- Gráficos con NGX-Charts
|
|
||||||
- Vista de aula con distribución visual
|
|
||||||
- Dashboard de estado global
|
|
||||||
|
|
||||||
### 🌍 Internacionalización (i18n)
|
|
||||||
- Soporte multiidioma (Español/Inglés)
|
|
||||||
- Más de 600 traducciones
|
|
||||||
- Cambio dinámico de idioma
|
|
||||||
|
|
||||||
### 🎨 UX/UI Moderna
|
|
||||||
- Material Design
|
|
||||||
- Responsive (adaptable a móviles)
|
|
||||||
- Tours guiados (Joyride)
|
|
||||||
- Notificaciones toast
|
|
||||||
- Spinners de carga
|
|
||||||
- Modales overlay
|
|
||||||
|
|
||||||
### 🛡️ Seguridad
|
|
||||||
- Autenticación JWT
|
|
||||||
- Guards de rutas
|
|
||||||
- Roles granulares
|
|
||||||
- Interceptores HTTP
|
|
||||||
|
|
||||||
### 📦 Optimización
|
|
||||||
- Lazy Loading de módulos
|
|
||||||
- Tree Shaking
|
|
||||||
- AOT Compilation
|
|
||||||
- Código optimizado para producción
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🖥 Ambiente de Desarrollo
|
|
||||||
|
|
||||||
### Requisitos del Sistema
|
|
||||||
|
|
||||||
- **Sistema Operativo**: Ubuntu 22.04 LTS (o superior)
|
|
||||||
- **Node.js**: v22.1.0
|
|
||||||
- **npm**: 10.7.0
|
|
||||||
- **Angular CLI**: 18.0.1
|
|
||||||
- **Angular Core**: 18.0.0
|
|
||||||
- **Angular Material**: 18.0.0
|
|
||||||
|
|
||||||
### Editor Recomendado
|
|
||||||
- Visual Studio Code 1.87.2 (o superior)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Instalación
|
|
||||||
|
|
||||||
### 1. Instalar Node.js
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Descargar Node.js v22.1.0 desde:
|
|
||||||
# https://nodejs.org
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Instalar npm (si no está incluido)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install npm
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Instalar Angular CLI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g @angular/cli
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Clonar el repositorio
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <url-del-repositorio>
|
|
||||||
cd oggui
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Instalar dependencias
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ogWebconsole
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Ejecutar en modo desarrollo
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ng serve
|
|
||||||
```
|
|
||||||
|
|
||||||
La aplicación estará disponible en `http://localhost:4200/`
|
|
||||||
|
|
||||||
### 7. Compilar para producción
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ng build --configuration production
|
|
||||||
```
|
|
||||||
|
|
||||||
Los archivos compilados estarán en el directorio `dist/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Configuración
|
|
||||||
|
|
||||||
### Archivo de Configuración
|
|
||||||
|
|
||||||
**Ubicación**: `ogWebconsole/src/assets/config.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"apiUrl": "https://127.0.0.1:8443",
|
|
||||||
"mercureUrl": "https://localhost:3000/.well-known/mercure"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Variables Configurables
|
|
||||||
|
|
||||||
| Variable | Descripción | Ejemplo |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `apiUrl` | URL del backend de OpenGnsys | `https://api.opengnsys.com:8443` |
|
|
||||||
| `mercureUrl` | URL del servidor Mercure para eventos en tiempo real | `https://mercure.example.com/.well-known/mercure` |
|
|
||||||
|
|
||||||
### Configuración de Nginx
|
|
||||||
|
|
||||||
Un archivo de configuración de ejemplo está disponible en:
|
|
||||||
```
|
|
||||||
etc/nginx/oggui.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔗 Integración con Servicios Externos
|
|
||||||
|
|
||||||
### Backend OpenGnsys
|
|
||||||
- **Comunicación**: API REST (HTTPS)
|
|
||||||
- **Autenticación**: JWT (Bearer Token)
|
|
||||||
- **Formato**: JSON
|
|
||||||
|
|
||||||
### Mercure Hub
|
|
||||||
- **Propósito**: Notificaciones push en tiempo real
|
|
||||||
- **Protocolo**: Server-Sent Events (SSE)
|
|
||||||
- **Uso**: Estado de clientes, logs, trazas
|
|
||||||
|
|
||||||
### OgGit
|
|
||||||
- **Propósito**: Versionado de imágenes
|
|
||||||
- **Funcionalidades**:
|
|
||||||
- Commits, tags, branches
|
|
||||||
- Push/Pull
|
|
||||||
- Backup de repositorios
|
|
||||||
- Gestión de conflictos
|
|
||||||
|
|
||||||
### Grafana
|
|
||||||
- **Propósito**: Logs en tiempo real
|
|
||||||
- **Integración**: Enlaces directos a dashboards
|
|
||||||
- **Ámbito**: Clientes y componentes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Estructura del Proyecto
|
|
||||||
|
|
||||||
```
|
|
||||||
oggui/
|
|
||||||
├── bin/
|
|
||||||
│ └── start-oggui.sh # Script de inicio
|
|
||||||
├── debian/ # Paquete Debian
|
|
||||||
├── docs/
|
|
||||||
│ └── VERSIONS.md
|
|
||||||
├── etc/
|
|
||||||
│ └── nginx/
|
|
||||||
│ └── oggui.conf # Configuración Nginx
|
|
||||||
├── Jenkins/ # CI/CD
|
|
||||||
├── ogWebconsole/ # 🔥 Aplicación Angular principal
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── app/
|
|
||||||
│ │ │ ├── components/ # Componentes de la aplicación
|
|
||||||
│ │ │ │ ├── admin/ # Administración
|
|
||||||
│ │ │ │ ├── calendar/ # Calendarios
|
|
||||||
│ │ │ │ ├── commands/ # Comandos y tareas
|
|
||||||
│ │ │ │ ├── global-status/ # Estado global
|
|
||||||
│ │ │ │ ├── groups/ # Grupos y clientes
|
|
||||||
│ │ │ │ ├── hardware/ # Hardware
|
|
||||||
│ │ │ │ ├── images/ # Imágenes
|
|
||||||
│ │ │ │ ├── login/ # Autenticación
|
|
||||||
│ │ │ │ ├── menus/ # Menús
|
|
||||||
│ │ │ │ ├── ogboot/ # PXE Boot
|
|
||||||
│ │ │ │ ├── ogdhcp/ # DHCP
|
|
||||||
│ │ │ │ ├── operative-system/ # Sistemas operativos
|
|
||||||
│ │ │ │ ├── repositories/ # Repositorios
|
|
||||||
│ │ │ │ ├── software/ # Software
|
|
||||||
│ │ │ │ └── task-logs/ # Trazas
|
|
||||||
│ │ │ ├── core/
|
|
||||||
│ │ │ │ └── services/
|
|
||||||
│ │ │ │ └── custom.interceptor.ts
|
|
||||||
│ │ │ ├── guards/ # Guards de rutas
|
|
||||||
│ │ │ ├── layout/ # Layouts
|
|
||||||
│ │ │ ├── services/ # Servicios globales
|
|
||||||
│ │ │ ├── shared/ # Componentes compartidos
|
|
||||||
│ │ │ ├── app.module.ts
|
|
||||||
│ │ │ └── app-routing.module.ts
|
|
||||||
│ │ ├── assets/
|
|
||||||
│ │ │ ├── config.json # 🔧 Configuración
|
|
||||||
│ │ │ └── images/ # Recursos gráficos
|
|
||||||
│ │ ├── locale/
|
|
||||||
│ │ │ ├── es.json # Traducciones español
|
|
||||||
│ │ │ └── en.json # Traducciones inglés
|
|
||||||
│ │ └── styles.css
|
|
||||||
│ ├── angular.json
|
|
||||||
│ ├── package.json
|
|
||||||
│ └── tsconfig.json
|
|
||||||
├── CHANGELOG.md # Historial de cambios
|
|
||||||
├── README.md # 📖 Este archivo
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Testing
|
|
||||||
|
|
||||||
### Ejecutar tests unitarios
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ogWebconsole
|
|
||||||
ng test
|
|
||||||
```
|
|
||||||
|
|
||||||
Los tests se ejecutan con **Karma** y **Jasmine**.
|
|
||||||
|
|
||||||
### Resultados
|
|
||||||
|
|
||||||
Los reportes JUnit se generan en:
|
|
||||||
```
|
|
||||||
ogWebconsole/test-results/ogGui-junit-report.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Despliegue
|
|
||||||
|
|
||||||
### Build de producción
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ogWebconsole
|
|
||||||
ng build --configuration production
|
|
||||||
```
|
|
||||||
|
|
||||||
### Empaquetado Debian
|
|
||||||
|
|
||||||
El proyecto incluye archivos para crear un paquete `.deb`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver debian/ para la configuración del paquete
|
|
||||||
```
|
|
||||||
|
|
||||||
### CI/CD con Jenkins
|
|
||||||
|
|
||||||
Pipeline definido en:
|
|
||||||
```
|
|
||||||
Jenkins/Jenkinsfile-deb-pkg
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Changelog
|
|
||||||
|
|
||||||
Para ver el historial detallado de cambios, consultar:
|
|
||||||
- [CHANGELOG.md](CHANGELOG.md)
|
|
||||||
- [docs/VERSIONS.md](docs/VERSIONS.md)
|
|
||||||
|
|
||||||
### Últimas actualizaciones (v0.26.0 - Oct 2025):
|
|
||||||
- ✅ Eliminados campos redundantes en creación/edición de clientes
|
|
||||||
- ✅ Autocompletado de disco en particionador cuando hay solo 1
|
|
||||||
- ✅ Filtrado de sistemas de archivos según tipo de partición
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 👥 Roles y Permisos
|
|
||||||
|
|
||||||
### Jerarquía de Roles
|
|
||||||
|
|
||||||
1. **Super Admin** (`ROLE_SUPER_ADMIN`)
|
|
||||||
- Acceso completo a todas las funcionalidades
|
|
||||||
- Gestión de usuarios y roles
|
|
||||||
- Gestión de variables de entorno
|
|
||||||
|
|
||||||
2. **OU Admin** (`ROLE_ORGANIZATIONAL_UNIT_ADMIN`)
|
|
||||||
- Gestión completa de su unidad organizativa
|
|
||||||
- Todas las funcionalidades excepto administración global
|
|
||||||
|
|
||||||
3. **OU Operator** (`ROLE_ORGANIZATIONAL_UNIT_OPERATOR`)
|
|
||||||
- Operaciones estándar en su OU
|
|
||||||
- Permisos limitados
|
|
||||||
|
|
||||||
4. **OU Minimal** (`ROLE_ORGANIZATIONAL_UNIT_MINIMAL`)
|
|
||||||
- Permisos mínimos de visualización
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠 Tecnologías y Librerías
|
|
||||||
|
|
||||||
### Core
|
|
||||||
- **Angular 18**: Framework SPA
|
|
||||||
- **TypeScript**: Tipado estático
|
|
||||||
- **RxJS**: Programación reactiva
|
|
||||||
|
|
||||||
### UI/UX
|
|
||||||
- **Angular Material**: Componentes UI
|
|
||||||
- **NGX-Charts**: Gráficos
|
|
||||||
- **NGX-Toastr**: Notificaciones
|
|
||||||
- **NGX-Joyride**: Tours interactivos
|
|
||||||
- **Angular CDK**: Drag & Drop
|
|
||||||
|
|
||||||
### Utilidades
|
|
||||||
- **NGX-Translate**: i18n
|
|
||||||
- **JWT-Decode**: Tokens
|
|
||||||
- **PapaParse**: CSV
|
|
||||||
- **Karma/Jasmine**: Testing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Convenciones de Código
|
|
||||||
|
|
||||||
- **Idioma**: Español para UI, comentarios en español
|
|
||||||
- **Estilo**: Angular Style Guide
|
|
||||||
- **Formato**: Prettier/ESLint
|
|
||||||
- **Componentes**: Modular y reutilizable
|
|
||||||
- **Servicios**: Singleton con `providedIn: 'root'`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Soporte
|
|
||||||
|
|
||||||
Para reportar bugs o solicitar funcionalidades, utilizar el sistema de issues del repositorio.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 Licencia
|
|
||||||
|
|
||||||
[Especificar licencia del proyecto]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🙏 Contribuciones
|
|
||||||
|
|
||||||
Las contribuciones son bienvenidas. Por favor:
|
|
||||||
1. Fork del proyecto
|
|
||||||
2. Crear rama feature (`git checkout -b feature/nueva-funcionalidad`)
|
|
||||||
3. Commit de cambios (`git commit -m 'Añadir nueva funcionalidad'`)
|
|
||||||
4. Push a la rama (`git push origin feature/nueva-funcionalidad`)
|
|
||||||
5. Abrir Pull Request
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Desarrollado para OpenGnsys** - Sistema de clonación y administración de equipos
|
|
||||||
|
|
||||||
|
1. Instala la versión mas reciente de Node.js (v22.1.0)
|
||||||
|
<br>`https://nodejs.org`
|
||||||
|
2. En caso de no tener instalado npm
|
||||||
|
<br>`sudo apt install npm`
|
||||||
|
3. Instalar Angular cli >= 12
|
||||||
|
<br>`npm install -g @angular/cli`
|
||||||
|
4. Clona este repositorio en tu máquina local utilizando Git.
|
||||||
|
5. Situate dentro del directorio /ogWebconsole
|
||||||
|
6. Instala las dependencias del proyecto
|
||||||
|
<br>`npm install`.
|
||||||
|
7. Para ejecutar la aplicación en modo de desarrollo, utiliza el comando `ng serve`. La aplicación estará disponible en `http://localhost:4200/`.
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
oggui (0.0.1-1) unstable; urgency=medium
|
oggui (1.0.1+deb-pkg20250310-1) unstable; urgency=medium
|
||||||
|
|
||||||
* Add debian files
|
* Add debian files
|
||||||
* Update .gitignore
|
* Update .gitignore
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@ Standards-Version: 4.5.0
|
||||||
Package: oggui
|
Package: oggui
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
|
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
|
||||||
Depends: ${shlibs:Depends}, ${misc:Depends}, nginx
|
Depends: ${shlibs:Depends}, ${misc:Depends}, nginx, nodejs, npm
|
||||||
Description: OpenGnsys GUI created for the Opengnsys Team
|
Description: OpenGnsys GUI
|
||||||
Opengnsys Graphical Intercface
|
Una interfaz gráfica para OpenGnsys.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,6 @@ set -e
|
||||||
. /usr/share/debconf/confmodule
|
. /usr/share/debconf/confmodule
|
||||||
|
|
||||||
db_input high opengnsys/oggui_ogcoreUrl || true
|
db_input high opengnsys/oggui_ogcoreUrl || true
|
||||||
db_input high opengnsys/oggui_ogmercureUrl || true
|
|
||||||
|
|
||||||
db_go
|
db_go
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
ogWebconsole/dist/oggui/browser /opt/opengnsys/oggui/
|
ogWebconsole/dist/oggui/browser /opt/opengnsys/oggui/browser/
|
||||||
etc /opt/opengnsys/oggui/
|
etc /opt/opengnsys/oggui/
|
||||||
|
bin /opt/opengnsys/oggui/
|
||||||
|
var /opt/opengnsys/oggui/
|
||||||
|
ogWebconsole/*.json /opt/opengnsys/oggui/src/
|
||||||
|
ogWebconsole/*.js /opt/opengnsys/oggui/src/
|
||||||
|
ogWebconsole/src /opt/opengnsys/oggui/src/
|
||||||
ogWebconsole/ssl/* /opt/opengnsys/oggui/etc/nginx/certs/
|
ogWebconsole/ssl/* /opt/opengnsys/oggui/etc/nginx/certs/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,51 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
set -x
|
|
||||||
|
|
||||||
. /usr/share/debconf/confmodule
|
. /usr/share/debconf/confmodule
|
||||||
|
|
||||||
db_get opengnsys/oggui_ogcoreUrl
|
db_get opengnsys/oggui_ogcoreUrl
|
||||||
OGCORE_URL="$RET"
|
OGCORE_URL="$RET"
|
||||||
db_get opengnsys/oggui_ogmercureUrl
|
|
||||||
OGMERCURE_URL="$RET"
|
|
||||||
db_get opengnsys/oggui_oglogUrl
|
|
||||||
OGLOG_URL="$RET"
|
|
||||||
|
|
||||||
# Asegurarse de que el usuario exista
|
# Asegurarse de que el usuario exista
|
||||||
USER="opengnsys"
|
USER="opengnsys"
|
||||||
CONFIG_FILE="/opt/opengnsys/oggui/browser/assets/config.json"
|
HASH_FILE="/opt/opengnsys/oggui/var/lib/oggui/oggui.config.hash"
|
||||||
|
CONFIG_FILE="/opt/opengnsys/oggui/src/.env"
|
||||||
|
|
||||||
restore_config_if_modified() {
|
|
||||||
local new="$1"
|
|
||||||
local backup="$1.bak"
|
|
||||||
|
|
||||||
if [ -f "$backup" ]; then
|
|
||||||
if ! cmp -s "$new" "$backup"; then
|
# Provisionar base de datos si es necesario en caso de instalación.
|
||||||
if [ "$new" = "$CONFIG_FILE" ]; then
|
|
||||||
echo ">>> Fusionando configuración de usuario en $new"
|
|
||||||
# El operador '*' realiza un merge recursivo:
|
|
||||||
# Preserva los valores del usuario (.[1]) y añade solo las claves nuevas del paquete (.[0])
|
|
||||||
jq -s '.[0] * .[1]' "$new" "$backup" > "${new}.tmp" && mv "${new}.tmp" "$new"
|
|
||||||
rm -f "$backup"
|
|
||||||
else
|
|
||||||
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"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo ">>> El archivo $new no ha cambiado desde la última versión, eliminando backup"
|
|
||||||
rm -f "$backup"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Detectar si es una instalación nueva o una actualización
|
# Detectar si es una instalación nueva o una actualización
|
||||||
if [ "$1" = "configure" ] && [ -z "$2" ]; then
|
if [ "$1" = "configure" ] && [ -z "$2" ]; then
|
||||||
jq --arg apiUrl "$OGCORE_URL" --arg mercureUrl "$OGMERCURE_URL" --arg logUrl "$OGLOG_URL" '.apiUrl = $apiUrl | .mercureUrl = $mercureUrl | .logUrl = $logUrl' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
cd /opt/opengnsys/oggui/src/
|
||||||
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf
|
echo NG_APP_BASE_API_URL=$OGCORE_URL > "$CONFIG_FILE"
|
||||||
ln -s $CONFIG_FILE /opt/opengnsys/oggui/etc/config.json
|
npm install -g @angular/cli
|
||||||
mkdir -p /etc/nginx/certs/
|
npm install
|
||||||
cp -p /opt/opengnsys/oggui/etc/nginx/certs/* /etc/nginx/certs/
|
/usr/local/bin/ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production --localize=false
|
||||||
chown -R www-data:www-data /etc/nginx/certs
|
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 daemon-reload
|
||||||
systemctl restart nginx
|
systemctl enable oggui
|
||||||
elif [ "$1" = "configure" ] && [ -n "$2" ]; then
|
elif [ "$1" = "configure" ] && [ -n "$2" ]; then
|
||||||
cd /opt/opengnsys/oggui
|
cd /opt/opengnsys/oggui
|
||||||
echo "Actualización desde la versión $2"
|
echo "Actualización desde la versión $2"
|
||||||
# Si upgrade recupero los archivos de configuracion
|
|
||||||
echo ">>> Backup de archivos de configuración reales en /opt/opengnsys"
|
|
||||||
restore_config_if_modified "/opt/opengnsys/oggui/etc/nginx/oggui.conf"
|
|
||||||
restore_config_if_modified "$CONFIG_FILE"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Cambiar la propiedad de los archivos al usuario especificado
|
# Cambiar la propiedad de los archivos al usuario especificado
|
||||||
chown opengnsys:www-data /opt/opengnsys/
|
chown opengnsys:www-data /opt/opengnsys/
|
||||||
chown -R opengnsys:www-data /opt/opengnsys/oggui
|
chown -R opengnsys:www-data /opt/opengnsys/oggui
|
||||||
|
chmod 755 /opt/opengnsys/oggui/bin/start-oggui.sh
|
||||||
|
# Install http server stuff
|
||||||
|
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf
|
||||||
|
mkdir -p /etc/nginx/certs/
|
||||||
|
cp -p /opt/opengnsys/oggui/etc/nginx/certs/* /etc/nginx/certs/
|
||||||
|
chown -R www-data:www-data /etc/nginx/certs
|
||||||
|
# Reiniciar servicios si es necesario
|
||||||
|
# systemctl restart nombre_del_servicio
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart nginx
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,6 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
backup_file_if_exists() {
|
|
||||||
local original="$1"
|
|
||||||
local backup="$1.bak"
|
|
||||||
|
|
||||||
if [ -e "$original" ]; then
|
|
||||||
echo " - Guardando backup de $original en $backup"
|
|
||||||
cp -a "$original" "$backup"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
CONFIG_FILE="/opt/opengnsys/oggui/browser/assets/config.json"
|
|
||||||
# Asegurarse de que el usuario exista
|
# Asegurarse de que el usuario exista
|
||||||
USER="opengnsys"
|
USER="opengnsys"
|
||||||
HOME_DIR="/opt/opengnsys"
|
HOME_DIR="/opt/opengnsys"
|
||||||
|
|
@ -22,11 +12,4 @@ else
|
||||||
useradd -m -d "$HOME_DIR" -s /bin/bash "$USER"
|
useradd -m -d "$HOME_DIR" -s /bin/bash "$USER"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Si upgrade hago backup del archivo de configuración
|
exit 0
|
||||||
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
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
set -e
|
set -e
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
# Solo eliminar archivos de configuración si se está eliminando el paquete
|
if [ "$1" = "upgrade" ]; then
|
||||||
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
|
# Eliminar enlaces simbólicos creados en postinst
|
||||||
|
rm -f /etc/systemd/system/oggui.service
|
||||||
rm -f /etc/nginx/sites-enabled/oggui.conf
|
rm -f /etc/nginx/sites-enabled/oggui.conf
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl restart nginx
|
systemctl restart nginx
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,3 @@ Type: string
|
||||||
Default: https://127.0.0.1:8443
|
Default: https://127.0.0.1:8443
|
||||||
Description: Introduzca la URL delAPI de OgCore
|
Description: Introduzca la URL delAPI de OgCore
|
||||||
|
|
||||||
Template: opengnsys/oggui_ogmercureUrl
|
|
||||||
Type: string
|
|
||||||
Default: https://127.0.0.1:3000/.well-known/mercure
|
|
||||||
Description: Introduzca el endpoint de mercure
|
|
||||||
|
|
||||||
Template: opengnsys/oggui_oglogUrl
|
|
||||||
Type: string
|
|
||||||
Default: https://127.0.0.1:3030/
|
|
||||||
Description: Oglog Grafana URL
|
|
||||||
|
|
@ -5,7 +5,16 @@
|
||||||
|
|
||||||
override_dh_auto_build:
|
override_dh_auto_build:
|
||||||
cd ogWebconsole && npm install
|
cd ogWebconsole && npm install
|
||||||
cd ogWebconsole && npx ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production
|
cd ogWebconsole && /usr/local/bin/ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production --localize=false
|
||||||
|
|
||||||
override_dh_auto_install:
|
override_dh_auto_install:
|
||||||
dh_auto_install
|
dh_auto_install
|
||||||
|
mkdir -p debian/oggui/opt/opengnsys/oggui/browser
|
||||||
|
mkdir -p debian/oggui/opt/opengnsys/oggui/src/
|
||||||
|
cp -pr ogWebconsole/dist/oggui/browser/* debian/oggui/opt/opengnsys/oggui/browser/
|
||||||
|
rm -rf debian/oggui/opt/opengnsys/oggui/browser/node_modules
|
||||||
|
cp -pr etc debian/oggui/opt/opengnsys/oggui/
|
||||||
|
cp -pr bin debian/oggui/opt/opengnsys/oggui/
|
||||||
|
cp -pr var debian/oggui/opt/opengnsys/oggui/
|
||||||
|
cp -p ogWebconsole/.env debian/oggui/opt/opengnsys/oggui/src/
|
||||||
|
md5sum debian/oggui/opt/opengnsys/oggui/src/.env > debian/oggui/opt/opengnsys/oggui/var/lib/oggui/oggui.config.hash
|
||||||
|
|
|
||||||
|
|
@ -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 @@
|
||||||
# NG_APP_BASE_API_URL=https://127.0.0.1:8443
|
NG_APP_BASE_API_URL=https://127.0.0.1:8443
|
||||||
# NG_APP_OGCORE_MERCURE_BASE_URL=http://localhost:3000/.well-known/mercure
|
|
||||||
|
|
|
||||||
|
|
@ -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,7 +41,3 @@ testem.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
test-results/
|
|
||||||
|
|
||||||
.e2e-state.json
|
|
||||||
playwright-report/
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
"ogWebconsole": {
|
"ogWebconsole": {
|
||||||
|
"i18n": {
|
||||||
|
"sourceLocale": "es",
|
||||||
|
"locales": {
|
||||||
|
"en": "src/locale/en.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
|
|
@ -24,7 +30,7 @@
|
||||||
"builder": "@ngx-env/builder:application",
|
"builder": "@ngx-env/builder:application",
|
||||||
"options": {
|
"options": {
|
||||||
"baseHref": "/oggui/",
|
"baseHref": "/oggui/",
|
||||||
"localize": false,
|
"localize": true,
|
||||||
"aot": true,
|
"aot": true,
|
||||||
"outputPath": "dist/og-webconsole",
|
"outputPath": "dist/og-webconsole",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
|
|
@ -35,23 +41,20 @@
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets",
|
"src/assets",
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "src/locale",
|
"input": "src/locale",
|
||||||
"output": "/locale"
|
"output": "/locale"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/custom-theme.scss",
|
"src/custom-theme.scss",
|
||||||
"src/styles.css",
|
"src/styles.css",
|
||||||
"node_modules/ngx-toastr/toastr.css"
|
"node_modules/ngx-toastr/toastr.css"
|
||||||
],
|
],
|
||||||
"scripts": [],
|
"scripts": []
|
||||||
"allowedCommonJsDependencies": [
|
|
||||||
"rfdc"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
|
@ -63,8 +66,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
"maximumWarning": "35kb",
|
"maximumWarning": "4kb",
|
||||||
"maximumError": "40kb"
|
"maximumError": "10kb"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
|
|
@ -73,6 +76,16 @@
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": false
|
"sourceMap": false
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"localize": [
|
||||||
|
"es-ES"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"localize": [
|
||||||
|
"en-US"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
|
|
@ -91,16 +104,29 @@
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "ogWebconsole:build:development"
|
"buildTarget": "ogWebconsole:build:development"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"buildTarget": "ogWebconsole:build:es"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"buildTarget": "ogWebconsole:build:en"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@ngx-env/builder:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "ogWebconsole:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@ngx-env/builder:karma",
|
"builder": "@ngx-env/builder:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
"zone.js",
|
"zone.js",
|
||||||
"zone.js/testing"
|
"zone.js/testing",
|
||||||
|
"@angular/localize/init"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"assets": [
|
"assets": [
|
||||||
|
|
@ -120,4 +146,4 @@
|
||||||
"cli": {
|
"cli": {
|
||||||
"analytics": "95fac95c-8936-41a8-8c9c-1fae82fe6912"
|
"analytics": "95fac95c-8936-41a8-8c9c-1fae82fe6912"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { setupTestUsers } from './helpers/api';
|
|
||||||
|
|
||||||
export default async function globalSetup() {
|
|
||||||
console.log('\n▶ E2E — creando usuarios de test...');
|
|
||||||
await setupTestUsers();
|
|
||||||
console.log(' ✓ Setup completado\n');
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { teardownTestUsers } from './helpers/api';
|
|
||||||
|
|
||||||
export default async function globalTeardown() {
|
|
||||||
console.log('\n▶ E2E — eliminando usuarios de test...');
|
|
||||||
await teardownTestUsers();
|
|
||||||
console.log(' ✓ Teardown completado\n');
|
|
||||||
}
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
/**
|
|
||||||
* Helpers para crear y borrar usuarios de test vía la API de ogCore.
|
|
||||||
* Se usan en globalSetup / globalTeardown de Playwright.
|
|
||||||
*/
|
|
||||||
import { Agent } from 'undici';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
export const API_URL = process.env['API_URL'] || 'https://ogserver.local/api';
|
|
||||||
export const APP_URL = process.env['APP_URL'] || 'https://localhost:4200/oggui';
|
|
||||||
export const ADMIN_USER = process.env['ADMIN_USER'] || 'ogadmin';
|
|
||||||
export const ADMIN_PASS = process.env['ADMIN_PASS'] || '12345678';
|
|
||||||
export const TEST_PASS = 'Test1234!';
|
|
||||||
|
|
||||||
// Nombres fijos — sin timestamp para que sean consistentes entre procesos
|
|
||||||
export const TEST_USERS = {
|
|
||||||
superAdmin: { username: 'e2e-superadmin', role: 'ROLE_SUPER_ADMIN' },
|
|
||||||
ouAdmin: { username: 'e2e-ouadmin', role: 'ROLE_ORGANIZATIONAL_UNIT_ADMIN' },
|
|
||||||
operator: { username: 'e2e-operator', role: 'ROLE_ORGANIZATIONAL_UNIT_OPERATOR' },
|
|
||||||
minimal: { username: 'e2e-minimal', role: 'ROLE_ORGANIZATIONAL_UNIT_MINIMAL' },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fichero temporal para pasar IRIs del setup al teardown
|
|
||||||
const STATE_FILE = path.join(__dirname, '../../.e2e-state.json');
|
|
||||||
|
|
||||||
// undici Agent que ignora certificados autofirmados (equivalente a curl -k)
|
|
||||||
const insecureAgent = new Agent({ connect: { rejectUnauthorized: false } });
|
|
||||||
|
|
||||||
async function fetchJson(url: string, init: RequestInit): Promise<any> {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
...init,
|
|
||||||
headers: { 'Content-Type': 'application/ld+json', ...(init.headers as any) },
|
|
||||||
dispatcher: insecureAgent,
|
|
||||||
} as any);
|
|
||||||
const text = await res.text();
|
|
||||||
try { return JSON.parse(text); } catch { return text; }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAdminToken(): Promise<string> {
|
|
||||||
const data = await fetchJson(`${API_URL}/auth/login`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username: ADMIN_USER, password: ADMIN_PASS }),
|
|
||||||
});
|
|
||||||
if (!data.token) throw new Error(`Login fallido: ${JSON.stringify(data)}`);
|
|
||||||
return data.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findUserByUsername(token: string, username: string): Promise<string | null> {
|
|
||||||
const data = await fetchJson(`${API_URL}/users?username=${encodeURIComponent(username)}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
const members = data['hydra:member'] ?? [];
|
|
||||||
return members.length > 0 ? members[0]['@id'] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findUserGroupByName(token: string, name: string): Promise<string | null> {
|
|
||||||
const data = await fetchJson(`${API_URL}/user-groups?name=${encodeURIComponent(name)}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
const members = data['hydra:member'] ?? [];
|
|
||||||
return members.length > 0 ? members[0]['@id'] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteResource(token: string, iri: string): Promise<void> {
|
|
||||||
await fetch(`${API_URL.replace('/api', '')}${iri}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
dispatcher: insecureAgent,
|
|
||||||
} as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUserGroup(token: string, role: string, name: string): Promise<string> {
|
|
||||||
const data = await fetchJson(`${API_URL}/user-groups`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
body: JSON.stringify({ name, permissions: [role], enabled: true }),
|
|
||||||
});
|
|
||||||
if (!data['@id']) throw new Error(`No se pudo crear UserGroup ${name}: ${JSON.stringify(data)}`);
|
|
||||||
return data['@id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUser(token: string, username: string, ugIri: string): Promise<string> {
|
|
||||||
const data = await fetchJson(`${API_URL}/users`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
body: JSON.stringify({ username, password: TEST_PASS, enabled: true, userGroups: [ugIri] }),
|
|
||||||
});
|
|
||||||
if (!data['@id']) throw new Error(`No se pudo crear usuario ${username}: ${JSON.stringify(data)}`);
|
|
||||||
return data['@id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setupTestUsers(): Promise<void> {
|
|
||||||
const token = await getAdminToken();
|
|
||||||
const userIris: string[] = [];
|
|
||||||
const ugIris: string[] = [];
|
|
||||||
|
|
||||||
for (const { username, role } of Object.values(TEST_USERS)) {
|
|
||||||
const ugName = `e2e-ug-${username}`;
|
|
||||||
|
|
||||||
// Eliminar usuario previo si existe (limpia runs anteriores)
|
|
||||||
const existingUser = await findUserByUsername(token, username);
|
|
||||||
if (existingUser) {
|
|
||||||
await deleteResource(token, existingUser);
|
|
||||||
console.log(` ↺ Eliminado usuario previo: ${username}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eliminar UserGroup previo si existe
|
|
||||||
const existingUg = await findUserGroupByName(token, ugName);
|
|
||||||
if (existingUg) {
|
|
||||||
await deleteResource(token, existingUg);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ugIri = await createUserGroup(token, role, ugName);
|
|
||||||
const userIri = await createUser(token, username, ugIri);
|
|
||||||
ugIris.push(ugIri);
|
|
||||||
userIris.push(userIri);
|
|
||||||
console.log(` ✓ ${username} [${role}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guarda IRIs en fichero para que teardown pueda leerlos (env vars no siempre pasan entre procesos)
|
|
||||||
fs.writeFileSync(STATE_FILE, JSON.stringify({ userIris, ugIris }, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function teardownTestUsers(): Promise<void> {
|
|
||||||
if (!fs.existsSync(STATE_FILE)) return;
|
|
||||||
|
|
||||||
const { userIris, ugIris } = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
||||||
const token = await getAdminToken();
|
|
||||||
|
|
||||||
for (const iri of [...userIris, ...ugIris]) {
|
|
||||||
await deleteResource(token, iri);
|
|
||||||
console.log(` ✓ Eliminado: ${iri}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.unlinkSync(STATE_FILE);
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { Page, expect } from '@playwright/test';
|
|
||||||
import { APP_URL, TEST_PASS, TEST_USERS } from './api';
|
|
||||||
|
|
||||||
export type Role = keyof typeof TEST_USERS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hace login en la GUI con el usuario de test del rol indicado.
|
|
||||||
* Espera a que la redirección post-login termine (URL cambia a /groups).
|
|
||||||
*/
|
|
||||||
export async function loginAs(page: Page, role: Role): Promise<void> {
|
|
||||||
const { username } = TEST_USERS[role];
|
|
||||||
await page.goto(`${APP_URL}/auth/login`);
|
|
||||||
await page.locator('input[name="username"]').fill(username);
|
|
||||||
await page.locator('input[name="password"]').fill(TEST_PASS);
|
|
||||||
await page.locator('button[type="submit"]').click();
|
|
||||||
await expect(page).toHaveURL(/\/groups/, { timeout: 10_000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navega a una ruta y devuelve la URL final (útil para comprobar redirecciones).
|
|
||||||
*/
|
|
||||||
export async function navigateTo(page: Page, path: string): Promise<string> {
|
|
||||||
await page.goto(`${APP_URL}${path}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
return page.url();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Comprueba que un enlace del sidebar ES visible */
|
|
||||||
export async function expectSidebarLink(page: Page, routerLink: string, visible: true): Promise<void>;
|
|
||||||
/** Comprueba que un enlace del sidebar NO está en el DOM */
|
|
||||||
export async function expectSidebarLink(page: Page, routerLink: string, visible: false): Promise<void>;
|
|
||||||
export async function expectSidebarLink(page: Page, routerLink: string, visible: boolean): Promise<void> {
|
|
||||||
const locator = page.locator(`[routerLink="${routerLink}"], a[href="${routerLink}"]`).first();
|
|
||||||
if (visible) {
|
|
||||||
await expect(locator).toBeVisible({ timeout: 5_000 });
|
|
||||||
} else {
|
|
||||||
await expect(locator).not.toBeAttached({ timeout: 5_000 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { loginAs, navigateTo, expectSidebarLink } from '../helpers/auth';
|
|
||||||
import { APP_URL } from '../helpers/api';
|
|
||||||
|
|
||||||
test.describe('OU Admin — acceso a gestión sin infraestructura', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await loginAs(page, 'ouAdmin');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Sidebar ─────────────────────────────────────────────────────────────────
|
|
||||||
test.describe('Sidebar', () => {
|
|
||||||
test('ve /groups', async ({ page }) => expectSidebarLink(page, '/groups', true));
|
|
||||||
test('ve /dhcp-servers', async ({ page }) => expectSidebarLink(page, '/dhcp-servers', true));
|
|
||||||
test('ve /boot-servers', async ({ page }) => expectSidebarLink(page, '/boot-servers', true));
|
|
||||||
test('ve /calendars', async ({ page }) => expectSidebarLink(page, '/calendars', true));
|
|
||||||
test('ve /repositories', async ({ page }) => expectSidebarLink(page, '/repositories', true));
|
|
||||||
test('ve /menus', async ({ page }) => expectSidebarLink(page, '/menus', true));
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Rutas permitidas ─────────────────────────────────────────────────────────
|
|
||||||
test.describe('Rutas — puede acceder', () => {
|
|
||||||
const allowed = [
|
|
||||||
'/groups', '/commands', '/dhcp-servers', '/boot-servers',
|
|
||||||
'/calendars', '/repositories', '/pxe',
|
|
||||||
];
|
|
||||||
for (const route of allowed) {
|
|
||||||
test(`accede a ${route}`, async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, route);
|
|
||||||
expect(finalUrl).toContain(route);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Rutas bloqueadas (solo super-admin) ──────────────────────────────────────
|
|
||||||
test.describe('Rutas — redirige a /groups cuando accede a rutas de super-admin', () => {
|
|
||||||
const blocked = ['/users', '/roles', '/env-vars', '/import-legacy'];
|
|
||||||
for (const route of blocked) {
|
|
||||||
test(`${route} → redirige a /groups`, async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, route);
|
|
||||||
expect(finalUrl).toContain('/groups');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── GUI: botones de infraestructura OCULTOS ──────────────────────────────────
|
|
||||||
test('NO ve el botón "Añadir OU" en /groups (solo ou-admin no puede crear desde aquí... wait, sí puede)', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/groups`);
|
|
||||||
// ou-admin SÍ ve el botón de añadir OU (no es ou-operator ni ou-minimal)
|
|
||||||
const addOuBtn = page.locator('button.action-button').first();
|
|
||||||
await expect(addOuBtn).toBeVisible({ timeout: 5_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('NO ve botón crear en /dhcp-servers (solo super-admin puede crear servidores DHCP)', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/dhcp-servers`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
// El botón de añadir dhcp tiene *ngIf="auth.isSuperAdmin" → no debe aparecer
|
|
||||||
const addBtn = page.locator('button[color="primary"]').filter({ hasText: /add|añadir/i });
|
|
||||||
await expect(addBtn).not.toBeAttached({ timeout: 5_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('NO ve botón crear en /boot-servers (solo super-admin)', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/boot-servers`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
const addBtn = page.locator('button').filter({ hasText: /add|añadir/i }).first();
|
|
||||||
await expect(addBtn).not.toBeAttached({ timeout: 5_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('NO ve botón crear en /commands (solo super-admin)', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/commands`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
const addBtn = page.locator('button.action-button');
|
|
||||||
await expect(addBtn).not.toBeAttached({ timeout: 5_000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { loginAs, navigateTo, expectSidebarLink } from '../helpers/auth';
|
|
||||||
import { APP_URL } from '../helpers/api';
|
|
||||||
|
|
||||||
test.describe('OU Minimal — solo vista de grupos', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await loginAs(page, 'minimal');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Login ────────────────────────────────────────────────────────────────────
|
|
||||||
test('tras login aterriza en /groups', async ({ page }) => {
|
|
||||||
await expect(page).toHaveURL(/\/groups/);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Sidebar: solo /groups ────────────────────────────────────────────────────
|
|
||||||
test.describe('Sidebar — solo ve /groups', () => {
|
|
||||||
test('ve /groups', async ({ page }) => expectSidebarLink(page, '/groups', true));
|
|
||||||
test('NO ve /dhcp-servers', async ({ page }) => expectSidebarLink(page, '/dhcp-servers', false));
|
|
||||||
test('NO ve /boot-servers', async ({ page }) => expectSidebarLink(page, '/boot-servers', false));
|
|
||||||
test('NO ve /calendars', async ({ page }) => expectSidebarLink(page, '/calendars', false));
|
|
||||||
test('NO ve /repositories', async ({ page }) => expectSidebarLink(page, '/repositories', false));
|
|
||||||
test('NO ve /menus', async ({ page }) => expectSidebarLink(page, '/menus', false));
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Rutas bloqueadas ─────────────────────────────────────────────────────────
|
|
||||||
test.describe('Todas las rutas protegidas redirigen a /groups', () => {
|
|
||||||
const blocked = [
|
|
||||||
'/users', '/roles', '/env-vars',
|
|
||||||
'/commands', '/dhcp-servers', '/boot-servers',
|
|
||||||
'/calendars', '/repositories', '/pxe',
|
|
||||||
'/clients/deploy-image', '/clients/partition-assistant', '/clients/run-script',
|
|
||||||
];
|
|
||||||
for (const route of blocked) {
|
|
||||||
test(`${route} → redirige a /groups`, async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, route);
|
|
||||||
expect(finalUrl).toContain('/groups');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── GUI en /groups: sin botones de acción ────────────────────────────────────
|
|
||||||
test.describe('/groups — sin botones de creación ni edición', () => {
|
|
||||||
test('NO ve botones "Añadir OU" ni "Añadir cliente"', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/groups`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
const actionBtns = page.locator('button.action-button');
|
|
||||||
await expect(actionBtns).not.toBeAttached({ timeout: 5_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('NO ve botones de editar ni eliminar en el árbol de grupos', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/groups`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
// Los botones de editar/borrar nodo tienen *ngIf="auth.userCategory !== 'ou-minimal'"
|
|
||||||
// Se verifican a través del menú contextual que no debería aparecer con opciones de edición
|
|
||||||
const editBtns = page.locator('button').filter({ hasText: /editar|edit|modificar/i });
|
|
||||||
await expect(editBtns).not.toBeAttached({ timeout: 5_000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { loginAs, navigateTo, expectSidebarLink } from '../helpers/auth';
|
|
||||||
import { APP_URL } from '../helpers/api';
|
|
||||||
|
|
||||||
test.describe('OU Operator — solo grupos y operaciones de equipos', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await loginAs(page, 'operator');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Sidebar: solo /groups visible ───────────────────────────────────────────
|
|
||||||
test.describe('Sidebar — solo ve /groups', () => {
|
|
||||||
test('ve /groups', async ({ page }) => expectSidebarLink(page, '/groups', true));
|
|
||||||
test('NO ve /dhcp-servers', async ({ page }) => expectSidebarLink(page, '/dhcp-servers', false));
|
|
||||||
test('NO ve /boot-servers', async ({ page }) => expectSidebarLink(page, '/boot-servers', false));
|
|
||||||
test('NO ve /calendars', async ({ page }) => expectSidebarLink(page, '/calendars', false));
|
|
||||||
test('NO ve /repositories', async ({ page }) => expectSidebarLink(page, '/repositories', false));
|
|
||||||
test('NO ve /menus', async ({ page }) => expectSidebarLink(page, '/menus', false));
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Rutas bloqueadas: redirige a /groups ─────────────────────────────────────
|
|
||||||
test.describe('Rutas de admin → redirigen a /groups', () => {
|
|
||||||
const blocked = [
|
|
||||||
'/users', '/roles', '/env-vars',
|
|
||||||
'/commands', '/dhcp-servers', '/boot-servers',
|
|
||||||
'/calendars', '/repositories', '/pxe',
|
|
||||||
];
|
|
||||||
for (const route of blocked) {
|
|
||||||
test(`${route} → redirige a /groups`, async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, route);
|
|
||||||
expect(finalUrl).toContain('/groups');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Rutas permitidas: operaciones de equipos ─────────────────────────────────
|
|
||||||
test.describe('Rutas de operación — puede acceder', () => {
|
|
||||||
test('accede a /groups', async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, '/groups');
|
|
||||||
expect(finalUrl).toContain('/groups');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accede a /clients/deploy-image', async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, '/clients/deploy-image');
|
|
||||||
expect(finalUrl).toContain('/clients/deploy-image');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accede a /clients/partition-assistant', async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, '/clients/partition-assistant');
|
|
||||||
expect(finalUrl).toContain('/clients/partition-assistant');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accede a /clients/run-script', async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, '/clients/run-script');
|
|
||||||
expect(finalUrl).toContain('/clients/run-script');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accede a /commands-logs (trazas de sus acciones)', async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, '/commands-logs');
|
|
||||||
expect(finalUrl).toContain('/commands-logs');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── GUI en /groups: botones de crear OU/cliente ocultos ─────────────────────
|
|
||||||
test.describe('/groups — botones de creación ocultos', () => {
|
|
||||||
test('NO ve el botón "Añadir OU"', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/groups`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
// *ngIf="auth.userCategory !== 'ou-operator' && ..."
|
|
||||||
const addOuBtn = page.locator('button.action-button').filter({ hasText: /ou|unidad/i });
|
|
||||||
await expect(addOuBtn).not.toBeAttached({ timeout: 5_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('NO ve el botón de menú "Añadir cliente"', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/groups`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
// Los botones de acción principales no deben aparecer para operator
|
|
||||||
const actionBtns = page.locator('button.action-button');
|
|
||||||
await expect(actionBtns).not.toBeAttached({ timeout: 5_000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { loginAs, navigateTo, expectSidebarLink } from '../helpers/auth';
|
|
||||||
import { APP_URL } from '../helpers/api';
|
|
||||||
|
|
||||||
test.describe('Super Admin — acceso total', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await loginAs(page, 'superAdmin');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Sidebar: todos los elementos visibles ───────────────────────────────────
|
|
||||||
test.describe('Sidebar — ve todos los enlaces', () => {
|
|
||||||
const links = [
|
|
||||||
'/groups', '/dhcp-servers', '/boot-servers',
|
|
||||||
'/calendars', '/repositories', '/menus',
|
|
||||||
];
|
|
||||||
for (const link of links) {
|
|
||||||
test(`ve el enlace a ${link}`, async ({ page }) => {
|
|
||||||
await expectSidebarLink(page, link, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Rutas: acceso permitido ─────────────────────────────────────────────────
|
|
||||||
test.describe('Rutas — puede acceder a todas', () => {
|
|
||||||
const routes = [
|
|
||||||
'/users', '/roles', '/env-vars',
|
|
||||||
'/commands', '/pxe', '/dhcp-servers', '/boot-servers',
|
|
||||||
'/calendars', '/repositories',
|
|
||||||
];
|
|
||||||
for (const route of routes) {
|
|
||||||
test(`accede a ${route} sin ser redirigido`, async ({ page }) => {
|
|
||||||
const finalUrl = await navigateTo(page, route);
|
|
||||||
expect(finalUrl).toContain(route);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── GUI: botones de infraestructura visibles ────────────────────────────────
|
|
||||||
test('ve el botón "Añadir OU" en /groups', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/groups`);
|
|
||||||
await expect(page.locator('button.action-button').first()).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ve botones de crear/editar en /dhcp-servers', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/dhcp-servers`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
// Botón de añadir servidor (solo visible para super-admin)
|
|
||||||
const addBtn = page.locator('button').filter({ hasText: /add|añadir|nuevo/i }).first();
|
|
||||||
await expect(addBtn).toBeVisible({ timeout: 8_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ve botones de crear en /commands', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/commands`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
const addBtn = page.locator('button.action-button').first();
|
|
||||||
await expect(addBtn).toBeVisible({ timeout: 8_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ve botones de crear en /boot-servers', async ({ page }) => {
|
|
||||||
await page.goto(`${APP_URL}/boot-servers`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
const addBtn = page.locator('button').filter({ hasText: /add|añadir|nuevo/i }).first();
|
|
||||||
await expect(addBtn).toBeVisible({ timeout: 8_000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "og-webconsole",
|
"name": "og-webconsole",
|
||||||
"version": "1.14.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "og-webconsole",
|
"name": "og-webconsole",
|
||||||
"version": "1.14.0",
|
"version": "0.5.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^18.0.0",
|
"@angular/animations": "^18.0.0",
|
||||||
"@angular/cdk": "~18.0.0",
|
"@angular/cdk": "~18.0.0",
|
||||||
|
|
@ -35,7 +35,6 @@
|
||||||
"@angular/compiler-cli": "^18.0.0",
|
"@angular/compiler-cli": "^18.0.0",
|
||||||
"@angular/localize": "^18.1.0",
|
"@angular/localize": "^18.1.0",
|
||||||
"@ngx-env/builder": "^18.0.1",
|
"@ngx-env/builder": "^18.0.1",
|
||||||
"@playwright/test": "^1.60.0",
|
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@types/papaparse": "^5.3.15",
|
"@types/papaparse": "^5.3.15",
|
||||||
"jasmine-core": "~5.1.0",
|
"jasmine-core": "~5.1.0",
|
||||||
|
|
@ -273,7 +272,6 @@
|
||||||
"version": "18.1.0",
|
"version": "18.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.1.0.tgz",
|
||||||
"integrity": "sha512-K0BhvZ/SIVoGXZVuh1KOJDdgcGlHfFGMGrs58utndndAb+gYXReMfz4GR5cQs2OObH6TKmIOY2EH7Og1CY2tsw==",
|
"integrity": "sha512-K0BhvZ/SIVoGXZVuh1KOJDdgcGlHfFGMGrs58utndndAb+gYXReMfz4GR5cQs2OObH6TKmIOY2EH7Og1CY2tsw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -358,7 +356,6 @@
|
||||||
"version": "18.0.6",
|
"version": "18.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.0.6.tgz",
|
||||||
"integrity": "sha512-9CmlQ8uZ0cHuj+1jDdabbFw90QpjrJzh1/cgeHN/8vAGE4ranxX3l6cqpVxuoWRLkB2ykTHt2xeoTM8y0d9ERA==",
|
"integrity": "sha512-9CmlQ8uZ0cHuj+1jDdabbFw90QpjrJzh1/cgeHN/8vAGE4ranxX3l6cqpVxuoWRLkB2ykTHt2xeoTM8y0d9ERA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -408,7 +405,6 @@
|
||||||
"version": "18.1.0",
|
"version": "18.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-18.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/common/-/common-18.1.0.tgz",
|
||||||
"integrity": "sha512-noHDLarQSCZZh7hyNd0HR61Fut+q4QCVq9qc/jKPglfbV/6nPujQSmSpT+rNJlNuBOrCLuvH/CNBNbiqii+x3g==",
|
"integrity": "sha512-noHDLarQSCZZh7hyNd0HR61Fut+q4QCVq9qc/jKPglfbV/6nPujQSmSpT+rNJlNuBOrCLuvH/CNBNbiqii+x3g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -424,7 +420,6 @@
|
||||||
"version": "18.1.0",
|
"version": "18.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.1.0.tgz",
|
||||||
"integrity": "sha512-JRQzVTeJGSfRLY+dx+gwu/hPQVB8K+5pW12Z42M9x/HBgGW4in0cO2zHkeQPvImqm0nak82Us1Hyf5C+qTlMMQ==",
|
"integrity": "sha512-JRQzVTeJGSfRLY+dx+gwu/hPQVB8K+5pW12Z42M9x/HBgGW4in0cO2zHkeQPvImqm0nak82Us1Hyf5C+qTlMMQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -445,7 +440,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.1.0.tgz",
|
||||||
"integrity": "sha512-BBsogLPJwxkPh7f8RVHsxyyqNE8XpHbAanjB5fAwnU4W6Sw1kR5rFzkeZM3xaRm2MDiC8DovIl6hlf+s/mKYOw==",
|
"integrity": "sha512-BBsogLPJwxkPh7f8RVHsxyyqNE8XpHbAanjB5fAwnU4W6Sw1kR5rFzkeZM3xaRm2MDiC8DovIl6hlf+s/mKYOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "7.24.7",
|
"@babel/core": "7.24.7",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||||
|
|
@ -473,7 +467,6 @@
|
||||||
"version": "18.1.0",
|
"version": "18.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-18.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/core/-/core-18.1.0.tgz",
|
||||||
"integrity": "sha512-/57/s7CD/0CwlN+3FlhVmx7ypCWXjKi5UKtnlBAUg0D1denIf6ADxwTHFZABYZcYBqOTJgeQUtUw9u/A+0CIlg==",
|
"integrity": "sha512-/57/s7CD/0CwlN+3FlhVmx7ypCWXjKi5UKtnlBAUg0D1denIf6ADxwTHFZABYZcYBqOTJgeQUtUw9u/A+0CIlg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -489,7 +482,6 @@
|
||||||
"version": "18.1.0",
|
"version": "18.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.1.0.tgz",
|
||||||
"integrity": "sha512-m+7m9wa+n5dEacd458eSZsZTz0B+HbOtr7/uqM0YTMQaPrhwl1epG5Y103mB6yr00JiJcLNlPLjP888cHFjldQ==",
|
"integrity": "sha512-m+7m9wa+n5dEacd458eSZsZTz0B+HbOtr7/uqM0YTMQaPrhwl1epG5Y103mB6yr00JiJcLNlPLjP888cHFjldQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -508,7 +500,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.1.0.tgz",
|
||||||
"integrity": "sha512-84D06p2Th5NxoJZzsSIn4FkTJGImj7rtNnvyTrHvHdomzzUKwiBOXDB2FiCLDstND0DsCtgjD/uBJivg77z9tg==",
|
"integrity": "sha512-84D06p2Th5NxoJZzsSIn4FkTJGImj7rtNnvyTrHvHdomzzUKwiBOXDB2FiCLDstND0DsCtgjD/uBJivg77z9tg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "7.24.7",
|
"@babel/core": "7.24.7",
|
||||||
"@types/babel__core": "7.20.5",
|
"@types/babel__core": "7.20.5",
|
||||||
|
|
@ -597,7 +588,6 @@
|
||||||
"version": "18.1.0",
|
"version": "18.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.1.0.tgz",
|
||||||
"integrity": "sha512-jCmxthiI4Zef54crckNht60xwfIsuciGeyZvb7SsXna2maLW9fA4uz1VhZqIWTiBnHwNynVlyfBX3/jBD7S9+g==",
|
"integrity": "sha512-jCmxthiI4Zef54crckNht60xwfIsuciGeyZvb7SsXna2maLW9fA4uz1VhZqIWTiBnHwNynVlyfBX3/jBD7S9+g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -619,7 +609,6 @@
|
||||||
"version": "18.1.0",
|
"version": "18.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.1.0.tgz",
|
||||||
"integrity": "sha512-D/wuOQf+gULld9DVEzn2Lw3WbTyAYf/sp3DC5k83O+DQsG3eAIsVkt0zdE+U3DrDYsiWg8M3X+ioi3ouqK0mNg==",
|
"integrity": "sha512-D/wuOQf+gULld9DVEzn2Lw3WbTyAYf/sp3DC5k83O+DQsG3eAIsVkt0zdE+U3DrDYsiWg8M3X+ioi3ouqK0mNg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -677,7 +666,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz",
|
||||||
"integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==",
|
"integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.24.7",
|
"@babel/code-frame": "^7.24.7",
|
||||||
|
|
@ -3229,7 +3217,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.0.7.tgz",
|
||||||
"integrity": "sha512-GFcigCxJTKCH3aECzMIu4FhgLJWnFvMXzpI4CCSoELWFtkOOU2P+goYA61+OKpGrB8fPE7q6n8zAXBSlZRrHjQ==",
|
"integrity": "sha512-GFcigCxJTKCH3aECzMIu4FhgLJWnFvMXzpI4CCSoELWFtkOOU2P+goYA61+OKpGrB8fPE7q6n8zAXBSlZRrHjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/checkbox": "^2.3.7",
|
"@inquirer/checkbox": "^2.3.7",
|
||||||
"@inquirer/confirm": "^3.1.11",
|
"@inquirer/confirm": "^3.1.11",
|
||||||
|
|
@ -4531,6 +4518,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"aix"
|
"aix"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4547,6 +4535,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4563,6 +4552,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4579,6 +4569,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4595,6 +4586,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4611,6 +4603,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4627,6 +4620,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4643,6 +4637,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4659,6 +4654,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4675,6 +4671,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4691,6 +4688,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4707,6 +4705,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4723,6 +4722,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4739,6 +4739,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4755,6 +4756,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4771,6 +4773,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4787,6 +4790,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4803,6 +4807,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"netbsd"
|
"netbsd"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4819,6 +4824,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"openbsd"
|
"openbsd"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4835,6 +4841,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"sunos"
|
"sunos"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4851,6 +4858,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4867,6 +4875,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -4883,6 +4892,7 @@
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -5296,22 +5306,6 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
|
||||||
"version": "1.60.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
|
||||||
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"playwright": "1.60.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.18.0",
|
"version": "4.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
|
||||||
|
|
@ -5897,7 +5891,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz",
|
||||||
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
|
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
|
|
@ -6195,7 +6188,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -6269,7 +6261,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz",
|
||||||
"integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==",
|
"integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"json-schema-traverse": "^1.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
|
@ -6675,7 +6666,6 @@
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001640",
|
"caniuse-lite": "^1.0.30001640",
|
||||||
"electron-to-chromium": "^1.4.820",
|
"electron-to-chromium": "^1.4.820",
|
||||||
|
|
@ -7735,7 +7725,6 @@
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -9821,8 +9810,7 @@
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
|
||||||
"integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
|
"integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/jest-worker": {
|
"node_modules/jest-worker": {
|
||||||
"version": "27.5.1",
|
"version": "27.5.1",
|
||||||
|
|
@ -9971,7 +9959,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz",
|
||||||
"integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==",
|
"integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@colors/colors": "1.5.0",
|
"@colors/colors": "1.5.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
|
|
@ -10272,7 +10259,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
|
||||||
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
|
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"copy-anything": "^2.0.1",
|
"copy-anything": "^2.0.1",
|
||||||
"parse-node-version": "^1.0.1",
|
"parse-node-version": "^1.0.1",
|
||||||
|
|
@ -12202,53 +12188,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
|
||||||
"version": "1.60.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
|
||||||
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"playwright-core": "1.60.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright-core": {
|
|
||||||
"version": "1.60.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
|
||||||
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"playwright-core": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright/node_modules/fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.38",
|
"version": "8.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||||
|
|
@ -12268,7 +12207,6 @@
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.7",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
|
|
@ -12885,7 +12823,6 @@
|
||||||
"version": "7.8.1",
|
"version": "7.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
|
@ -13866,7 +13803,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz",
|
||||||
"integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==",
|
"integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.8.2",
|
"acorn": "^8.8.2",
|
||||||
|
|
@ -13919,7 +13855,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
|
@ -14052,8 +13987,7 @@
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tuf-js": {
|
"node_modules/tuf-js": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
|
|
@ -14105,7 +14039,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -14351,7 +14284,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz",
|
||||||
"integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==",
|
"integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
|
|
@ -14453,7 +14385,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz",
|
||||||
"integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==",
|
"integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.3",
|
"@types/eslint-scope": "^3.7.3",
|
||||||
"@types/estree": "^1.0.5",
|
"@types/estree": "^1.0.5",
|
||||||
|
|
@ -14530,7 +14461,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz",
|
||||||
"integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==",
|
"integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/bonjour": "^3.5.13",
|
"@types/bonjour": "^3.5.13",
|
||||||
"@types/connect-history-api-fallback": "^1.5.4",
|
"@types/connect-history-api-fallback": "^1.5.4",
|
||||||
|
|
@ -14720,7 +14650,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
|
@ -15102,8 +15031,7 @@
|
||||||
"node_modules/zone.js": {
|
"node_modules/zone.js": {
|
||||||
"version": "0.14.7",
|
"version": "0.14.7",
|
||||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.7.tgz",
|
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.7.tgz",
|
||||||
"integrity": "sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg==",
|
"integrity": "sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg=="
|
||||||
"peer": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "og-webconsole",
|
"name": "og-webconsole",
|
||||||
"version": "1.26.0",
|
"version": "0.5.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test",
|
"test": "ng test"
|
||||||
"e2e": "playwright test",
|
|
||||||
"e2e:headed": "playwright test --headed --slow-mo=400",
|
|
||||||
"e2e:ui": "playwright test --ui",
|
|
||||||
"e2e:report": "playwright show-report"
|
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -41,7 +37,6 @@
|
||||||
"@angular/compiler-cli": "^18.0.0",
|
"@angular/compiler-cli": "^18.0.0",
|
||||||
"@angular/localize": "^18.1.0",
|
"@angular/localize": "^18.1.0",
|
||||||
"@ngx-env/builder": "^18.0.1",
|
"@ngx-env/builder": "^18.0.1",
|
||||||
"@playwright/test": "^1.60.0",
|
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@types/papaparse": "^5.3.15",
|
"@types/papaparse": "^5.3.15",
|
||||||
"jasmine-core": "~5.1.0",
|
"jasmine-core": "~5.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { defineConfig, devices } from '@playwright/test';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: './e2e',
|
|
||||||
globalSetup: './e2e/global-setup.ts',
|
|
||||||
globalTeardown: './e2e/global-teardown.ts',
|
|
||||||
timeout: 30_000,
|
|
||||||
retries: 0,
|
|
||||||
workers: 1, // secuencial para no crear conflictos entre usuarios de test
|
|
||||||
reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
|
|
||||||
|
|
||||||
use: {
|
|
||||||
baseURL: 'https://localhost:4200/oggui',
|
|
||||||
ignoreHTTPSErrors: true,
|
|
||||||
screenshot: 'only-on-failure',
|
|
||||||
video: 'retain-on-failure',
|
|
||||||
},
|
|
||||||
|
|
||||||
webServer: {
|
|
||||||
command: 'npm start',
|
|
||||||
url: 'https://localhost:4200/oggui',
|
|
||||||
reuseExistingServer: true,
|
|
||||||
ignoreHTTPSErrors: true,
|
|
||||||
timeout: 120_000,
|
|
||||||
},
|
|
||||||
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'chromium',
|
|
||||||
use: { ...devices['Desktop Chrome'] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
@ -3,26 +3,30 @@ import { RouterModule, Routes } from '@angular/router';
|
||||||
import { MainLayoutComponent } from './layout/main-layout/main-layout.component';
|
import { MainLayoutComponent } from './layout/main-layout/main-layout.component';
|
||||||
import { AuthLayoutComponent } from './layout/auth-layout/auth-layout.component';
|
import { AuthLayoutComponent } from './layout/auth-layout/auth-layout.component';
|
||||||
import { LoginComponent } from './components/login/login.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 { 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 { UsersComponent } from './components/admin/users/users/users.component';
|
||||||
import { RolesComponent } from './components/admin/roles/roles/roles.component';
|
import { RolesComponent } from './components/admin/roles/roles/roles.component';
|
||||||
import { GroupsComponent } from './components/groups/groups.component';
|
import { GroupsComponent } from './components/groups/groups.component';
|
||||||
import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.component';
|
import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.component';
|
||||||
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
|
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
|
||||||
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.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 { CalendarComponent } from "./components/calendar/calendar.component";
|
||||||
import { CommandsComponent } from './components/commands/main-commands/commands.component';
|
import { CommandsComponent } from './components/commands/main-commands/commands.component';
|
||||||
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
|
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
|
||||||
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
|
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
|
||||||
import { TaskLogsComponent } from './components/task-logs/task-logs.component';
|
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
|
||||||
import { SoftwareComponent } from "./components/software/software.component";
|
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||||
import { SoftwareProfileComponent } from "./components/software-profile/software-profile.component";
|
import { ImagesComponent } from './components/images/images.component';
|
||||||
import { OperativeSystemComponent } from "./components/operative-system/operative-system.component";
|
import {SoftwareComponent} from "./components/software/software.component";
|
||||||
|
import {SoftwareProfileComponent} from "./components/software-profile/software-profile.component";
|
||||||
|
import {OperativeSystemComponent} from "./components/operative-system/operative-system.component";
|
||||||
import {
|
import {
|
||||||
PartitionAssistantComponent
|
PartitionAssistantComponent
|
||||||
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
|
} 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 {
|
import {
|
||||||
CreateClientImageComponent
|
CreateClientImageComponent
|
||||||
} from "./components/groups/components/client-main-view/create-image/create-image.component";
|
} from "./components/groups/components/client-main-view/create-image/create-image.component";
|
||||||
|
|
@ -32,67 +36,49 @@ import {
|
||||||
import {
|
import {
|
||||||
MainRepositoryViewComponent
|
MainRepositoryViewComponent
|
||||||
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
||||||
import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component";
|
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
|
||||||
import { MenusComponent } from "./components/menus/menus.component";
|
import {MenusComponent} from "./components/menus/menus.component";
|
||||||
import { ImportLegacyComponent } from './components/admin/import-legacy/import-legacy.component';
|
import {OgDhcpSubnetsComponent} from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
import {StatusComponent} from "./components/ogdhcp/status/status.component";
|
||||||
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
|
|
||||||
import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.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 { HardwareComponent } from './components/hardware/hardware.component';
|
|
||||||
import { HardwareProfileComponent } from './components/hardware-profile/hardware-profile.component';
|
|
||||||
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
|
|
||||||
import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component';
|
|
||||||
import { PluginsComponent } from './components/plugins/plugins.component';
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||||
{
|
{ path: '', component: MainLayoutComponent,
|
||||||
path: '', component: MainLayoutComponent,
|
|
||||||
children: [
|
children: [
|
||||||
{ path: 'users', component: UsersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
{ path: 'dashboard', component: DashboardComponent },
|
||||||
{ path: 'env-vars', component: EnvVarsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
{ path: 'admin', component: AdminComponent },
|
||||||
{ path: 'roles', component: RolesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
{ path: 'users', component: UsersComponent },
|
||||||
{ path: 'import-legacy', component: ImportLegacyComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
{ path: 'env-vars', component: EnvVarsComponent },
|
||||||
|
{ path: 'user-groups', component: RolesComponent },
|
||||||
{ path: 'groups', component: GroupsComponent },
|
{ path: 'groups', component: GroupsComponent },
|
||||||
{ path: 'pxe-images', component: PXEimagesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'pxe-images', component: PXEimagesComponent },
|
||||||
{ path: 'pxe', component: PxeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'pxe', component: PxeComponent },
|
||||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
|
||||||
{ path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'ogboot-status', component: OgbootStatusComponent },
|
||||||
{ path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'subnets', component: OgDhcpSubnetsComponent },
|
||||||
{ path: 'dhcp-servers', component: DhcpServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'ogdhcp-status', component: StatusComponent },
|
||||||
{ path: 'boot-servers', component: BootServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'commands', component: CommandsComponent },
|
||||||
{ path: 'boot-servers/:uuid', component: BootServerDashboardComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'commands-groups', component: CommandsGroupsComponent },
|
||||||
{ path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||||
{ path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'commands-logs', component: TaskLogsComponent },
|
||||||
{ path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'calendars', component: CalendarComponent },
|
||||||
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
{ path: 'clients/deploy-image', component: DeployImageComponent },
|
||||||
{ path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
|
||||||
{ path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
{ path: 'clients/:id', component: ClientMainViewComponent },
|
||||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
|
||||||
{ path: 'clients/run-script', component: RunScriptAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
{ path: 'images', component: ImagesComponent },
|
||||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
{ path: 'repositories', component: RepositoriesComponent },
|
||||||
{ path: 'repositories', component: RepositoriesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||||
{ path: 'repository/:id', component: MainRepositoryViewComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'software', component: SoftwareComponent },
|
||||||
{ path: 'software', component: SoftwareComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'software-profiles', component: SoftwareProfileComponent },
|
||||||
{ path: 'software-profiles', component: SoftwareProfileComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'operative-systems', component: OperativeSystemComponent },
|
||||||
{ path: 'operative-systems', component: OperativeSystemComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
{ path: 'menus', component: MenusComponent },
|
||||||
{ path: 'menus', component: MenusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
|
||||||
{ path: 'hardware', component: HardwareComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
|
||||||
{ path: 'hardware-profiles', component: HardwareProfileComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
|
||||||
{ path: 'hardware-types', component: HardwareTypeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
|
||||||
{ path: 'plugins', component: PluginsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'auth',
|
path: 'auth',
|
||||||
component: AuthLayoutComponent,
|
component: AuthLayoutComponent,
|
||||||
children: [
|
children: [
|
||||||
{ path: 'login', component: LoginComponent, canActivate: [LogoutGuard] },
|
{ path: 'login', component: LoginComponent },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '**', component: PageNotFoundComponent },
|
{ path: '**', component: PageNotFoundComponent },
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,5 @@
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,6 @@ export class AppComponent {
|
||||||
this.translateService.use(savedLanguage);
|
this.translateService.use(savedLanguage);
|
||||||
}
|
}
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Language is already handled in constructor
|
sessionStorage.setItem('language', 'es');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,4 @@
|
||||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, LOCALE_ID, APP_INITIALIZER, Injectable } from '@angular/core';
|
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { MatPaginatorIntl } from '@angular/material/paginator';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
class SpanishPaginatorIntl extends MatPaginatorIntl {
|
|
||||||
override itemsPerPageLabel = 'Elementos por página:';
|
|
||||||
override nextPageLabel = 'Siguiente';
|
|
||||||
override previousPageLabel = 'Anterior';
|
|
||||||
override firstPageLabel = 'Primera página';
|
|
||||||
override lastPageLabel = 'Última página';
|
|
||||||
override getRangeLabel = (page: number, pageSize: number, length: number) => {
|
|
||||||
if (length === 0 || pageSize === 0) return `0 de ${length}`;
|
|
||||||
const startIndex = page * pageSize;
|
|
||||||
const endIndex = Math.min(startIndex + pageSize, length);
|
|
||||||
return `${startIndex + 1} – ${endIndex} de ${length}`;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
import { ConfigService } from './services/config.service';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
|
@ -33,6 +16,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { AdminComponent } from './components/admin/admin.component';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
|
@ -52,16 +36,15 @@ import { MatDividerModule } from '@angular/material/divider';
|
||||||
import { MatStepperModule } from '@angular/material/stepper';
|
import { MatStepperModule } from '@angular/material/stepper';
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
import { DeleteModalComponent } from './shared/delete_modal/delete-modal/delete-modal.component';
|
import { DeleteModalComponent } from './shared/delete_modal/delete-modal/delete-modal.component';
|
||||||
import { DeleteResultsModalComponent } from './components/groups/shared/delete-results-modal/delete-results-modal.component';
|
|
||||||
import { ClassroomViewComponent } from './components/groups/shared/classroom-view/classroom-view.component';
|
import { ClassroomViewComponent } from './components/groups/shared/classroom-view/classroom-view.component';
|
||||||
import { MatProgressSpinner } from "@angular/material/progress-spinner";
|
import { MatProgressSpinner } from "@angular/material/progress-spinner";
|
||||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu";
|
import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu";
|
||||||
import { MatAutocomplete, MatAutocompleteTrigger } from "@angular/material/autocomplete";
|
import { MatAutocomplete, MatAutocompleteTrigger } from "@angular/material/autocomplete";
|
||||||
import { MatChipsModule } from "@angular/material/chips";
|
import { MatChip, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule } from "@angular/material/chips";
|
||||||
import { ClientViewComponent } from './components/groups/shared/client-view/client-view.component';
|
import { ClientViewComponent } from './components/groups/shared/client-view/client-view.component';
|
||||||
import { MatTabsModule } from "@angular/material/tabs";
|
import { MatTab, MatTabGroup } from "@angular/material/tabs";
|
||||||
import { MatTooltipModule } from "@angular/material/tooltip";
|
import { MatTooltip } from "@angular/material/tooltip";
|
||||||
import { MatExpansionModule } from '@angular/material/expansion';
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
import { ToastrModule } from 'ngx-toastr';
|
import { ToastrModule } from 'ngx-toastr';
|
||||||
|
|
@ -83,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 { 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 { InfoImageComponent } from './components/ogboot/pxe-images/info-image/info-image/info-image.component';
|
||||||
import { PxeComponent } from './components/ogboot/pxe/pxe.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 { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
|
||||||
import { MatExpansionPanel, MatExpansionPanelDescription, MatExpansionPanelTitle } from "@angular/material/expansion";
|
import { MatExpansionPanel, MatExpansionPanelDescription, MatExpansionPanelTitle } from "@angular/material/expansion";
|
||||||
import { OgbootStatusComponent } from './components/ogboot/ogboot-status/ogboot-status.component';
|
import { OgbootStatusComponent } from './components/ogboot/ogboot-status/ogboot-status.component';
|
||||||
|
|
@ -103,26 +86,24 @@ import { CreateCommandGroupComponent } from './components/commands/commands-grou
|
||||||
import { DetailCommandGroupComponent } from './components/commands/commands-groups/detail-command-group/detail-command-group.component';
|
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 { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component';
|
||||||
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-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 { 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 { ImagesComponent } from './components/images/images.component';
|
||||||
import { CreateImageComponent } from './components/images/create-image/create-image.component';
|
import { CreateImageComponent } from './components/images/create-image/create-image.component';
|
||||||
import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component';
|
import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component';
|
||||||
import { CreateRepositoryModalComponent } from './components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component';
|
|
||||||
import { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component';
|
import { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component';
|
||||||
import { SoftwareComponent } from './components/software/software.component';
|
import { SoftwareComponent } from './components/software/software.component';
|
||||||
import { CreateSoftwareComponent } from './components/software/create-software/create-software.component';
|
import { CreateSoftwareComponent } from './components/software/create-software/create-software.component';
|
||||||
import { SoftwareProfileComponent } from './components/software-profile/software-profile.component';
|
import { SoftwareProfileComponent } from './components/software-profile/software-profile.component';
|
||||||
import { CreateSoftwareProfileComponent } from './components/software-profile/create-software-profile/create-software-profile.component';
|
import { CreateSoftwareProfileComponent } from './components/software-profile/create-software-profile/create-software-profile.component';
|
||||||
import { OperativeSystemComponent } from './components/operative-system/operative-system.component';
|
import { OperativeSystemComponent } from './components/operative-system/operative-system.component';
|
||||||
import { SafePipe } from './shared/pipes/safe.pipe';
|
|
||||||
import { CreateOperativeSystemComponent } from './components/operative-system/create-operative-system/create-operative-system.component';
|
import { CreateOperativeSystemComponent } from './components/operative-system/create-operative-system/create-operative-system.component';
|
||||||
import { ShowTemplateContentComponent } from './components/ogboot/pxe/show-template-content/show-template-content.component';
|
import { ShowTemplateContentComponent } from './components/ogboot/pxe/show-template-content/show-template-content.component';
|
||||||
import { RepositoriesComponent } from './components/repositories/repositories.component';
|
import { RepositoriesComponent } from './components/repositories/repositories.component';
|
||||||
import { ManageRepositoryComponent } from './components/repositories/manage-repository/manage-repository.component';
|
import { CreateRepositoryComponent } from './components/repositories/create-repository/create-repository.component';
|
||||||
import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component';
|
import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component';
|
||||||
import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component';
|
import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component';
|
||||||
import { DeployMethodConfigDialogComponent } from './components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component';
|
|
||||||
import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component';
|
import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component';
|
||||||
import { ExecuteCommandOuComponent } from './components/groups/shared/execute-command-ou/execute-command-ou.component';
|
import { ExecuteCommandOuComponent } from './components/groups/shared/execute-command-ou/execute-command-ou.component';
|
||||||
import { JoyrideModule } from 'ngx-joyride';
|
import { JoyrideModule } from 'ngx-joyride';
|
||||||
|
|
@ -131,96 +112,27 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||||
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
|
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
|
||||||
import { MatSortModule } from '@angular/material/sort';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
import { MenusComponent } from './components/menus/menus.component';
|
import { MenusComponent } from './components/menus/menus.component';
|
||||||
import { AssetManagerModalComponent } from './components/menus/asset-manager-modal/asset-manager-modal.component';
|
|
||||||
import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component';
|
import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component';
|
||||||
import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component';
|
import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component';
|
||||||
import { ExportImageComponent } from './components/images/export-image/export-image.component';
|
import { ExportImageComponent } from './components/images/export-image/export-image.component';
|
||||||
import { ImportImageComponent } from "./components/repositories/import-image/import-image.component";
|
import { ImportImageComponent } from "./components/repositories/import-image/import-image.component";
|
||||||
import { LoadingComponent } from './shared/loading/loading.component';
|
import { LoadingComponent } from './shared/loading/loading.component';
|
||||||
import { 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 { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
|
||||||
import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
|
import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
|
||||||
import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component";
|
import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||||
|
import { StatusComponent } from "./components/ogdhcp/status/status.component";
|
||||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||||
import { SyncSubnetDialogComponent } from "./components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component";
|
|
||||||
import { SyncResultDialogComponent } from "./components/ogdhcp/sync-result-dialog/sync-result-dialog.component";
|
|
||||||
import { CreateSubnetComponent } from "./components/ogdhcp/create-subnet/create-subnet.component";
|
import { CreateSubnetComponent } from "./components/ogdhcp/create-subnet/create-subnet.component";
|
||||||
import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component";
|
import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component";
|
||||||
import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component';
|
import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component';
|
||||||
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component';
|
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component';
|
||||||
import { PluginsComponent } from './components/plugins/plugins.component';
|
|
||||||
import { PluginSettingsComponent } from './components/plugins/plugin-settings/plugin-settings.component';
|
|
||||||
import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component';
|
import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component';
|
||||||
import { ConvertImageComponent } from './components/repositories/convert-image/convert-image.component';
|
|
||||||
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
|
|
||||||
import { ManageDhcpServerComponent } from './components/ogdhcp/manage-dhcp-server/manage-dhcp-server.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 { ShowGitCommitsComponent } from './components/repositories/show-git-images/show-git-images.component';
|
|
||||||
import { RenameImageComponent } from './components/repositories/rename-image/rename-image.component';
|
|
||||||
import { ClientDetailsComponent } from './components/groups/shared/client-details/client-details.component';
|
|
||||||
import { PartitionTypeOrganizatorComponent } from './components/groups/shared/partition-type-organizator/partition-type-organizator.component';
|
|
||||||
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 { ViewScriptModalComponent } from './components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component';
|
|
||||||
import { OutputDialogComponent } from './components/task-logs/output-dialog/output-dialog.component';
|
|
||||||
import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs/client-task-logs.component';
|
|
||||||
import { BootSoPartitionComponent } from './components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component';
|
|
||||||
import { RemoveCacheImageComponent } from './components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component';
|
|
||||||
import { ChangeParentComponent } from './components/groups/shared/change-parent/change-parent.component';
|
|
||||||
import { SoftwareProfilePartitionComponent } from './components/commands/main-commands/execute-command/software-profile-partition/software-profile-partition.component';
|
|
||||||
import { ExecuteTaskNowComponent } from './components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component';
|
|
||||||
import { ClientPendingTasksComponent } from './components/task-logs/client-pending-tasks/client-pending-tasks.component';
|
|
||||||
import { QueueConfirmationModalComponent } from './shared/queue-confirmation-modal/queue-confirmation-modal.component';
|
|
||||||
import { PluginCommandModalComponent } from './shared/plugin-command-modal/plugin-command-modal.component';
|
|
||||||
import { ModalOverlayComponent } from './shared/modal-overlay/modal-overlay.component';
|
|
||||||
import { ScrollToTopComponent } from './shared/scroll-to-top/scroll-to-top.component';
|
|
||||||
import { CreateTagModalComponent } from './components/repositories/show-git-images/create-tag-modal/create-tag-modal.component';
|
|
||||||
import { CreateBranchModalComponent } from './components/repositories/show-git-images/create-branch-modal/create-branch-modal.component';
|
|
||||||
import { ClientLogsModalComponent } from './components/groups/shared/client-logs-modal/client-logs-modal.component';
|
|
||||||
import { ClientDiskLogsModalComponent } from './components/groups/shared/client-disk-logs-modal/client-disk-logs-modal.component';
|
|
||||||
import { BackupRepositoryModalComponent } from './components/repositories/show-git-images/backup-repository-modal/backup-repository-modal.component';
|
|
||||||
import { HardwareComponent } from './components/hardware/hardware.component';
|
|
||||||
import { CreateHardwareComponent } from './components/hardware/create-hardware/create-hardware.component';
|
|
||||||
import { HardwareProfileComponent } from './components/hardware-profile/hardware-profile.component';
|
|
||||||
import { HardwareCollectionModalComponent } from './components/hardware-profile/hardware-collection-modal/hardware-collection-modal.component';
|
|
||||||
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
|
|
||||||
import { ShowDetailsComponent } from './components/hardware-type/show-details/show-details.component';
|
|
||||||
import { DiskPartitionSelectorComponent } from './components/groups/shared/disk-partition-selector/disk-partition-selector.component';
|
|
||||||
import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
|
|
||||||
import { ManageBootServerComponent } from './components/ogboot/manage-boot-server/manage-boot-server.component';
|
|
||||||
import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component';
|
|
||||||
import { OgcoreStatusComponent } from './components/global-status/ogcore-status/ogcore-status.component';
|
|
||||||
import { GlobalOgbootStatusComponent } from './components/global-status/ogboot-status/ogboot-status.component';
|
|
||||||
import { OgrepositoryStatusComponent } from './components/global-status/ogrepository-status/ogrepository-status.component';
|
|
||||||
import { OgdhcpStatusComponent } from './components/global-status/ogdhcp-status/ogdhcp-status.component';
|
|
||||||
import { StatusLogsComponent } from './components/global-status/status-logs/status-logs.component';
|
|
||||||
export function HttpLoaderFactory(http: HttpClient) {
|
export function HttpLoaderFactory(http: HttpClient) {
|
||||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
import { ImportLegacyComponent } from './components/admin/import-legacy/import-legacy.component';
|
|
||||||
import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component';
|
|
||||||
import { OuSelectComponent } from './components/shared/ou-select/ou-select.component';
|
|
||||||
|
|
||||||
export function initializeApp(configService: ConfigService) {
|
|
||||||
return () => configService.loadConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
registerLocaleData(localeEs, 'es-ES');
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
|
|
@ -229,6 +141,7 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
SidebarComponent,
|
SidebarComponent,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
|
AdminComponent,
|
||||||
MainLayoutComponent,
|
MainLayoutComponent,
|
||||||
UsersComponent,
|
UsersComponent,
|
||||||
RolesComponent,
|
RolesComponent,
|
||||||
|
|
@ -238,9 +151,6 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
GroupsComponent,
|
GroupsComponent,
|
||||||
ManageClientComponent,
|
ManageClientComponent,
|
||||||
DeleteModalComponent,
|
DeleteModalComponent,
|
||||||
DeleteResultsModalComponent,
|
|
||||||
QueueConfirmationModalComponent,
|
|
||||||
PluginCommandModalComponent,
|
|
||||||
ClassroomViewComponent,
|
ClassroomViewComponent,
|
||||||
ClientViewComponent,
|
ClientViewComponent,
|
||||||
ShowOrganizationalUnitComponent,
|
ShowOrganizationalUnitComponent,
|
||||||
|
|
@ -256,8 +166,6 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
OgbootStatusComponent,
|
OgbootStatusComponent,
|
||||||
OgDhcpSubnetsComponent,
|
OgDhcpSubnetsComponent,
|
||||||
CreateSubnetComponent,
|
CreateSubnetComponent,
|
||||||
DhcpServersComponent,
|
|
||||||
ManageDhcpServerComponent,
|
|
||||||
AddClientsToSubnetComponent,
|
AddClientsToSubnetComponent,
|
||||||
CommandsComponent,
|
CommandsComponent,
|
||||||
CommandDetailComponent,
|
CommandDetailComponent,
|
||||||
|
|
@ -265,8 +173,6 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
CalendarComponent,
|
CalendarComponent,
|
||||||
CreateCalendarComponent,
|
CreateCalendarComponent,
|
||||||
CreateClientImageComponent,
|
CreateClientImageComponent,
|
||||||
CreateRepositoryModalComponent,
|
|
||||||
PartitionAssistantComponent,
|
|
||||||
CreateCalendarRuleComponent,
|
CreateCalendarRuleComponent,
|
||||||
CommandsGroupsComponent,
|
CommandsGroupsComponent,
|
||||||
CommandsTaskComponent,
|
CommandsTaskComponent,
|
||||||
|
|
@ -276,10 +182,11 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
DetailTaskComponent,
|
DetailTaskComponent,
|
||||||
TaskLogsComponent,
|
TaskLogsComponent,
|
||||||
ServerInfoDialogComponent,
|
ServerInfoDialogComponent,
|
||||||
SyncSubnetDialogComponent,
|
StatusComponent,
|
||||||
SyncResultDialogComponent,
|
ClientMainViewComponent,
|
||||||
ImagesComponent,
|
ImagesComponent,
|
||||||
CreateImageComponent,
|
CreateImageComponent,
|
||||||
|
PartitionAssistantComponent,
|
||||||
SoftwareComponent,
|
SoftwareComponent,
|
||||||
CreateSoftwareComponent,
|
CreateSoftwareComponent,
|
||||||
SoftwareProfileComponent,
|
SoftwareProfileComponent,
|
||||||
|
|
@ -288,12 +195,12 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
CreateOperativeSystemComponent,
|
CreateOperativeSystemComponent,
|
||||||
ShowTemplateContentComponent,
|
ShowTemplateContentComponent,
|
||||||
RepositoriesComponent,
|
RepositoriesComponent,
|
||||||
ManageRepositoryComponent,
|
CreateRepositoryComponent,
|
||||||
ExecuteCommandComponent,
|
ExecuteCommandComponent,
|
||||||
ExecuteCommandOuComponent,
|
ExecuteCommandOuComponent,
|
||||||
DeployImageComponent,
|
DeployImageComponent,
|
||||||
DeployMethodConfigDialogComponent,
|
|
||||||
MainRepositoryViewComponent,
|
MainRepositoryViewComponent,
|
||||||
|
ExecuteCommandOuComponent,
|
||||||
EnvVarsComponent,
|
EnvVarsComponent,
|
||||||
MenusComponent,
|
MenusComponent,
|
||||||
CreateMenuComponent,
|
CreateMenuComponent,
|
||||||
|
|
@ -301,65 +208,12 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
ExportImageComponent,
|
ExportImageComponent,
|
||||||
ImportImageComponent,
|
ImportImageComponent,
|
||||||
LoadingComponent,
|
LoadingComponent,
|
||||||
|
RepositoryImagesComponent,
|
||||||
InputDialogComponent,
|
InputDialogComponent,
|
||||||
ManageOrganizationalUnitComponent,
|
ManageOrganizationalUnitComponent,
|
||||||
BackupImageComponent,
|
BackupImageComponent,
|
||||||
ShowClientsComponent,
|
ShowClientsComponent,
|
||||||
OperationResultDialogComponent,
|
OperationResultDialogComponent
|
||||||
ConvertImageComponent,
|
|
||||||
GlobalStatusComponent,
|
|
||||||
ShowMonoliticImagesComponent,
|
|
||||||
StatusTabComponent,
|
|
||||||
ConvertImageToVirtualComponent,
|
|
||||||
RunScriptAssistantComponent,
|
|
||||||
SaveScriptComponent,
|
|
||||||
EditImageComponent,
|
|
||||||
ShowGitCommitsComponent,
|
|
||||||
RenameImageComponent,
|
|
||||||
ClientDetailsComponent,
|
|
||||||
PartitionTypeOrganizatorComponent,
|
|
||||||
CreateTaskScheduleComponent,
|
|
||||||
ShowTaskScheduleComponent,
|
|
||||||
ShowTaskScriptComponent,
|
|
||||||
CreateTaskScriptComponent,
|
|
||||||
ViewParametersModalComponent,
|
|
||||||
ViewScriptModalComponent,
|
|
||||||
OutputDialogComponent,
|
|
||||||
ClientTaskLogsComponent,
|
|
||||||
BootSoPartitionComponent,
|
|
||||||
RemoveCacheImageComponent,
|
|
||||||
ChangeParentComponent,
|
|
||||||
SoftwareProfilePartitionComponent,
|
|
||||||
ExecuteTaskNowComponent,
|
|
||||||
ClientPendingTasksComponent,
|
|
||||||
ModalOverlayComponent,
|
|
||||||
ScrollToTopComponent,
|
|
||||||
CreateTagModalComponent,
|
|
||||||
CreateBranchModalComponent,
|
|
||||||
ClientLogsModalComponent,
|
|
||||||
ClientDiskLogsModalComponent,
|
|
||||||
SafePipe,
|
|
||||||
BackupRepositoryModalComponent,
|
|
||||||
HardwareComponent,
|
|
||||||
CreateHardwareComponent,
|
|
||||||
HardwareProfileComponent,
|
|
||||||
HardwareCollectionModalComponent,
|
|
||||||
HardwareTypeComponent,
|
|
||||||
ShowDetailsComponent,
|
|
||||||
AssetManagerModalComponent,
|
|
||||||
BootServersComponent,
|
|
||||||
ManageBootServerComponent,
|
|
||||||
BootServerDashboardComponent,
|
|
||||||
OgcoreStatusComponent,
|
|
||||||
GlobalOgbootStatusComponent,
|
|
||||||
OgrepositoryStatusComponent,
|
|
||||||
OgdhcpStatusComponent,
|
|
||||||
StatusLogsComponent,
|
|
||||||
ImportLegacyComponent,
|
|
||||||
PageNotFoundComponent,
|
|
||||||
OuSelectComponent,
|
|
||||||
PluginsComponent,
|
|
||||||
PluginSettingsComponent
|
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
imports: [BrowserModule,
|
imports: [BrowserModule,
|
||||||
|
|
@ -384,7 +238,7 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
MatProgressBarModule,
|
MatProgressBarModule,
|
||||||
MatStepperModule,
|
MatStepperModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipsModule, MatProgressSpinner, MatTabsModule, MatTooltipModule,
|
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip,
|
||||||
MatExpansionModule,
|
MatExpansionModule,
|
||||||
NgxChartsModule,
|
NgxChartsModule,
|
||||||
MatDatepickerModule,
|
MatDatepickerModule,
|
||||||
|
|
@ -408,7 +262,7 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
progressAnimation: 'increasing',
|
progressAnimation: 'increasing',
|
||||||
closeButton: true
|
closeButton: true
|
||||||
}
|
}
|
||||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger, NgOptimizedImage, DiskPartitionSelectorComponent
|
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger
|
||||||
],
|
],
|
||||||
schemas: [
|
schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA,
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
|
|
@ -419,17 +273,8 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
useClass: CustomInterceptor,
|
useClass: CustomInterceptor,
|
||||||
multi: true
|
multi: true
|
||||||
},
|
},
|
||||||
{ provide: LOCALE_ID, useValue: 'es-ES' },
|
|
||||||
{ provide: MatPaginatorIntl, useClass: SpanishPaginatorIntl },
|
|
||||||
provideAnimationsAsync(),
|
provideAnimationsAsync(),
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi())
|
||||||
ConfigService,
|
|
||||||
{
|
|
||||||
provide: APP_INITIALIZER,
|
|
||||||
useFactory: initializeApp,
|
|
||||||
deps: [ConfigService],
|
|
||||||
multi: true
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,41 +1,33 @@
|
||||||
<div class="env-settings">
|
<div class="env-settings">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-container-title">
|
<div class="header-container-title">
|
||||||
<h2>{{ 'editEnvVarsTitle' | translate }}</h2>
|
<h2>Editar Variables de Entorno</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-table [dataSource]="envVars" class="mat-elevation-z8">
|
<mat-table [dataSource]="envVars" class="mat-elevation-z8">
|
||||||
<!-- Nombre de la variable -->
|
<!-- Nombre de la variable -->
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
<mat-header-cell *matHeaderCellDef> {{ 'labelVariable' | translate }}</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef> Variable </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let variable">{{ variable.name }}</mat-cell>
|
<mat-cell *matCellDef="let variable">{{ variable.name }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Valor de la variable -->
|
<!-- Valor de la variable -->
|
||||||
<ng-container matColumnDef="value">
|
<ng-container matColumnDef="value">
|
||||||
<mat-header-cell *matHeaderCellDef> {{ 'labelValue' | translate }}</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef> Valor </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let variable">
|
<mat-cell *matCellDef="let variable">
|
||||||
<!-- Si es booleano, usamos checkbox -->
|
<mat-form-field class="value-input">
|
||||||
<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">
|
|
||||||
<input matInput [(ngModel)]="variable.value" />
|
<input matInput [(ngModel)]="variable.value" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="action-button" (click)="loadEnvVars()">{{ 'buttonReload' | translate }}</button>
|
<button class="action-button" (click)="loadEnvVars()">Recargar</button>
|
||||||
<button class="submit-button" (click)="saveEnvVars()">{{ 'buttonSaveChanges' | translate }}</button>
|
<button class="submit-button" (click)="saveEnvVars()">Guardar Cambios</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -13,29 +13,24 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||||
import { DataService } from '../users/users/data.service';
|
import { DataService } from '../users/users/data.service';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
describe('EnvVarsComponent', () => {
|
describe('EnvVarsComponent', () => {
|
||||||
let component: EnvVarsComponent;
|
let component: EnvVarsComponent;
|
||||||
let fixture: ComponentFixture<EnvVarsComponent>;
|
let fixture: ComponentFixture<EnvVarsComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const mockConfigService = {
|
|
||||||
apiUrl: 'http://mock-api-url'
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [EnvVarsComponent],
|
declarations: [EnvVarsComponent],
|
||||||
imports: [
|
imports: [
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
ToastrModule.forRoot(),
|
ToastrModule.forRoot(),
|
||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot()
|
||||||
],
|
],
|
||||||
|
|
@ -52,10 +47,6 @@ describe('EnvVarsComponent', () => {
|
||||||
{
|
{
|
||||||
provide: MAT_DIALOG_DATA,
|
provide: MAT_DIALOG_DATA,
|
||||||
useValue: {}
|
useValue: {}
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ConfigService,
|
|
||||||
useValue: mockConfigService
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { HttpClient } from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
import { ToastrService } from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-env-vars',
|
selector: 'app-env-vars',
|
||||||
|
|
@ -11,18 +8,16 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrl: './env-vars.component.css'
|
styleUrl: './env-vars.component.css'
|
||||||
})
|
})
|
||||||
export class EnvVarsComponent {
|
export class EnvVarsComponent {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
envVars: { name: string; value: string }[] = [];
|
envVars: { name: string; value: string }[] = [];
|
||||||
displayedColumns: string[] = ['name', 'value'];
|
displayedColumns: string[] = ['name', 'value'];
|
||||||
private apiUrl: string;
|
|
||||||
|
private apiUrl = `${this.baseUrl}/env-vars`;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService,
|
||||||
private configService: ConfigService
|
) {}
|
||||||
) {
|
|
||||||
this.apiUrl = `${this.configService.apiUrl}/env-vars`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadEnvVars();
|
this.loadEnvVars();
|
||||||
|
|
@ -34,15 +29,12 @@ export class EnvVarsComponent {
|
||||||
this.envVars = Object.entries(response.vars).map(([name, value]) => ({ name, value }));
|
this.envVars = Object.entries(response.vars).map(([name, value]) => ({ name, value }));
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.toastService.error(this.translate.instant('couldNotBeLoaded'));
|
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 {
|
saveEnvVars(): void {
|
||||||
const vars = this.envVars.reduce((acc, variable) => {
|
const vars = this.envVars.reduce((acc, variable) => {
|
||||||
acc[variable.name] = variable.value;
|
acc[variable.name] = variable.value;
|
||||||
|
|
@ -51,11 +43,11 @@ export class EnvVarsComponent {
|
||||||
|
|
||||||
this.http.post(this.apiUrl, { vars }).subscribe({
|
this.http.post(this.apiUrl, { vars }).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.success(this.translate.instant('environmentVariablesSavedCorrectly'));
|
this.toastService.success('Variables de entorno guardadas correctamente.');
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(this.translate.instant('errorSavingVariables'), err);
|
console.error('Error al guardar las variables de entorno:', err);
|
||||||
this.toastService.error(this.translate.instant('couldNotBeLoaded'));
|
this.toastService.error('No se pudieron cargar las variables de entorno.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
.main-content {
|
|
||||||
padding: 20px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
max-width: 400px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-text {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-input-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
font-style: italic;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning-text {
|
|
||||||
color: #f44336;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40px;
|
|
||||||
text-align: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-icon {
|
|
||||||
font-size: 64px;
|
|
||||||
height: 64px;
|
|
||||||
width: 64px;
|
|
||||||
color: #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-banner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #ffebee;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border: 1px solid #ef9a9a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-banner h3 {
|
|
||||||
color: #c62828;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-banner p {
|
|
||||||
color: #c62828;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
height: 48px;
|
|
||||||
width: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-box {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-box mat-spinner {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unified-log-box {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-align: left;
|
|
||||||
margin-top: 30px;
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
color: #e0e0e0;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unified-log-box h4 {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: #4caf50;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unified-log-box pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-width-card {
|
|
||||||
width: 100% !important;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
<div class="main-content">
|
|
||||||
<div class="header-container">
|
|
||||||
<div class="header-container-title">
|
|
||||||
<h2>{{ 'importLegacyDbTitle' | translate }}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<mat-card class="full-width-card">
|
|
||||||
<mat-card-header>
|
|
||||||
<mat-card-title>{{ 'importLegacyDbDesc' | translate }}</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
<mat-card-content>
|
|
||||||
@if (isLoading) {
|
|
||||||
<div class="loader-box">
|
|
||||||
<mat-spinner diameter="50"></mat-spinner>
|
|
||||||
<h3>{{ 'migrationInProgress' | translate }}</h3>
|
|
||||||
<p class="warning-text">{{ 'migrationWaitWarning' | translate }}</p>
|
|
||||||
</div>
|
|
||||||
} @else if (migrationStatus === 'success') {
|
|
||||||
<div class="success-container">
|
|
||||||
<mat-icon color="primary" class="success-icon">check_circle</mat-icon>
|
|
||||||
<h3>{{ 'migrationSuccess' | translate }}</h3>
|
|
||||||
<p>Los datos han sido migrados correctamente a la nueva versión.</p>
|
|
||||||
<button mat-raised-button color="primary" (click)="resetForm()">Realizar otra importación</button>
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
@if (migrationStatus === 'error') {
|
|
||||||
<div class="error-banner">
|
|
||||||
<mat-icon color="warn" class="error-icon">error</mat-icon>
|
|
||||||
<h3>{{ 'migrationError' | translate }}</h3>
|
|
||||||
<p>Revisa el log en la parte inferior para más detalles o corrige los parámetros e inténtalo de nuevo.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<mat-radio-group [(ngModel)]="importMode" class="radio-group">
|
|
||||||
<mat-radio-button value="db">{{ 'fromRemoteDb' | translate }}</mat-radio-button>
|
|
||||||
<mat-radio-button value="file">{{ 'fromSqlFile' | translate }}</mat-radio-button>
|
|
||||||
</mat-radio-group>
|
|
||||||
|
|
||||||
@if (importMode === 'db') {
|
|
||||||
<form [formGroup]="dbForm" (ngSubmit)="onSubmit()" class="form-container">
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>{{ 'hostLabel' | translate }}</mat-label>
|
|
||||||
<input matInput formControlName="legacyHost" placeholder="192.168.1.100">
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>{{ 'userLabel' | translate }}</mat-label>
|
|
||||||
<input matInput formControlName="legacyUser" placeholder="root">
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>{{ 'passLabel' | translate }}</mat-label>
|
|
||||||
<input matInput formControlName="legacyPassword" type="password">
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>{{ 'dbLabel' | translate }}</mat-label>
|
|
||||||
<input matInput formControlName="legacyDb" placeholder="opengnsys">
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<button mat-raised-button color="primary" type="submit" [disabled]="dbForm.invalid">
|
|
||||||
<mat-icon>cloud_download</mat-icon> {{ 'importButton' | translate }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (importMode === 'file') {
|
|
||||||
<div class="form-container">
|
|
||||||
<p class="help-text">{{ 'uploadSqlOrZip' | translate }}</p>
|
|
||||||
<div class="file-input-container">
|
|
||||||
<button mat-stroked-button type="button" (click)="fileInput.click()">
|
|
||||||
<mat-icon>attach_file</mat-icon> {{ 'sqlFileLabel' | translate }}
|
|
||||||
</button>
|
|
||||||
<input hidden (change)="onFileSelected($event)" #fileInput type="file" accept=".sql,.zip">
|
|
||||||
@if (selectedFile) {
|
|
||||||
<span class="file-name">{{ selectedFile.name }}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!selectedFile" style="margin-top: 15px;">
|
|
||||||
<mat-icon>upload_file</mat-icon> {{ 'importButton' | translate }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (outputLog) {
|
|
||||||
<div class="unified-log-box">
|
|
||||||
<h4>Consola en vivo:</h4>
|
|
||||||
<pre>{{ outputLog }}</pre>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import { Component, NgZone, ChangeDetectorRef } from '@angular/core';
|
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { ConfigService } from '../../../services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-import-legacy',
|
|
||||||
templateUrl: './import-legacy.component.html',
|
|
||||||
styleUrls: ['./import-legacy.component.css']
|
|
||||||
})
|
|
||||||
export class ImportLegacyComponent {
|
|
||||||
importMode: 'file' | 'db' = 'db';
|
|
||||||
dbForm: FormGroup;
|
|
||||||
selectedFile: File | null = null;
|
|
||||||
isLoading = false;
|
|
||||||
outputLog = '';
|
|
||||||
migrationStatus: 'idle' | 'success' | 'error' = 'idle';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private fb: FormBuilder,
|
|
||||||
private http: HttpClient,
|
|
||||||
private snackBar: MatSnackBar,
|
|
||||||
private translate: TranslateService,
|
|
||||||
private configService: ConfigService,
|
|
||||||
private ngZone: NgZone,
|
|
||||||
private cdr: ChangeDetectorRef
|
|
||||||
) {
|
|
||||||
this.dbForm = this.fb.group({
|
|
||||||
legacyHost: ['', Validators.required],
|
|
||||||
legacyUser: ['', Validators.required],
|
|
||||||
legacyPassword: [''],
|
|
||||||
legacyDb: ['', Validators.required]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.checkMigrationStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
checkMigrationStatus() {
|
|
||||||
this.http.get<any>(`${this.configService.apiUrl}/migration/import/status`).subscribe({
|
|
||||||
next: (response) => {
|
|
||||||
if (response.isRunning) {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.outputLog = response.log || 'Recuperando estado de migración...';
|
|
||||||
this.connectToMercure();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error checking migration status:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private connectToMercure() {
|
|
||||||
const url = new URL(this.configService.mercureUrl);
|
|
||||||
url.searchParams.append('topic', '/migration');
|
|
||||||
const eventSource = new EventSource(url.toString());
|
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
|
||||||
this.ngZone.run(() => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
|
|
||||||
if (data.type === 'migration_progress') {
|
|
||||||
this.outputLog = data.output;
|
|
||||||
this.cdr.detectChanges();
|
|
||||||
}
|
|
||||||
else if (data.type === 'migration_result') {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.migrationStatus = data.status;
|
|
||||||
this.outputLog = data.output;
|
|
||||||
this.cdr.detectChanges();
|
|
||||||
|
|
||||||
if (data.status === 'success') {
|
|
||||||
this.snackBar.open(this.translate.instant('migrationSuccess'), 'OK', { duration: 5000 });
|
|
||||||
} else {
|
|
||||||
this.snackBar.open(this.translate.instant('migrationError'), 'OK', { duration: 5000 });
|
|
||||||
}
|
|
||||||
eventSource.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSource.onerror = (err) => {
|
|
||||||
console.error("Mercure connection error", err);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onFileSelected(event: any) {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
this.selectedFile = file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetForm() {
|
|
||||||
this.migrationStatus = 'idle';
|
|
||||||
this.outputLog = '';
|
|
||||||
this.selectedFile = null;
|
|
||||||
this.dbForm.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit() {
|
|
||||||
if (this.importMode === 'file' && !this.selectedFile) {
|
|
||||||
this.snackBar.open('Please select a file', 'Close', { duration: 3000 });
|
|
||||||
} else if ((this.importMode === 'db' && this.dbForm.valid) || (this.importMode === 'file' && this.selectedFile)) {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.outputLog = '';
|
|
||||||
this.migrationStatus = 'idle';
|
|
||||||
|
|
||||||
let request;
|
|
||||||
if (this.importMode === 'file') {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('sql_file', this.selectedFile!);
|
|
||||||
request = this.http.post<any>(`${this.configService.apiUrl}/migration/import`, formData);
|
|
||||||
} else {
|
|
||||||
request = this.http.post<any>(`${this.configService.apiUrl}/migration/import`, this.dbForm.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
request.subscribe({
|
|
||||||
next: (response) => {
|
|
||||||
if (response.status === 'started') {
|
|
||||||
this.outputLog = 'Proceso de migración en curso. Esperando respuesta del servidor en segundo plano...';
|
|
||||||
this.connectToMercure();
|
|
||||||
} else {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.migrationStatus = 'success';
|
|
||||||
this.outputLog = response.output || response.message;
|
|
||||||
this.snackBar.open(this.translate.instant('migrationSuccess'), 'OK', { duration: 5000 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.migrationStatus = 'error';
|
|
||||||
const details = error.error?.error || error.error?.details || error.message || 'Unknown error';
|
|
||||||
this.outputLog = details;
|
|
||||||
this.snackBar.open(this.translate.instant('migrationError'), 'OK', { duration: 5000 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<p><mat-checkbox formControlName="orgAdmin">{{ 'checkboxOrgAdmin' | translate }}</mat-checkbox></p>
|
<p><mat-checkbox formControlName="orgAdmin">{{ 'checkboxOrgAdmin' | translate }}</mat-checkbox></p>
|
||||||
<p><mat-checkbox formControlName="orgOperator">{{ 'checkboxOrgOperator' | translate }}</mat-checkbox></p>
|
<p><mat-checkbox formControlName="orgOperator">{{ 'checkboxOrgOperator' | translate }}</mat-checkbox></p>
|
||||||
<p><mat-checkbox formControlName="orgMinimal">{{ 'checkboxOrgMinimal' | translate }}</mat-checkbox></p>
|
<p><mat-checkbox formControlName="orgMinimal">{{ 'checkboxOrgMinimal' | translate }}</mat-checkbox></p>
|
||||||
<p><mat-checkbox formControlName="userRole">{{ 'labelUser' | translate }}</mat-checkbox></p>
|
<p><mat-checkbox formControlName="userRole">{{ 'checkboxUserRole' | translate }}</mat-checkbox></p>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||||
import {DataService} from "../data.service";
|
import {DataService} from "../data.service";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-add-role-modal',
|
selector: 'app-add-role-modal',
|
||||||
|
|
@ -14,10 +11,9 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrls: ['./add-role-modal.component.css']
|
styleUrls: ['./add-role-modal.component.css']
|
||||||
})
|
})
|
||||||
export class AddRoleModalComponent {
|
export class AddRoleModalComponent {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
roleForm: FormGroup<any>;
|
roleForm: FormGroup<any>;
|
||||||
roleId: string | null = null;
|
roleId: string | null = null;
|
||||||
baseUrl: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public dialogRef: MatDialogRef<AddRoleModalComponent>,
|
public dialogRef: MatDialogRef<AddRoleModalComponent>,
|
||||||
|
|
@ -25,10 +21,8 @@ export class AddRoleModalComponent {
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService
|
||||||
private configService: ConfigService
|
|
||||||
) {
|
) {
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.roleForm = this.fb.group({
|
this.roleForm = this.fb.group({
|
||||||
name: ['', Validators.required],
|
name: ['', Validators.required],
|
||||||
superAdmin: [false],
|
superAdmin: [false],
|
||||||
|
|
@ -59,7 +53,7 @@ export class AddRoleModalComponent {
|
||||||
this.roleId = response['@id'];
|
this.roleId = response['@id'];
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(this.translate.instant('errorFetchingRemoteCalendar'), err);
|
console.error('Error fetching remote calendar:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -88,23 +82,23 @@ export class AddRoleModalComponent {
|
||||||
if (this.roleId) {
|
if (this.roleId) {
|
||||||
this.http.put(`${this.baseUrl}${this.roleId}`, payload).subscribe(
|
this.http.put(`${this.baseUrl}${this.roleId}`, payload).subscribe(
|
||||||
(response) => {
|
(response) => {
|
||||||
this.toastService.success(this.translate.instant('roleUpdatedSuccessfully'));
|
this.toastService.success('Rol actualizado correctamente');
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.toastService.error(this.translate.instant('errorEditingRole1'), error);
|
this.toastService.error('Error al editar el rol:', error);
|
||||||
console.error(this.translate.instant('errorEditingRole'), error);
|
console.error('Error al editar el rol', error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.http.post(`${this.baseUrl}/user-groups`, payload).subscribe(
|
this.http.post(`${this.baseUrl}/user-groups`, payload).subscribe(
|
||||||
(response) => {
|
(response) => {
|
||||||
this.toastService.success(this.translate.instant('roleAddedSuccessfully'));
|
this.toastService.success('Rol añadido correctamente');
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.toastService.error(this.translate.instant('errorAddingRole1'), error);
|
this.toastService.error('Error al añadir rol:', error);
|
||||||
console.error(this.translate.instant('errorAddingRole'), error);
|
console.error('Error al añadir rol', error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,16 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {HttpClient, HttpParams} from '@angular/common/http';
|
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||||
import { Observable, throwError } from 'rxjs';
|
import { Observable, throwError } from 'rxjs';
|
||||||
import { catchError, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DataService {
|
export class DataService {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
private apiUrl = `${this.baseUrl}/user-groups?page=1&itemsPerPage=1000`;
|
||||||
private apiUrl: string;
|
|
||||||
|
|
||||||
constructor(private http: HttpClient, private configService: ConfigService) {
|
constructor(private http: HttpClient) {}
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.apiUrl = `${this.baseUrl}/user-groups?page=1&itemsPerPage=1000`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserGroups(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
getUserGroups(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
||||||
const params = new HttpParams({ fromObject: filters });
|
const params = new HttpParams({ fromObject: filters });
|
||||||
|
|
@ -34,7 +27,7 @@ export class DataService {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(this.translate.instant('errorFetchingUserGroups'), error);
|
console.error('Error fetching user groups', error);
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -50,7 +43,7 @@ export class DataService {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(this.translate.instant('errorFetchingUserGroup'), error);
|
console.error('Error fetching user group', error);
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
|
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<mat-form-field appearance="fill" class="search-string">
|
<mat-form-field appearance="fill" class="search-string">
|
||||||
<mat-label>{{ 'labelSearchRole' | translate }}</mat-label>
|
<mat-label>{{ 'searchRoleLabel' | translate }}</mat-label>
|
||||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']"
|
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
|
||||||
(keyup.enter)="search()">
|
(keyup.enter)="search()">
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||||
|
|
@ -24,12 +24,12 @@
|
||||||
<div *ngIf="!loading">
|
<div *ngIf="!loading">
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||||
<td mat-cell *matCellDef="let role"> {{ column.cell(role) }} </td>
|
<td mat-cell *matCellDef="let role"> {{ column.cell(role) }} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||||
<td mat-cell *matCellDef="let role" style="text-align: center;">
|
<td mat-cell *matCellDef="let role" style="text-align: center;">
|
||||||
<button mat-icon-button color="primary" (click)="editRole(role)">
|
<button mat-icon-button color="primary" (click)="editRole(role)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material/dialog';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { ConfigService } from '@services/config.service';
|
import { of } from 'rxjs';
|
||||||
import { MatDivider } from '@angular/material/divider';
|
import { MatDivider } from '@angular/material/divider';
|
||||||
import { MatFormField } from '@angular/material/form-field';
|
import { MatFormField } from '@angular/material/form-field';
|
||||||
import { MatLabel } from '@angular/material/form-field';
|
import { MatLabel } from '@angular/material/form-field';
|
||||||
|
|
@ -20,14 +20,12 @@ describe('RolesComponent', () => {
|
||||||
let mockHttpClient: jasmine.SpyObj<HttpClient>;
|
let mockHttpClient: jasmine.SpyObj<HttpClient>;
|
||||||
let mockToastrService: jasmine.SpyObj<ToastrService>;
|
let mockToastrService: jasmine.SpyObj<ToastrService>;
|
||||||
let mockDataService: jasmine.SpyObj<DataService>;
|
let mockDataService: jasmine.SpyObj<DataService>;
|
||||||
let mockConfigService: jasmine.SpyObj<ConfigService>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
|
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
|
||||||
const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']);
|
const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']);
|
||||||
const toastrServiceSpy = jasmine.createSpyObj('ToastrService', ['success', 'error']);
|
const toastrServiceSpy = jasmine.createSpyObj('ToastrService', ['success', 'error']);
|
||||||
const dataServiceSpy = jasmine.createSpyObj('DataService', ['getRoles']);
|
const dataServiceSpy = jasmine.createSpyObj('DataService', ['getRoles']);
|
||||||
const configServiceSpy = jasmine.createSpyObj('ConfigService', [], { apiUrl: 'http://mock-api-url' });
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [RolesComponent, LoadingComponent],
|
declarations: [RolesComponent, LoadingComponent],
|
||||||
|
|
@ -37,8 +35,7 @@ describe('RolesComponent', () => {
|
||||||
{ provide: MatDialog, useValue: matDialogSpy },
|
{ provide: MatDialog, useValue: matDialogSpy },
|
||||||
{ provide: HttpClient, useValue: httpClientSpy },
|
{ provide: HttpClient, useValue: httpClientSpy },
|
||||||
{ provide: ToastrService, useValue: toastrServiceSpy },
|
{ provide: ToastrService, useValue: toastrServiceSpy },
|
||||||
{ provide: DataService, useValue: dataServiceSpy },
|
{ provide: DataService, useValue: dataServiceSpy }
|
||||||
{ provide: ConfigService, useValue: configServiceSpy }
|
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
@ -50,7 +47,6 @@ describe('RolesComponent', () => {
|
||||||
mockHttpClient = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
|
mockHttpClient = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
|
||||||
mockToastrService = TestBed.inject(ToastrService) as jasmine.SpyObj<ToastrService>;
|
mockToastrService = TestBed.inject(ToastrService) as jasmine.SpyObj<ToastrService>;
|
||||||
mockDataService = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
|
mockDataService = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
|
||||||
mockConfigService = TestBed.inject(ConfigService) as jasmine.SpyObj<ConfigService>;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
|
@ -9,7 +7,6 @@ import { DataService } from "./data.service";
|
||||||
import { PageEvent } from "@angular/material/paginator";
|
import { PageEvent } from "@angular/material/paginator";
|
||||||
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
|
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||||
import { AddRoleModalComponent } from './add-role-modal/add-role-modal.component';
|
import { AddRoleModalComponent } from './add-role-modal/add-role-modal.component';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-roles',
|
selector: 'app-roles',
|
||||||
|
|
@ -17,8 +14,7 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrls: ['./roles.component.css']
|
styleUrls: ['./roles.component.css']
|
||||||
})
|
})
|
||||||
export class RolesComponent implements OnInit {
|
export class RolesComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string = this.configService.apiUrl;
|
|
||||||
dataSource = new MatTableDataSource<any>();
|
dataSource = new MatTableDataSource<any>();
|
||||||
filters: { [key: string]: string } = {};
|
filters: { [key: string]: string } = {};
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
|
|
@ -30,17 +26,17 @@ export class RolesComponent implements OnInit {
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
columnDef: 'id',
|
columnDef: 'id',
|
||||||
header: 'labelId',
|
header: 'ID',
|
||||||
cell: (role: any) => `${role.id}`
|
cell: (role: any) => `${role.id}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'name',
|
columnDef: 'name',
|
||||||
header: 'labelName',
|
header: 'Nombre',
|
||||||
cell: (role: any) => `${role.name}`
|
cell: (role: any) => `${role.name}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'permissions',
|
columnDef: 'permissions',
|
||||||
header: 'permissions',
|
header: 'Permisos',
|
||||||
cell: (role: any) => `${role.permissions.join(', ')}`
|
cell: (role: any) => `${role.permissions.join(', ')}`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
@ -52,8 +48,7 @@ export class RolesComponent implements OnInit {
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService
|
||||||
private configService: ConfigService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|
@ -69,7 +64,7 @@ export class RolesComponent implements OnInit {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error(this.translate.instant('errorFetchingRoles'), error);
|
console.error('Error fetching roles', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -111,10 +106,10 @@ export class RolesComponent implements OnInit {
|
||||||
this.http.delete(apiUrl).subscribe({
|
this.http.delete(apiUrl).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.search();
|
this.search();
|
||||||
this.toastService.success(this.translate.instant('roleDeletedSuccessfully'));
|
this.toastService.success('Role deleted successfully');
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.toastService.error(this.translate.instant('errorDeletingRole'), error);
|
this.toastService.error('Error deleting role:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,45 @@
|
||||||
<app-loading [isLoading]="loading"></app-loading>
|
<app-loading [isLoading]="loading"></app-loading>
|
||||||
|
|
||||||
<h1 mat-dialog-title>{{ isEditMode ? ('dialogTitleEditUser' | translate) : ('dialogTitleAddUser' | translate) }}</h1>
|
<h1 mat-dialog-title>{{ 'dialogTitleAddUser' | translate }}</h1>
|
||||||
<mat-dialog-content class="form-container">
|
<mat-dialog-content class="form-container">
|
||||||
<form [formGroup]="userForm" class="user-form">
|
<form [formGroup]="userForm" class="user-form">
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'labelAddUserUsername' | translate }}</mat-label>
|
<mat-label>{{ 'addUserlabelUsername' | translate }}</mat-label>
|
||||||
<input matInput formControlName="username" required>
|
<input matInput formControlName="username" required>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'labelAddUserPassword' | translate }}</mat-label>
|
<mat-label>{{ 'addUserlabelPassword' | translate }}</mat-label>
|
||||||
<input matInput formControlName="password" type="password" required>
|
<input matInput formControlName="password" type="password" required>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'labelRole' | translate }}</mat-label>
|
<mat-label>{{ 'labelRole' | translate }}</mat-label>
|
||||||
<mat-select formControlName="role" required>
|
<mat-select formControlName="role" required>
|
||||||
@for (group of userGroups; track group['@id']) {
|
<mat-option *ngFor="let group of userGroups" [value]="group['@id']">
|
||||||
<mat-option [value]="group['@id']">
|
{{ group.name }}
|
||||||
{{ group.name }}
|
</mat-option>
|
||||||
</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<app-ou-select
|
|
||||||
class="full-width"
|
|
||||||
formControlName="organizationalUnits"
|
|
||||||
[multiple]="true"
|
|
||||||
[units]="organizationalUnits"
|
|
||||||
[label]="'labelOrganizationalUnit'">
|
|
||||||
</app-ou-select>
|
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'cardView' | translate }}</mat-label>
|
<mat-label>{{ 'labelOrganizationalUnit' | translate }}</mat-label>
|
||||||
|
<mat-select multiple formControlName="organizationalUnits">
|
||||||
|
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit['@id']">
|
||||||
|
{{ unit.name }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
|
<mat-label>Vista tarjetas</mat-label>
|
||||||
<mat-select formControlName="groupsView" required>
|
<mat-select formControlName="groupsView" required>
|
||||||
@for (option of views; track option.value) {
|
<mat-option *ngFor="let option of views" [value]="option.value" >
|
||||||
<mat-option [value]="option.value">
|
{{ option.name }}
|
||||||
{{ option.name }}
|
</mat-option>
|
||||||
</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
|
||||||
<mat-label>{{ 'userLanguage' | translate }}</mat-label>
|
|
||||||
<mat-select formControlName="language">
|
|
||||||
@for (lang of availableLanguages; track lang.code) {
|
|
||||||
<mat-option [value]="lang.code">
|
|
||||||
{{ lang.label }}
|
|
||||||
</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
<mat-hint>{{ 'userLanguageHint' | translate }}</mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
|
||||||
<mat-label>{{ 'clientsPerPage' | translate }}</mat-label>
|
|
||||||
<input matInput formControlName="clientsPerPage" type="number" min="1" [placeholder]="'default20' | translate">
|
|
||||||
<mat-hint>{{ 'clientsPerPageHint' | translate }}</mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
</form>
|
</form>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
<mat-dialog-actions class="action-container">
|
<mat-dialog-actions class="action-container">
|
||||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||||
<button class="submit-button" (click)="onSubmit()" [disabled]="userForm.invalid">{{ isEditMode ? ('buttonEdit' | translate) : ('buttonAdd' | translate) }}</button>
|
<button class="submit-button" (click)="onSubmit()">{{ 'buttonAdd' | translate }}</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { ToastrService } from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
import { HttpClient } from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
import { DataService } from "../data.service";
|
import {DataService} from "../data.service";
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
import { DataService as GroupsDataService } from '../../../../groups/services/data.service';
|
|
||||||
|
|
||||||
interface UserGroup {
|
interface UserGroup {
|
||||||
'@id': string;
|
'@id': string;
|
||||||
|
|
@ -21,50 +17,34 @@ interface UserGroup {
|
||||||
styleUrls: ['./add-user-modal.component.css']
|
styleUrls: ['./add-user-modal.component.css']
|
||||||
})
|
})
|
||||||
export class AddUserModalComponent implements OnInit {
|
export class AddUserModalComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
@Output() userAdded = new EventEmitter<void>();
|
@Output() userAdded = new EventEmitter<void>();
|
||||||
@Output() userEdited = new EventEmitter<void>();
|
|
||||||
userForm: FormGroup<any>;
|
userForm: FormGroup<any>;
|
||||||
userGroups: UserGroup[] = [];
|
userGroups: UserGroup[] = [];
|
||||||
organizationalUnits: any[] = [];
|
organizationalUnits: any[] = [];
|
||||||
userId: string | null = null;
|
userId: string | null = null;
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
isEditMode: boolean = false;
|
|
||||||
|
|
||||||
protected views = [
|
protected views = [
|
||||||
{ value: 'card', name: 'Tarjetas' },
|
{value: 'card', name: 'Tarjetas'},
|
||||||
{ value: 'list', name: 'Listado' },
|
{value: 'list', name: 'Listado'},
|
||||||
];
|
];
|
||||||
|
|
||||||
readonly availableLanguages: { code: string; label: string }[];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public dialogRef: MatDialogRef<AddUserModalComponent>,
|
public dialogRef: MatDialogRef<AddUserModalComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private groupsDataService: GroupsDataService,
|
private toastService: ToastrService
|
||||||
private toastService: ToastrService,
|
|
||||||
private configService: ConfigService
|
|
||||||
) {
|
) {
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
const langs = this.configService.languages;
|
|
||||||
this.availableLanguages = Object.keys(langs).map(code => ({ code, label: langs[code] }));
|
|
||||||
this.userForm = this.fb.group({
|
this.userForm = this.fb.group({
|
||||||
username: ['', Validators.required],
|
username: ['', Validators.required],
|
||||||
password: ['', Validators.required],
|
password: ['', Validators.required],
|
||||||
role: ['', Validators.required],
|
role: ['', Validators.required],
|
||||||
groupsView: ['card', Validators.required],
|
groupsView: ['card', Validators.required],
|
||||||
clientsPerPage: [null, [Validators.min(1)]],
|
|
||||||
language: ['es'],
|
|
||||||
organizationalUnits: [[]]
|
organizationalUnits: [[]]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data) {
|
|
||||||
this.isEditMode = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
@ -73,13 +53,7 @@ export class AddUserModalComponent implements OnInit {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataService.getOrganizationalUnits().subscribe((data) => {
|
this.dataService.getOrganizationalUnits().subscribe((data) => {
|
||||||
const allUnits = data['hydra:member'];
|
this.organizationalUnits = data['hydra:member'].filter((item: any) => item.type === 'organizational-unit');
|
||||||
this.organizationalUnits = allUnits
|
|
||||||
.filter((item: any) => item.type === 'organizational-unit')
|
|
||||||
.map((unit: any) => ({
|
|
||||||
...unit,
|
|
||||||
path: this.groupsDataService.getOrganizationalUnitPath(unit, allUnits)
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
|
|
@ -99,9 +73,7 @@ export class AddUserModalComponent implements OnInit {
|
||||||
username: response.username,
|
username: response.username,
|
||||||
role: response.userGroups.length > 0 ? response.userGroups[0]['@id'] : null,
|
role: response.userGroups.length > 0 ? response.userGroups[0]['@id'] : null,
|
||||||
organizationalUnits: organizationalUnitIds,
|
organizationalUnits: organizationalUnitIds,
|
||||||
groupsView: response.groupsView,
|
groupsView: response.groupsView
|
||||||
clientsPerPage: response.clientsPerPage ?? null,
|
|
||||||
language: response.language ?? 'es'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userId = response['@id'];
|
this.userId = response['@id'];
|
||||||
|
|
@ -111,7 +83,7 @@ export class AddUserModalComponent implements OnInit {
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
console.error(this.translate.instant('fetchingUserError'), err);
|
console.error('Error fetching user:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -122,59 +94,42 @@ export class AddUserModalComponent implements OnInit {
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
if (this.userForm.valid) {
|
if (this.userForm.valid) {
|
||||||
const mappedOUs = (this.userForm.value.organizationalUnits || []).map((ou: any) => {
|
const payload = {
|
||||||
if (typeof ou === 'number' || (typeof ou === 'string' && !ou.includes('/'))) {
|
|
||||||
const selectedUnit = this.organizationalUnits.find((u: any) => u.id === Number(ou));
|
|
||||||
return selectedUnit ? (selectedUnit['@id'] || `/organizational-units/${selectedUnit.uuid}`) : ou;
|
|
||||||
}
|
|
||||||
return ou;
|
|
||||||
});
|
|
||||||
|
|
||||||
const payload: any = {
|
|
||||||
username: this.userForm.value.username,
|
username: this.userForm.value.username,
|
||||||
allowedOrganizationalUnits: mappedOUs,
|
allowedOrganizationalUnits: this.userForm.value.organizationalUnit,
|
||||||
|
password: this.userForm.value.password,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
userGroups: [this.userForm.value.role],
|
userGroups: [this.userForm.value.role ],
|
||||||
groupsView: this.userForm.value.groupsView,
|
groupsView: this.userForm.value.groupsView
|
||||||
clientsPerPage: this.userForm.value.clientsPerPage || null,
|
|
||||||
language: this.userForm.value.language || null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.userId && this.userForm.value.password) {
|
|
||||||
payload.password = this.userForm.value.password;
|
|
||||||
} else if (this.userId && this.userForm.value.password.trim() !== '') {
|
|
||||||
payload.password = this.userForm.value.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
if (this.userId) {
|
if (this.userId) {
|
||||||
this.http.put(`${this.baseUrl}${this.userId}`, payload).subscribe(
|
this.http.put(`${this.baseUrl}${this.userId}`, payload).subscribe(
|
||||||
(response) => {
|
(response) => {
|
||||||
this.toastService.success(this.translate.instant('userEditedSuccessfully'));
|
this.toastService.success('Usuario editado correctamente');
|
||||||
this.userEdited.emit();
|
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
this.loading = false;
|
this.loading = false
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.toastService.error(error['error']['hydra:description']);
|
this.toastService.error(error['error']['hydra:description']);
|
||||||
this.loading = false;
|
this.loading = false
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.http.post(`${this.baseUrl}/users`, payload).subscribe(
|
this.http.post(`${this.baseUrl}/users`, payload).subscribe(
|
||||||
(response) => {
|
(response) => {
|
||||||
this.toastService.success(this.translate.instant('userAddedSuccessfully'));
|
this.toastService.success('Usuario añadido correctamente');
|
||||||
this.userAdded.emit();
|
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
this.loading = false;
|
this.loading = false
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.toastService.error(error['error']['hydra:description']);
|
this.toastService.error(error['error']['hydra:description']);
|
||||||
this.loading = false;
|
this.loading = false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
.user-form .form-field {
|
.user-form .form-field {
|
||||||
display: block;
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-group label {
|
.checkbox-group label {
|
||||||
|
|
@ -22,8 +23,4 @@ mat-spinner {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
}
|
|
||||||
|
|
||||||
.form-container {
|
|
||||||
margin-top: 2em;
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<h1 mat-dialog-title>{{ 'changePassword' | translate }}</h1>
|
<h1 mat-dialog-title>{{ 'dialogTitleEditUser' | translate }}</h1>
|
||||||
<mat-dialog-content class="form-container">
|
<mat-dialog-content class="form-container">
|
||||||
<form [formGroup]="userForm" class="user-form">
|
<form [formGroup]="userForm" class="user-form">
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, EventEmitter, Inject, Output } from '@angular/core';
|
import { Component, EventEmitter, Inject, Output } from '@angular/core';
|
||||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
|
@ -12,7 +10,6 @@ import {ToastrService} from "ngx-toastr";
|
||||||
styleUrl: './change-password-modal.component.css'
|
styleUrl: './change-password-modal.component.css'
|
||||||
})
|
})
|
||||||
export class ChangePasswordModalComponent {
|
export class ChangePasswordModalComponent {
|
||||||
public translate = inject(TranslateService);
|
|
||||||
@Output() userEdited = new EventEmitter<void>();
|
@Output() userEdited = new EventEmitter<void>();
|
||||||
userForm: FormGroup;
|
userForm: FormGroup;
|
||||||
passwordMismatch: boolean = false;
|
passwordMismatch: boolean = false;
|
||||||
|
|
@ -58,10 +55,10 @@ export class ChangePasswordModalComponent {
|
||||||
next: () => {
|
next: () => {
|
||||||
this.userEdited.emit();
|
this.userEdited.emit();
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
this.toastService.success(this.translate.instant('passwordChangedSuccessfully'));
|
this.toastService.success('Contraseña cambiada con éxito');
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(this.translate.instant('errorChangingPassword'), err);
|
console.error('Error changing password:', err);
|
||||||
this.toastService.error(err.error['hydra:description']);
|
this.toastService.error(err.error['hydra:description']);
|
||||||
this.resetPasswordError = err.error.message;
|
this.resetPasswordError = err.error.message;
|
||||||
this.updateError = true;
|
this.updateError = true;
|
||||||
|
|
@ -69,7 +66,7 @@ export class ChangePasswordModalComponent {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error(this.translate.instant('formIsInvalid'));
|
console.error('Form is invalid');
|
||||||
this.passwordMismatch = this.userForm.hasError('mismatch');
|
this.passwordMismatch = this.userForm.hasError('mismatch');
|
||||||
this.updateError = true;
|
this.updateError = true;
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,17 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {HttpClient, HttpParams} from '@angular/common/http';
|
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||||
import { Observable, throwError } from 'rxjs';
|
import { Observable, throwError } from 'rxjs';
|
||||||
import { catchError, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DataService {
|
export class DataService {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
private apiUrl = `${this.baseUrl}/users?page=1&itemsPerPage=1000`;
|
||||||
private apiUrl: string;
|
|
||||||
|
|
||||||
constructor(private http: HttpClient, private configService: ConfigService) {
|
constructor(private http: HttpClient) {}
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.apiUrl = `${this.baseUrl}/users?page=1&itemsPerPage=1000`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUsers(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
getUsers(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
||||||
const params = new HttpParams({ fromObject: filters });
|
const params = new HttpParams({ fromObject: filters });
|
||||||
|
|
@ -35,7 +28,7 @@ export class DataService {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(this.translate.instant('fetchingUsersError'), error);
|
console.error('Error fetching users', error);
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -51,7 +44,7 @@ export class DataService {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(this.translate.instant('errorFetchingUserGroup'), error);
|
console.error('Error fetching user group', error);
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -67,7 +60,7 @@ export class DataService {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(this.translate.instant('errorFetchingUserGroups'), error);
|
console.error('Error fetching user groups', error);
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -83,7 +76,7 @@ export class DataService {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(this.translate.instant('errorFetchingOrganizationalUnits'), error);
|
console.error('Error fetching organizational units', error);
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<mat-form-field appearance="fill" class="search-string">
|
<mat-form-field appearance="fill" class="search-string">
|
||||||
<mat-label>{{ 'labelSearch' | translate }}</mat-label>
|
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
|
||||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']"
|
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
|
||||||
(keyup.enter)="search()">
|
(keyup.enter)="search()">
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||||
|
|
@ -24,21 +25,21 @@
|
||||||
<div *ngIf="!loading">
|
<div *ngIf="!loading">
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||||
<td mat-cell *matCellDef="let user">
|
<td mat-cell *matCellDef="let user">
|
||||||
<ng-container *ngIf="column.columnDef === 'groupsView'">
|
<ng-container *ngIf="column.columnDef === 'groupsView'">
|
||||||
<mat-chip>
|
<mat-chip>
|
||||||
{{ user[column.columnDef] === 'card' ? 'Vista tarjetas' : 'Listado' }}
|
{{ user[column.columnDef] === 'card' ? 'Vista tarjetas' : 'Listado' }}
|
||||||
</mat-chip>
|
</mat-chip>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="column.columnDef !== 'groupsView'">
|
<ng-container *ngIf="column.columnDef !== 'groupsView'" >
|
||||||
{{ column.cell(user) }}
|
{{ column.cell(user) }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||||
<td mat-cell *matCellDef="let user" style="text-align: center;">
|
<td mat-cell *matCellDef="let user" style="text-align: center;">
|
||||||
<button mat-icon-button color="primary" (click)="editUser(user)">
|
<button mat-icon-button color="primary" (click)="editUser(user)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
|
|
@ -57,4 +58,4 @@
|
||||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||||
(page)="onPageChange($event)">
|
(page)="onPageChange($event)">
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { ToastrService } from 'ngx-toastr';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
class MockToastrService {
|
class MockToastrService {
|
||||||
success() {}
|
success() {}
|
||||||
|
|
@ -19,11 +18,6 @@ describe('UsersComponent', () => {
|
||||||
let fixture: ComponentFixture<UsersComponent>;
|
let fixture: ComponentFixture<UsersComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const mockConfigService = {
|
|
||||||
apiUrl: 'http://mock-api-url',
|
|
||||||
mercureUrl: 'http://mock-mercure-url'
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [UsersComponent],
|
declarations: [UsersComponent],
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -34,7 +28,6 @@ describe('UsersComponent', () => {
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ToastrService, useClass: MockToastrService },
|
{ provide: ToastrService, useClass: MockToastrService },
|
||||||
{ provide: ConfigService, useValue: mockConfigService }
|
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA], // Ignorar elementos desconocidos
|
schemas: [NO_ERRORS_SCHEMA], // Ignorar elementos desconocidos
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
|
@ -7,7 +5,7 @@ import { AddUserModalComponent } from './add-user-modal/add-user-modal.component
|
||||||
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
|
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ConfigService } from '@services/config.service';
|
import { DataService } from "./data.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-users',
|
selector: 'app-users',
|
||||||
|
|
@ -15,9 +13,7 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrls: ['./users.component.css']
|
styleUrls: ['./users.component.css']
|
||||||
})
|
})
|
||||||
export class UsersComponent implements OnInit {
|
export class UsersComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
private apiUrl: string;
|
|
||||||
dataSource = new MatTableDataSource<any>();
|
dataSource = new MatTableDataSource<any>();
|
||||||
filters: { [key: string]: string } = {};
|
filters: { [key: string]: string } = {};
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
|
|
@ -28,46 +24,40 @@ export class UsersComponent implements OnInit {
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
columnDef: 'id',
|
columnDef: 'id',
|
||||||
header: 'labelId',
|
header: 'ID',
|
||||||
cell: (user: any) => `${user.id}`
|
cell: (user: any) => `${user.id}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'username',
|
columnDef: 'username',
|
||||||
header: 'userName',
|
header: 'Nombre de Usuario',
|
||||||
cell: (user: any) => `${user.username}`
|
cell: (user: any) => `${user.username}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'groupsView',
|
columnDef: 'groupsView',
|
||||||
header: 'groupView',
|
header: 'Vista de Grupos',
|
||||||
cell: (user: any) => `${user.groupsView}`
|
cell: (user: any) => `${user.groupsView}`
|
||||||
},
|
},
|
||||||
{
|
|
||||||
columnDef: 'clientsPerPage',
|
|
||||||
header: 'clientsPerPage',
|
|
||||||
cell: (user: any) => user.clientsPerPage != null ? `${user.clientsPerPage}` : '-'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
columnDef: 'allowedOrganizationalUnits',
|
columnDef: 'allowedOrganizationalUnits',
|
||||||
header: 'allowedOrganizationalUnits',
|
header: 'Unidades Organizacionales Permitidas',
|
||||||
cell: (user: any) => `${user.allowedOrganizationalUnits.map((unit: { name: any; }) => unit.name).join(', ')}`
|
cell: (user: any) => `${user.allowedOrganizationalUnits.map((unit: { name: any; }) => unit.name).join(', ')}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'roles',
|
columnDef: 'roles',
|
||||||
header: 'labelRoles',
|
header: 'Roles',
|
||||||
cell: (user: any) => `${user.roles.join(', ')}`
|
cell: (user: any) => `${user.roles.join(', ')}`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||||
|
|
||||||
|
private apiUrl = `${this.baseUrl}/users`;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private configService: ConfigService,
|
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
|
private dataService: DataService,
|
||||||
private toastService: ToastrService
|
private toastService: ToastrService
|
||||||
) {
|
) {}
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.apiUrl = `${this.baseUrl}/users`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.search();
|
this.search();
|
||||||
|
|
@ -82,7 +72,7 @@ export class UsersComponent implements OnInit {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error(this.translate.instant('fetchingUsersError'), error);
|
console.error('Error fetching users', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -102,10 +92,6 @@ export class UsersComponent implements OnInit {
|
||||||
data: user['@id']
|
data: user['@id']
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.componentInstance.userEdited.subscribe(() => {
|
|
||||||
this.search();
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
if (result) {
|
if (result) {
|
||||||
this.search();
|
this.search();
|
||||||
|
|
@ -126,11 +112,11 @@ export class UsersComponent implements OnInit {
|
||||||
this.http.delete(apiUrl).subscribe({
|
this.http.delete(apiUrl).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
console.log('User deleted successfully');
|
console.log('User deleted successfully');
|
||||||
this.toastService.success(this.translate.instant('userDeletedSuccessfully'));
|
this.toastService.success('User deleted successfully');
|
||||||
this.search();
|
this.search();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error(this.translate.instant('errorDeletingUser'), error);
|
console.error('Error deleting user:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -143,4 +129,4 @@ export class UsersComponent implements OnInit {
|
||||||
this.length = event.length;
|
this.length = event.length;
|
||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<mat-form-field joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}" appearance="fill"
|
<mat-form-field joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}" appearance="fill"
|
||||||
class="search-string">
|
class="search-string">
|
||||||
<mat-label>{{ 'labelSearchCalendar' | translate }}</mat-label>
|
<mat-label>{{ 'searchCalendarLabel' | translate }}</mat-label>
|
||||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']"
|
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
|
||||||
(keyup.enter)="search()">
|
(keyup.enter)="search()">
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||||
text="{{ 'tableStepText' | translate }}">
|
text="{{ 'tableStepText' | translate }}">
|
||||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||||
<td mat-cell *matCellDef="let image">
|
<td mat-cell *matCellDef="let image">
|
||||||
<ng-container *ngIf="column.columnDef === 'isDefault' || column.columnDef === 'installed'">
|
<ng-container *ngIf="column.columnDef === 'isDefault' || column.columnDef === 'installed'">
|
||||||
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
<ng-container matColumnDef="actions" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||||
<td mat-cell *matCellDef="let calendar" style="text-align: center;">
|
<td mat-cell *matCellDef="let calendar" style="text-align: center;">
|
||||||
<button mat-icon-button color="primary" (click)="editCalendar(calendar)">
|
<button mat-icon-button color="primary" (click)="editCalendar(calendar)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,12 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||||
import { JoyrideModule, JoyrideService } from 'ngx-joyride';
|
import { JoyrideModule, JoyrideService } from 'ngx-joyride';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { LoadingComponent } from '../../shared/loading/loading.component';
|
import { LoadingComponent } from '../../shared/loading/loading.component';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
describe('CalendarComponent', () => {
|
describe('CalendarComponent', () => {
|
||||||
let component: CalendarComponent;
|
let component: CalendarComponent;
|
||||||
let fixture: ComponentFixture<CalendarComponent>;
|
let fixture: ComponentFixture<CalendarComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const mockConfigService = {
|
|
||||||
apiUrl: 'http://mock-api-url',
|
|
||||||
mercureUrl: 'http://mock-mercure-url'
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [CalendarComponent, LoadingComponent],
|
declarations: [CalendarComponent, LoadingComponent],
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -47,9 +41,6 @@ describe('CalendarComponent', () => {
|
||||||
JoyrideModule.forRoot(),
|
JoyrideModule.forRoot(),
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
],
|
],
|
||||||
providers: [
|
|
||||||
{ provide: ConfigService, useValue: mockConfigService }
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, OnInit, signal } from '@angular/core';
|
import { Component, OnInit, signal } from '@angular/core';
|
||||||
import { MatTableDataSource } from "@angular/material/table";
|
import { MatTableDataSource } from "@angular/material/table";
|
||||||
import { DatePipe } from "@angular/common";
|
import { DatePipe } from "@angular/common";
|
||||||
|
|
@ -11,7 +9,6 @@ import { PageEvent } from "@angular/material/paginator";
|
||||||
import { CreateCalendarComponent } from "./create-calendar/create-calendar.component";
|
import { CreateCalendarComponent } from "./create-calendar/create-calendar.component";
|
||||||
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
|
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||||
import { JoyrideService } from 'ngx-joyride';
|
import { JoyrideService } from 'ngx-joyride';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-calendar',
|
selector: 'app-calendar',
|
||||||
|
|
@ -19,9 +16,7 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrl: './calendar.component.css'
|
styleUrl: './calendar.component.css'
|
||||||
})
|
})
|
||||||
export class CalendarComponent implements OnInit {
|
export class CalendarComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
private apiUrl: string;
|
|
||||||
images: { downloadUrl: string; name: string; uuid: string }[] = [];
|
images: { downloadUrl: string; name: string; uuid: string }[] = [];
|
||||||
dataSource = new MatTableDataSource<any>();
|
dataSource = new MatTableDataSource<any>();
|
||||||
length: number = 0;
|
length: number = 0;
|
||||||
|
|
@ -37,38 +32,35 @@ export class CalendarComponent implements OnInit {
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
columnDef: 'id',
|
columnDef: 'id',
|
||||||
header: 'labelId',
|
header: 'ID',
|
||||||
cell: (remoteCalendar: any) => `${remoteCalendar.id}`,
|
cell: (remoteCalendar: any) => `${remoteCalendar.id}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'name',
|
columnDef: 'name',
|
||||||
header: 'labelName',
|
header: 'Nombre',
|
||||||
cell: (remoteCalendar: any) => `${remoteCalendar.name}`
|
cell: (remoteCalendar: any) => `${remoteCalendar.name}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'rulesLength',
|
columnDef: 'rulesLength',
|
||||||
header: 'numberOfRules',
|
header: 'Nº de reglas',
|
||||||
cell: (remoteCalendar: any) => `${remoteCalendar.remoteCalendarRules.length}`
|
cell: (remoteCalendar: any) => `${remoteCalendar.remoteCalendarRules.length}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'createdAt',
|
columnDef: 'createdAt',
|
||||||
header: 'creationDate',
|
header: 'Fecha de creación',
|
||||||
cell: (remoteCalendar: any) => `${this.datePipe.transform(remoteCalendar.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
cell: (remoteCalendar: any) => `${this.datePipe.transform(remoteCalendar.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||||
|
private apiUrl = `${this.baseUrl}/remote-calendars`;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService,
|
||||||
private configService: ConfigService,
|
|
||||||
private joyrideService: JoyrideService
|
private joyrideService: JoyrideService
|
||||||
) {
|
) {}
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.apiUrl = `${this.baseUrl}/remote-calendars`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.search();
|
this.search();
|
||||||
|
|
@ -92,7 +84,7 @@ export class CalendarComponent implements OnInit {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
console.error(this.translate.instant('errorFetchingCalendars'), error);
|
console.error('Error fetching calendars', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -102,7 +94,7 @@ export class CalendarComponent implements OnInit {
|
||||||
this.syncUds = true;
|
this.syncUds = true;
|
||||||
this.http.post(`${this.apiUrl}/${calendar.uuid}/sync-uds`, {}).subscribe({
|
this.http.post(`${this.apiUrl}/${calendar.uuid}/sync-uds`, {}).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.success(this.translate.instant('calendarsSynchronizedCorrectly'));
|
this.toastService.success('Calendarios sincronizados correctamente');
|
||||||
this.search();
|
this.search();
|
||||||
this.syncUds = false;
|
this.syncUds = false;
|
||||||
},
|
},
|
||||||
|
|
@ -139,10 +131,10 @@ export class CalendarComponent implements OnInit {
|
||||||
this.http.delete(apiUrl).subscribe({
|
this.http.delete(apiUrl).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.search();
|
this.search();
|
||||||
this.toastService.success(this.translate.instant('calendarDeletedSuccessfully'));
|
this.toastService.success('Calendar deleted successfully');
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.toastService.error(this.translate.instant('errorDeletingCalendar'));
|
this.toastService.error('Error deleting calendar');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +165,7 @@ export class CalendarComponent implements OnInit {
|
||||||
this.joyrideService.startTour({
|
this.joyrideService.startTour({
|
||||||
steps: ['titleStep', 'addButtonStep', 'searchStep', 'tableStep', 'actionsStep'],
|
steps: ['titleStep', 'addButtonStep', 'searchStep', 'tableStep', 'actionsStep'],
|
||||||
showPrevButton: true,
|
showPrevButton: true,
|
||||||
themeColor: '#3f51b5'
|
themeColor: '#3f51b5'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,7 @@
|
||||||
|
|
||||||
.time-fields {
|
.time-fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px; /* Espacio entre los campos */
|
||||||
}
|
|
||||||
|
|
||||||
.hour-fields {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-field {
|
.time-field {
|
||||||
|
|
@ -39,74 +34,4 @@
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
padding: 1.5em;
|
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>
|
<h2 mat-dialog-title>{{ isEditMode ? ('editCalendar' | translate) : ('addCalendar' | translate) }}</h2>
|
||||||
<mat-dialog-content class="form-container">
|
<mat-dialog-content class="form-container">
|
||||||
<mat-checkbox [(ngModel)]="isRemoteAvailable">
|
<mat-slide-toggle [(ngModel)]="isRemoteAvailable" class="example-margin">
|
||||||
{{ 'remoteAvailability' | translate }}
|
{{ 'remoteAvailability' | translate }}
|
||||||
</mat-checkbox>
|
</mat-slide-toggle>
|
||||||
|
|
||||||
<mat-divider style="margin: 10px 0;"></mat-divider>
|
|
||||||
|
|
||||||
<div *ngIf="!isRemoteAvailable" class="form-group">
|
<div *ngIf="!isRemoteAvailable" class="form-group">
|
||||||
<mat-label>{{ 'selectWeekDays' | translate }}</mat-label>
|
<mat-label>{{ 'selectWeekDays' | translate }}</mat-label>
|
||||||
<p class="custom-text"> {{ 'remotePcCalendarRuleInfo' | translate }}</p>
|
<div class="row">
|
||||||
<div class="weekday-toggle-group full-width">
|
<div class="col-md-6 checkbox-group">
|
||||||
<button
|
<mat-checkbox *ngFor="let day of weekDays.slice(0, (weekDays.length / 2) + 1)" [(ngModel)]="busyWeekDays[day]">
|
||||||
*ngFor="let day of weekDays"
|
{{ day }}
|
||||||
type="button"
|
</mat-checkbox>
|
||||||
class="weekday-toggle"
|
</div>
|
||||||
[class.selected]="busyWeekDays[day]"
|
<div class="col-md-6 checkbox-group">
|
||||||
(click)="busyWeekDays[day] = !busyWeekDays[day]">
|
<mat-checkbox *ngFor="let day of weekDays.slice(weekDays.length / 2 + 1)" [(ngModel)]="busyWeekDays[day]">
|
||||||
{{ day.slice(0, 3) }}
|
{{ day }}
|
||||||
</button>
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="time-fields">
|
<div class="time-fields">
|
||||||
<mat-form-field appearance="fill" class="time-field">
|
<mat-form-field appearance="fill" class="time-field">
|
||||||
<mat-label>{{ 'startTime' | translate }}</mat-label>
|
<mat-label>{{ 'startTime' | translate }}</mat-label>
|
||||||
|
|
@ -32,63 +30,29 @@
|
||||||
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</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">
|
|
||||||
{{ 'classroomWillBe' | translate }}<strong>{{ 'notAvailableLowercase' | translate }}</strong> {{ 'forRemotePcOnDays' | translate }}<strong>{{ getSelectedDays().join(', ') }}</strong> {{ 'labelOf' | translate }}<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isRemoteAvailable" class="form-group">
|
<div *ngIf="isRemoteAvailable" class="form-group">
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'labelReason' | translate }}</mat-label>
|
<mat-label>{{ 'reasonLabel' | translate }}</mat-label>
|
||||||
<input matInput [(ngModel)]="availableReason" placeholder="{{ 'reasonPlaceholder' | translate }}" [required]="isRemoteAvailable">
|
<input matInput [(ngModel)]="availableReason" placeholder="{{ 'reasonPlaceholder' | translate }}" [required]="isRemoteAvailable">
|
||||||
<mat-hint>{{ 'reasonAvailableRemotePc' | translate }}</mat-hint>
|
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="time-fields">
|
<div class="time-fields">
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'startDate' | translate }}</mat-label>
|
<mat-label>{{ 'startDate' | translate }}</mat-label>
|
||||||
<input matInput [matDatepicker]="picker1" [(ngModel)]="availableFromDate" [required]="isRemoteAvailable">
|
<input matInput [matDatepicker]="picker1" [(ngModel)]="availableFromDate" [required]="isRemoteAvailable">
|
||||||
<mat-hint>{{ 'dateFormatPlaceholder' | translate }}</mat-hint>
|
<mat-hint>MM/DD/YYYY</mat-hint>
|
||||||
<mat-datepicker-toggle matIconSuffix [for]="picker1"></mat-datepicker-toggle>
|
<mat-datepicker-toggle matIconSuffix [for]="picker1"></mat-datepicker-toggle>
|
||||||
<mat-datepicker #picker1></mat-datepicker>
|
<mat-datepicker #picker1></mat-datepicker>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'endDate' | translate }}</mat-label>
|
<mat-label>{{ 'endDate' | translate }}</mat-label>
|
||||||
<input matInput [matDatepicker]="picker2" [(ngModel)]="availableToDate" [required]="isRemoteAvailable">
|
<input matInput [matDatepicker]="picker2" [(ngModel)]="availableToDate" [required]="isRemoteAvailable">
|
||||||
<mat-hint>{{ 'dateFormatPlaceholder' | translate }}</mat-hint>
|
<mat-hint>MM/DD/YYYY</mat-hint>
|
||||||
<mat-datepicker-toggle matIconSuffix [for]="picker2"></mat-datepicker-toggle>
|
<mat-datepicker-toggle matIconSuffix [for]="picker2"></mat-datepicker-toggle>
|
||||||
<mat-datepicker #picker2></mat-datepicker>
|
<mat-datepicker #picker2></mat-datepicker>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</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">
|
|
||||||
{{ 'classroomWillBe' | translate }}<strong>{{ 'statusAvailableLowercase' | translate }}</strong> {{ 'forReservationFrom' | translate }}<strong>{{ availableFromDate | date:'fullDate' }}</strong> {{ 'untilThe' | translate }}<strong>{{ availableToDate | date:'fullDate' }}</strong>
|
|
||||||
<span *ngIf="busyFromHour && busyToHour">
|
|
||||||
{{ 'inScheduleOf' | translate }}<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
import { inject } from '@angular/core';
|
import {Component, Inject} from '@angular/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import {ToastrService} from "ngx-toastr";
|
||||||
import { Component, Inject } from '@angular/core';
|
import {HttpClient} from "@angular/common/http";
|
||||||
import { ToastrService } from "ngx-toastr";
|
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||||
import { HttpClient } from "@angular/common/http";
|
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-calendar-rule',
|
selector: 'app-create-calendar-rule',
|
||||||
|
|
@ -12,8 +9,7 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrl: './create-calendar-rule.component.css'
|
styleUrl: './create-calendar-rule.component.css'
|
||||||
})
|
})
|
||||||
export class CreateCalendarRuleComponent {
|
export class CreateCalendarRuleComponent {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
name: string = '';
|
name: string = '';
|
||||||
remoteCalendarRules: any[] = [];
|
remoteCalendarRules: any[] = [];
|
||||||
weekDays: string[] = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
|
weekDays: string[] = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
|
||||||
|
|
@ -33,23 +29,20 @@ export class CreateCalendarRuleComponent {
|
||||||
constructor(
|
constructor(
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private configService: ConfigService,
|
|
||||||
public dialogRef: MatDialogRef<CreateCalendarRuleComponent>,
|
public dialogRef: MatDialogRef<CreateCalendarRuleComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
) {
|
) { }
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.calendarId = this.data.calendar
|
this.calendarId = this.data.calendar
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.isEditMode = true;
|
this.isEditMode = true;
|
||||||
this.availableFromDate = this.data.rule ? this.data.rule.availableFromDate : null;
|
this.availableFromDate = this.data.rule? this.data.rule.availableFromDate : null;
|
||||||
this.availableToDate = this.data.rule ? this.data.rule.availableToDate : null;
|
this.availableToDate = this.data.rule? this.data.rule.availableToDate : null;
|
||||||
this.isRemoteAvailable = this.data.rule ? this.data.rule.isRemoteAvailable : false;
|
this.isRemoteAvailable = this.data.rule? this.data.rule.isRemoteAvailable : false;
|
||||||
this.availableReason = this.data.rule ? this.data.rule.availableReason : null;
|
this.availableReason = this.data.rule? this.data.rule.availableReason : null;
|
||||||
this.busyFromHour = this.data.rule ? this.data.rule.busyFromHour : null;
|
this.busyFromHour = this.data.rule? this.data.rule.busyFromHour : null;
|
||||||
this.busyToHour = this.data.rule ? this.data.rule.busyToHour : null;
|
this.busyToHour = this.data.rule? this.data.rule.busyToHour : null;
|
||||||
if (this.data.rule && this.data.rule.busyWeekDays) {
|
if (this.data.rule && this.data.rule.busyWeekDays) {
|
||||||
this.busyWeekDays = this.data.rule.busyWeekDays.reduce((acc: {
|
this.busyWeekDays = this.data.rule.busyWeekDays.reduce((acc: {
|
||||||
[x: string]: boolean;
|
[x: string]: boolean;
|
||||||
|
|
@ -67,8 +60,8 @@ export class CreateCalendarRuleComponent {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedDays(): string[] {
|
toggleAdditionalForm(): void {
|
||||||
return Object.keys(this.busyWeekDays || {}).filter(day => this.busyWeekDays[day]);
|
this.showAdditionalForm = !this.showAdditionalForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedDaysIndices() {
|
getSelectedDaysIndices() {
|
||||||
|
|
@ -77,11 +70,6 @@ export class CreateCalendarRuleComponent {
|
||||||
.filter(index => index !== -1);
|
.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 {
|
submitRule(): void {
|
||||||
this.getSelectedDaysIndices()
|
this.getSelectedDaysIndices()
|
||||||
const selectedDaysArray = Object.keys(this.busyWeekDays).map((day, index) => this.busyWeekDays[index]);
|
const selectedDaysArray = Object.keys(this.busyWeekDays).map((day, index) => this.busyWeekDays[index]);
|
||||||
|
|
@ -91,8 +79,8 @@ export class CreateCalendarRuleComponent {
|
||||||
busyWeekDays: this.selectedDaysIndices,
|
busyWeekDays: this.selectedDaysIndices,
|
||||||
busyFromHour: this.busyFromHour,
|
busyFromHour: this.busyFromHour,
|
||||||
busyToHour: this.busyToHour,
|
busyToHour: this.busyToHour,
|
||||||
availableFromDate: this.availableFromDate ? this.convertDateToLocalISO(this.availableFromDate) : null,
|
availableFromDate: this.availableFromDate,
|
||||||
availableToDate: this.availableToDate ? this.convertDateToLocalISO(this.availableToDate) : null,
|
availableToDate: this.availableToDate,
|
||||||
isRemoteAvailable: this.isRemoteAvailable,
|
isRemoteAvailable: this.isRemoteAvailable,
|
||||||
availableReason: this.availableReason
|
availableReason: this.availableReason
|
||||||
};
|
};
|
||||||
|
|
@ -101,7 +89,7 @@ export class CreateCalendarRuleComponent {
|
||||||
this.http.put(`${this.baseUrl}${this.ruleId}`, formData)
|
this.http.put(`${this.baseUrl}${this.ruleId}`, formData)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.toastService.success(this.translate.instant('calendarRuleUpdatedSuccessfully'));
|
this.toastService.success('Calendar updated successfully');
|
||||||
this.dialogRef.close(true);
|
this.dialogRef.close(true);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
|
|
@ -113,7 +101,7 @@ export class CreateCalendarRuleComponent {
|
||||||
this.http.post(`${this.baseUrl}/remote-calendar-rules`, formData)
|
this.http.post(`${this.baseUrl}/remote-calendar-rules`, formData)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.toastService.success(this.translate.instant('calendarRuleCreatedSuccessfully'));
|
this.toastService.success('Calendar created successfully');
|
||||||
this.dialogRef.close(true);
|
this.dialogRef.close(true);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
.time-fields {
|
.time-fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px; /* Espacio entre los campos */
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-field {
|
.time-field {
|
||||||
|
|
@ -34,25 +34,24 @@
|
||||||
|
|
||||||
.list-item-content {
|
.list-item-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start; /* Alinea el contenido al inicio */
|
||||||
justify-content: space-between;
|
justify-content: space-between; /* Espacio entre los textos y los íconos */
|
||||||
width: 100%;
|
width: 100%; /* Asegúrate de que el contenido ocupe todo el ancho */
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-content {
|
.text-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1; /* Permite que este contenedor ocupe el espacio disponible */
|
||||||
margin-right: 16px;
|
margin-right: 16px; /* Espaciado a la derecha para separar de los íconos */
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center; /* Alinea los íconos verticalmente */
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-icon {
|
.right-icon {
|
||||||
margin-left: 8px;
|
margin-left: 8px; /* Espaciado entre los íconos */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,15 +60,4 @@
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-available {
|
|
||||||
background-color: #e8f5e9;
|
|
||||||
border-left: 4px solid #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-unavailable {
|
|
||||||
background-color: #ffebee;
|
|
||||||
border-left: 4px solid #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
<mat-dialog-content class="form-container">
|
<mat-dialog-content class="form-container">
|
||||||
<!-- Campo para el nombre -->
|
<!-- Campo para el nombre -->
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
|
<mat-label>{{ 'labelName' | translate }}</mat-label>
|
||||||
<input matInput [(ngModel)]="name" required>
|
<input matInput [(ngModel)]="name" required>
|
||||||
<mat-icon *ngIf="isEditMode" matSuffix (click)="submitForm()">mode_edit</mat-icon>
|
<mat-icon *ngIf="isEditMode" matSuffix (click)="submitForm()">mode_edit</mat-icon>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<div *ngIf="isEditMode" mat-subheader>{{ 'rulesHeader' | translate }}</div>
|
<div *ngIf="isEditMode" mat-subheader>{{ 'rulesHeader' | translate }}</div>
|
||||||
<button class="action-button" *ngIf="isEditMode" (click)="createRule()" style="padding: 10px;">
|
<button class="action-button" *ngIf="isEditMode" (click)="createRule()" style="padding: 10px;">
|
||||||
|
|
@ -18,20 +18,14 @@
|
||||||
|
|
||||||
<mat-list *ngIf="isEditMode">
|
<mat-list *ngIf="isEditMode">
|
||||||
<ng-container *ngFor="let rule of remoteCalendarRules;">
|
<ng-container *ngFor="let rule of remoteCalendarRules;">
|
||||||
<mat-list-item
|
<mat-list-item>
|
||||||
[ngClass]="{
|
|
||||||
'rule-available': rule.isRemoteAvailable,
|
|
||||||
'rule-unavailable': !rule.isRemoteAvailable
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="list-item-content">
|
<div class="list-item-content">
|
||||||
<mat-icon matListItemIcon>event_available</mat-icon>
|
<mat-icon matListItemIcon>event_available</mat-icon>
|
||||||
<div class="text-content">
|
<div class="text-content">
|
||||||
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('remotePcStatusAvailable' | translate) : ('remotePcStatusUnavailable' | translate) }}</div>
|
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('statusAvailable' | translate) : ('statusUnavailable' | translate) }}</div>
|
||||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ 'labelDays' | translate }}<strong>{{ rule.busyWeekDaysMap }}</strong></div>
|
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
|
||||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">Razón: {{ rule.availableReason }}</div>
|
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyWeekDaysMap }}</div>
|
||||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">{{ 'labelDays' | translate }}<strong>{{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</strong></div>
|
<div matListItemLine *ngIf="rule.isRemoteAvailable">{{ rule.availableReason }} | {{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</div>
|
||||||
<div matListItemLine>Horario: {{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<button mat-icon-button color="primary" class="right-icon" (click)="createRule(rule)">
|
<button mat-icon-button color="primary" class="right-icon" (click)="createRule(rule)">
|
||||||
|
|
@ -50,6 +44,6 @@
|
||||||
<mat-dialog-actions class="action-container">
|
<mat-dialog-actions class="action-container">
|
||||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||||
<button class="submit-button" (click)="submitForm()" [disabled]="!name || name === ''" cdkFocusInitial>
|
<button class="submit-button" (click)="submitForm()" [disabled]="!name || name === ''" cdkFocusInitial>
|
||||||
{{ 'labelSaveButton' | translate }}
|
{{ 'buttonSave' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { inject } from '@angular/core';
|
import {Component, Inject, OnInit} from '@angular/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import {ToastrService} from "ngx-toastr";
|
||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import {HttpClient} from "@angular/common/http";
|
||||||
import { ToastrService } from "ngx-toastr";
|
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||||
import { HttpClient } from "@angular/common/http";
|
import {CreateCalendarRuleComponent} from "../create-calendar-rule/create-calendar-rule.component";
|
||||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dialog";
|
import {DataService} from "../data.service";
|
||||||
import { CreateCalendarRuleComponent } from "../create-calendar-rule/create-calendar-rule.component";
|
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||||
import { DataService } from "../data.service";
|
|
||||||
import { DeleteModalComponent } from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-calendar',
|
selector: 'app-create-calendar',
|
||||||
|
|
@ -15,8 +12,7 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrl: './create-calendar.component.css'
|
styleUrl: './create-calendar.component.css'
|
||||||
})
|
})
|
||||||
export class CreateCalendarComponent implements OnInit {
|
export class CreateCalendarComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
name: string = '';
|
name: string = '';
|
||||||
remoteCalendarRules: any[] = [];
|
remoteCalendarRules: any[] = [];
|
||||||
isEditMode: boolean = false;
|
isEditMode: boolean = false;
|
||||||
|
|
@ -26,14 +22,11 @@ export class CreateCalendarComponent implements OnInit {
|
||||||
constructor(
|
constructor(
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private configService: ConfigService,
|
|
||||||
public dialogRef: MatDialogRef<CreateCalendarComponent>,
|
public dialogRef: MatDialogRef<CreateCalendarComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
) {
|
) { }
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
|
|
@ -53,7 +46,7 @@ export class CreateCalendarComponent implements OnInit {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error(this.translate.instant('errorFetchingRemoteCalendar'), err);
|
console.error('Error fetching remote calendar:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +71,7 @@ export class CreateCalendarComponent implements OnInit {
|
||||||
this.http.patch(`${this.baseUrl}${this.calendarId}`, payload)
|
this.http.patch(`${this.baseUrl}${this.calendarId}`, payload)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.toastService.success(this.translate.instant('calendarUpdatedSuccessfully'));
|
this.toastService.success('Calendar updated successfully');
|
||||||
this.dialogRef.close(true);
|
this.dialogRef.close(true);
|
||||||
this.load()
|
this.load()
|
||||||
},
|
},
|
||||||
|
|
@ -91,7 +84,7 @@ export class CreateCalendarComponent implements OnInit {
|
||||||
this.http.post(`${this.baseUrl}/remote-calendars`, payload)
|
this.http.post(`${this.baseUrl}/remote-calendars`, payload)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.toastService.success(this.translate.instant('calendarCreatedSuccessfully'));
|
this.toastService.success('Calendar created successfully');
|
||||||
this.dialogRef.close(true);
|
this.dialogRef.close(true);
|
||||||
this.load()
|
this.load()
|
||||||
},
|
},
|
||||||
|
|
@ -132,10 +125,10 @@ export class CreateCalendarComponent implements OnInit {
|
||||||
next: () => {
|
next: () => {
|
||||||
console.log('Calendar deleted successfully');
|
console.log('Calendar deleted successfully');
|
||||||
this.load();
|
this.load();
|
||||||
this.toastService.success(this.translate.instant('calendarDeletedSuccessfully'));
|
this.toastService.success('Calendar deleted successfully');
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.toastService.error(this.translate.instant('errorDeletingCalendar'));
|
this.toastService.error('Error deleting calendar');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,16 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||||
import { Observable, throwError } from 'rxjs';
|
import { Observable, throwError } from 'rxjs';
|
||||||
import { catchError, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DataService {
|
export class DataService {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
private apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=1000`;
|
||||||
private apiUrl: string;
|
|
||||||
|
|
||||||
constructor(private http: HttpClient, private configService: ConfigService) {
|
constructor(private http: HttpClient) {}
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=1000`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRemoteCalendars(filters: { [key: string]: string }): Observable<any[]> {
|
getRemoteCalendars(filters: { [key: string]: string }): Observable<any[]> {
|
||||||
const params = new HttpParams({ fromObject: filters });
|
const params = new HttpParams({ fromObject: filters });
|
||||||
|
|
@ -31,7 +24,7 @@ export class DataService {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(this.translate.instant('errorFetchingRemoteCalendars'), error);
|
console.error('Error fetching remote calendars', error);
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -47,7 +40,7 @@ export class DataService {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error(this.translate.instant('fetchingCalendarError'), error);
|
console.error('Error fetching calendar', error);
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
|
|
||||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||||
<mat-form-field appearance="fill" class="search-string">
|
<mat-form-field appearance="fill" class="search-string">
|
||||||
<mat-label>{{ 'labelSearchGroupName' | translate }}</mat-label>
|
<mat-label>{{ 'searchGroupNameLabel' | translate }}</mat-label>
|
||||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
@ -28,16 +28,25 @@
|
||||||
<div *ngIf="!loading">
|
<div *ngIf="!loading">
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||||
<td mat-cell *matCellDef="let commandGroup">
|
<td mat-cell *matCellDef="let commandGroup">
|
||||||
<ng-container *ngIf="column.columnDef !== 'commands'">
|
<ng-container *ngIf="column.columnDef !== 'commands'">
|
||||||
{{ column.cell(commandGroup) }}
|
{{ column.cell(commandGroup) }}
|
||||||
</ng-container>
|
</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>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||||
<td mat-cell *matCellDef="let client" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
<td mat-cell *matCellDef="let client" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||||
<button mat-icon-button color="info" (click)="viewGroupDetails(client)">
|
<button mat-icon-button color="info" (click)="viewGroupDetails(client)">
|
||||||
<mat-icon>visibility</mat-icon>
|
<mat-icon>visibility</mat-icon>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
|
@ -10,7 +8,6 @@ import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/
|
||||||
import { MatTableDataSource } from "@angular/material/table";
|
import { MatTableDataSource } from "@angular/material/table";
|
||||||
import { DatePipe } from "@angular/common";
|
import { DatePipe } from "@angular/common";
|
||||||
import { JoyrideService } from 'ngx-joyride';
|
import { JoyrideService } from 'ngx-joyride';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-commands-groups',
|
selector: 'app-commands-groups',
|
||||||
|
|
@ -18,9 +15,7 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrls: ['./commands-groups.component.css']
|
styleUrls: ['./commands-groups.component.css']
|
||||||
})
|
})
|
||||||
export class CommandsGroupsComponent implements OnInit {
|
export class CommandsGroupsComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
private apiUrl: string;
|
|
||||||
dataSource = new MatTableDataSource<any>();
|
dataSource = new MatTableDataSource<any>();
|
||||||
filters: { [key: string]: string | boolean } = {};
|
filters: { [key: string]: string | boolean } = {};
|
||||||
length: number = 0;
|
length: number = 0;
|
||||||
|
|
@ -32,32 +27,30 @@ export class CommandsGroupsComponent implements OnInit {
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
columnDef: 'id',
|
columnDef: 'id',
|
||||||
header: 'labelId',
|
header: 'ID',
|
||||||
cell: (group: any) => `${group.id}`,
|
cell: (group: any) => `${group.id}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'name',
|
columnDef: 'name',
|
||||||
header: 'labelName',
|
header: 'Nombre',
|
||||||
cell: (group: any) => `${group.name}`
|
cell: (group: any) => `${group.name}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'commands',
|
columnDef: 'commands',
|
||||||
header: 'commandList',
|
header: 'Lista de comandos',
|
||||||
cell: (group: any) => `${group.commands}`
|
cell: (group: any) => `${group.commands}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'createdAt',
|
columnDef: 'createdAt',
|
||||||
header: 'creationDate',
|
header: 'Fecha de creación',
|
||||||
cell: (group: any) => `${this.datePipe.transform(group.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
cell: (group: any) => `${this.datePipe.transform(group.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||||
|
private apiUrl = `${this.baseUrl}/command-groups`;
|
||||||
|
|
||||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||||
private configService: ConfigService, private joyrideService: JoyrideService) {
|
private joyrideService: JoyrideService) {}
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.apiUrl = `${this.baseUrl}/command-groups`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.search();
|
this.search();
|
||||||
|
|
@ -72,7 +65,7 @@ export class CommandsGroupsComponent implements OnInit {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error(this.translate.instant('fetchingCommandGroupsError'), error);
|
console.error('Error fetching command groups', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -106,11 +99,11 @@ export class CommandsGroupsComponent implements OnInit {
|
||||||
if (result) {
|
if (result) {
|
||||||
this.http.delete(`${this.apiUrl}/${group.uuid}`).subscribe({
|
this.http.delete(`${this.apiUrl}/${group.uuid}`).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.success(this.translate.instant('commandGroupRemovedWith'));
|
this.toastService.success('Grupo de comandos eliminado con éxito');
|
||||||
this.search();
|
this.search();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error(this.translate.instant('errorDeletingGroup'), error);
|
console.error('Error al eliminar el grupo de comandos:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -127,17 +120,17 @@ export class CommandsGroupsComponent implements OnInit {
|
||||||
iniciarTour(): void {
|
iniciarTour(): void {
|
||||||
this.joyrideService.startTour({
|
this.joyrideService.startTour({
|
||||||
steps: [
|
steps: [
|
||||||
'titleStep',
|
'titleStep',
|
||||||
'addCommandGroupStep',
|
'addCommandGroupStep',
|
||||||
'searchStep',
|
'searchStep',
|
||||||
'tableStep',
|
'tableStep',
|
||||||
'viewCommandsStep',
|
'viewCommandsStep',
|
||||||
'actionsStep',
|
'actionsStep',
|
||||||
'paginationStep'
|
'paginationStep'
|
||||||
],
|
],
|
||||||
showPrevButton: true,
|
showPrevButton: true,
|
||||||
themeColor: '#3f51b5'
|
themeColor: '#3f51b5'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@
|
||||||
|
|
||||||
<form *ngIf="!loading" class="command-group-form" (ngSubmit)="onSubmit()">
|
<form *ngIf="!loading" class="command-group-form" (ngSubmit)="onSubmit()">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>{{ 'labelGroupName' | translate }}</mat-label>
|
<mat-label>{{ 'groupNameLabel' | translate }}</mat-label>
|
||||||
<input matInput [(ngModel)]="groupName" name="groupName" required />
|
<input matInput [(ngModel)]="groupName" name="groupName" required />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-slide-toggle [(ngModel)]="enabled" name="enabled">{{ 'labelEnabled' | translate }}</mat-slide-toggle>
|
<mat-slide-toggle [(ngModel)]="enabled" name="enabled">{{ 'enabledToggle' | translate }}</mat-slide-toggle>
|
||||||
|
|
||||||
<div class="command-selection">
|
<div class="command-selection">
|
||||||
<div class="available-commands">
|
<div class="available-commands">
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table mat-table [dataSource]="availableCommands" class="mat-elevation-z8">
|
<table mat-table [dataSource]="availableCommands" class="mat-elevation-z8">
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ 'labelRoleName' | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef> {{ 'nameColumn' | translate }} </th>
|
||||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ 'actions' | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef> {{ 'actionsColumn' | translate }} </th>
|
||||||
<td mat-cell *matCellDef="let command">
|
<td mat-cell *matCellDef="let command">
|
||||||
<button mat-icon-button type="button" (click)="addCommand(command)">
|
<button mat-icon-button type="button" (click)="addCommand(command)">
|
||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
|
|
@ -55,5 +55,5 @@
|
||||||
|
|
||||||
<mat-dialog-actions class="action-container">
|
<mat-dialog-actions class="action-container">
|
||||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||||
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>{{ 'labelSaveButton' | translate }}</button>
|
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, OnInit, Inject } from '@angular/core';
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-command-group',
|
selector: 'app-create-command-group',
|
||||||
|
|
@ -13,26 +10,21 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrls: ['./create-command-group.component.css']
|
styleUrls: ['./create-command-group.component.css']
|
||||||
})
|
})
|
||||||
export class CreateCommandGroupComponent implements OnInit {
|
export class CreateCommandGroupComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
availableCommands: any[] = [];
|
availableCommands: any[] = [];
|
||||||
selectedCommands: any[] = [];
|
selectedCommands: any[] = [];
|
||||||
groupName: string = '';
|
groupName: string = '';
|
||||||
enabled: boolean = true;
|
enabled: boolean = true;
|
||||||
editing: boolean = false;
|
editing: boolean = false;
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
private apiUrl: string;
|
private apiUrl = `${this.baseUrl}/commands`;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private dialogRef: MatDialogRef<CreateCommandGroupComponent>,
|
private dialogRef: MatDialogRef<CreateCommandGroupComponent>,
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService,
|
||||||
private configService: ConfigService,
|
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any
|
@Inject(MAT_DIALOG_DATA) public data: any
|
||||||
) {
|
) {}
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.apiUrl = `${this.baseUrl}/commands`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadAvailableCommands();
|
this.loadAvailableCommands();
|
||||||
|
|
@ -51,7 +43,7 @@ export class CreateCommandGroupComponent implements OnInit {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error(this.translate.instant('errorFetchingAvailableCommands'), error);
|
console.error('Error fetching available commands', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -91,23 +83,23 @@ export class CreateCommandGroupComponent implements OnInit {
|
||||||
const groupId = this.data.group.uuid;
|
const groupId = this.data.group.uuid;
|
||||||
this.http.patch(`${this.baseUrl}/command-groups/${groupId}`, payload).subscribe({
|
this.http.patch(`${this.baseUrl}/command-groups/${groupId}`, payload).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.success(this.translate.instant('commandGroupUpdatedWith'));
|
this.toastService.success('Grupo de comandos actualizado con éxito');
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error(this.translate.instant('errorUpdatingGroup'), error);
|
console.error('Error actualizando el grupo de comandos', error);
|
||||||
this.toastService.error(this.translate.instant('errorUpdatingGroup'));
|
this.toastService.error('Error al actualizar el grupo de comandos');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.http.post(`${this.baseUrl}/command-groups`, payload).subscribe({
|
this.http.post(`${this.baseUrl}/command-groups`, payload).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.success(this.translate.instant('commandGroupCreatedWith'));
|
this.toastService.success('Grupo de comandos creado con éxito');
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error(this.translate.instant('errorCreatingGroup'), error);
|
console.error('Error creando el grupo de comandos', error);
|
||||||
this.toastService.error(this.translate.instant('errorCreatingGroup'));
|
this.toastService.error('Error al crear el grupo de comandos');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
<h3>{{ 'includedCommands' | translate }}</h3>
|
<h3>{{ 'includedCommands' | translate }}</h3>
|
||||||
<table mat-table [dataSource]="data.commands" class="mat-elevation-z8">
|
<table mat-table [dataSource]="data.commands" class="mat-elevation-z8">
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ 'labelRoleName' | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef> {{ 'nameColumn' | translate }} </th>
|
||||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="uuid">
|
<ng-container matColumnDef="uuid">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ 'labelUuid' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||||
<td mat-cell *matCellDef="let command"> {{ command.uuid }} </td>
|
<td mat-cell *matCellDef="let command"> {{ command.uuid }} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<form [formGroup]="form">
|
<form [formGroup]="form">
|
||||||
<h4>{{ 'selectClients' | translate }}</h4>
|
<h4>{{ 'selectClients' | translate }}</h4>
|
||||||
<mat-form-field appearance="fill">
|
<mat-form-field appearance="fill">
|
||||||
<mat-label>{{ 'labelClients' | translate }}</mat-label>
|
<mat-label>{{ 'clientsLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="selectedClients" multiple (selectionChange)="onClientSelectionChange($event)">
|
<mat-select formControlName="selectedClients" multiple (selectionChange)="onClientSelectionChange($event)">
|
||||||
<mat-option *ngFor="let client of clients" [value]="client.uuid">
|
<mat-option *ngFor="let client of clients" [value]="client.uuid">
|
||||||
{{ client.name }}
|
{{ client.name }}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-detail-command-group',
|
selector: 'app-detail-command-group',
|
||||||
|
|
@ -13,8 +10,7 @@ import { ConfigService } from '@services/config.service';
|
||||||
styleUrls: ['./detail-command-group.component.css']
|
styleUrls: ['./detail-command-group.component.css']
|
||||||
})
|
})
|
||||||
export class DetailCommandGroupComponent implements OnInit {
|
export class DetailCommandGroupComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
form!: FormGroup;
|
form!: FormGroup;
|
||||||
clients: any[] = [];
|
clients: any[] = [];
|
||||||
showClientSelect = false;
|
showClientSelect = false;
|
||||||
|
|
@ -25,12 +21,9 @@ export class DetailCommandGroupComponent implements OnInit {
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
private dialogRef: MatDialogRef<DetailCommandGroupComponent>,
|
private dialogRef: MatDialogRef<DetailCommandGroupComponent>,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
private configService: ConfigService,
|
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private toastService: ToastrService
|
private toastService: ToastrService
|
||||||
) {
|
) { }
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.form = this.fb.group({
|
this.form = this.fb.group({
|
||||||
|
|
@ -48,7 +41,7 @@ export class DetailCommandGroupComponent implements OnInit {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error(this.translate.instant('fetchingClients1Error'), error);
|
console.error('Error fetching clients:', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -73,11 +66,11 @@ export class DetailCommandGroupComponent implements OnInit {
|
||||||
this.http.post(apiUrl, payload).subscribe({
|
this.http.post(apiUrl, payload).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
this.toastService.success(this.translate.instant('groupOfCommandsExecutedSuccessfully'));
|
this.toastService.success('Grupo de comandos ejecutado exitosamente');
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error(this.translate.instant('errorExecutingGroupOfCommands'), error);
|
console.error('Error ejecutando grupo de comandos:', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -77,117 +77,3 @@ table {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos para los botones de gestión */
|
|
||||||
.schedule-btn, .script-btn, .traces-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 4px;
|
|
||||||
line-height: 1;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-btn {
|
|
||||||
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-btn:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-btn {
|
|
||||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-btn:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.traces-btn {
|
|
||||||
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.traces-btn:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-btn mat-icon, .script-btn mat-icon, .traces-btn mat-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-btn span, .script-btn span, .traces-btn span {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Indicadores de estado de trazas */
|
|
||||||
.trace-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
padding: 0 6px;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-success {
|
|
||||||
background-color: #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-in-progress {
|
|
||||||
background-color: #2196f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-pending {
|
|
||||||
background-color: #ff9800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-failed {
|
|
||||||
background-color: #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-no-data {
|
|
||||||
color: #999;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive para los botones */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.schedule-btn, .script-btn, .traces-btn {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
<app-loading [isLoading]="loading"></app-loading>
|
|
||||||
|
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||||
<mat-icon>help</mat-icon>
|
<mat-icon>help</mat-icon>
|
||||||
|
|
@ -16,8 +14,8 @@
|
||||||
|
|
||||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||||
<mat-form-field appearance="fill" class="search-string">
|
<mat-form-field appearance="fill" class="search-string">
|
||||||
<mat-label>{{ 'labelSearchTask' | translate }}</mat-label>
|
<mat-label>{{ 'searchTaskLabel' | translate }}</mat-label>
|
||||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
@ -25,40 +23,37 @@
|
||||||
|
|
||||||
<div *ngIf="!loading">
|
<div *ngIf="!loading">
|
||||||
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
<ng-container matColumnDef="taskid">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef> {{ 'idColumn' | translate }} </th>
|
||||||
<td mat-cell *matCellDef="let task">
|
<td mat-cell *matCellDef="let task"> {{ task.id }} </td>
|
||||||
<ng-container *ngIf="column.columnDef !== 'management' && column.columnDef !== 'scope' && column.columnDef !== 'organizationalUnit'">
|
</ng-container>
|
||||||
{{ column.cell(task) }}
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'scope'">
|
<ng-container matColumnDef="notes">
|
||||||
<mat-chip [color]="getScopeColor(task.scope)" selected>
|
<th mat-header-cell *matHeaderCellDef> {{ 'infoColumn' | translate }} </th>
|
||||||
{{ getScopeTranslation(task.scope) }}
|
<td mat-cell *matCellDef="let task"> {{ task.notes }} </td>
|
||||||
</mat-chip>
|
</ng-container>
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'organizationalUnit'">
|
<ng-container matColumnDef="name">
|
||||||
<span *ngIf="task.scope !== 'clients'">{{ task.organizationalUnit?.name || '' }}</span>
|
<th mat-header-cell *matHeaderCellDef> {{ 'createdByColumn' | translate }} </th>
|
||||||
<span *ngIf="task.scope === 'clients'">-</span>
|
<td mat-cell *matCellDef="let task"> {{ task.createdBy }} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'management'">
|
<ng-container matColumnDef="scheduledDate">
|
||||||
<button class="action-button schedule-btn" (click)="openShowScheduleDialog(task)">
|
<th mat-header-cell *matHeaderCellDef> {{ 'executionDateColumn' | translate }} </th>
|
||||||
<mat-icon>schedule</mat-icon>
|
<td mat-cell *matCellDef="let task"> {{ task.dateTime | date:'short' }} </td>
|
||||||
<span>{{ 'schedules' | translate }}</span>
|
</ng-container>
|
||||||
</button>
|
|
||||||
<button class="action-button script-btn" (click)="openShowScriptDialog(task)">
|
<ng-container matColumnDef="enabled">
|
||||||
<mat-icon>code</mat-icon>
|
<th mat-header-cell *matHeaderCellDef> {{ 'statusColumn' | translate }} </th>
|
||||||
<span>{{ 'actions' | translate }}</span>
|
<td mat-cell *matCellDef="let task"> {{ task.enabled ? ('enabled' | translate) : ('disabled' | translate) }} </td>
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
<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 }}">
|
<td mat-cell *matCellDef="let task" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||||
|
<button mat-icon-button color="info" (click)="viewTaskDetails(task)">
|
||||||
|
<mat-icon>visibility</mat-icon>
|
||||||
|
</button>
|
||||||
<button mat-icon-button color="primary" (click)="editTask(task)">
|
<button mat-icon-button color="primary" (click)="editTask(task)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,11 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { LoadingComponent } from '../../../shared/loading/loading.component';
|
import { LoadingComponent } from '../../../shared/loading/loading.component';
|
||||||
import { JoyrideModule, JoyrideService, JoyrideStepService } from 'ngx-joyride';
|
import { JoyrideModule, JoyrideService, JoyrideStepService } from 'ngx-joyride';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
|
|
||||||
describe('CommandsTaskComponent', () => {
|
describe('CommandsTaskComponent', () => {
|
||||||
let component: CommandsTaskComponent;
|
let component: CommandsTaskComponent;
|
||||||
let fixture: ComponentFixture<CommandsTaskComponent>;
|
let fixture: ComponentFixture<CommandsTaskComponent>;
|
||||||
let mockConfigService: jasmine.SpyObj<ConfigService>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const configServiceSpy = jasmine.createSpyObj('ConfigService', [], { apiUrl: 'http://mock-api-url' });
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
|
|
@ -38,13 +33,8 @@ describe('CommandsTaskComponent', () => {
|
||||||
JoyrideModule.forRoot(),
|
JoyrideModule.forRoot(),
|
||||||
],
|
],
|
||||||
declarations: [CommandsTaskComponent, LoadingComponent],
|
declarations: [CommandsTaskComponent, LoadingComponent],
|
||||||
providers: [
|
|
||||||
{ provide: ConfigService, useValue: configServiceSpy }
|
|
||||||
],
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
mockConfigService = TestBed.inject(ConfigService) as jasmine.SpyObj<ConfigService>;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { CreateTaskComponent } from './create-task/create-task.component';
|
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 { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||||
import { JoyrideService } from 'ngx-joyride';
|
import { JoyrideService } from 'ngx-joyride';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
import {ShowTaskScheduleComponent} from "./show-task-schedule/show-task-schedule.component";
|
|
||||||
import {ShowTaskScriptComponent} from "./show-task-script/show-task-script.component";
|
|
||||||
import {DatePipe} from "@angular/common";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-commands-task',
|
selector: 'app-commands-task',
|
||||||
|
|
@ -18,36 +13,19 @@ import {DatePipe} from "@angular/common";
|
||||||
styleUrls: ['./commands-task.component.css']
|
styleUrls: ['./commands-task.component.css']
|
||||||
})
|
})
|
||||||
export class CommandsTaskComponent implements OnInit {
|
export class CommandsTaskComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
tasks: any[] = [];
|
tasks: any[] = [];
|
||||||
filters: { [key: string]: string | boolean } = {};
|
filters: { [key: string]: string | boolean } = {};
|
||||||
length: number = 0;
|
length: number = 0;
|
||||||
itemsPerPage: number = 10;
|
itemsPerPage: number = 10;
|
||||||
page: number = 1;
|
page: number = 1;
|
||||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||||
datePipe: DatePipe = new DatePipe('es-ES');
|
displayedColumns: string[] = ['taskid', 'notes', 'name', 'scheduledDate', 'enabled', 'actions'];
|
||||||
|
|
||||||
columns = [
|
|
||||||
{ columnDef: 'id', header: 'labelId', cell: (task: any) => task.id },
|
|
||||||
{ columnDef: 'name', header: 'taskName', cell: (task: any) => task.name },
|
|
||||||
{ columnDef: 'scope', header: 'labelScope', cell: (task: any) => task.scope },
|
|
||||||
{ columnDef: 'organizationalUnit', header: 'organizationalUnit', cell: (task: any) => task.organizationalUnit?.name || '' },
|
|
||||||
{ columnDef: 'management', header: 'efforts', cell: (task: any) => task.schedules },
|
|
||||||
{ columnDef: 'nextExecution', header: 'nextExecution', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss') },
|
|
||||||
{ columnDef: 'createdBy', header: 'createdBy', cell: (task: any) => task.createdBy },
|
|
||||||
];
|
|
||||||
|
|
||||||
displayedColumns: string[] = ['id', 'name', 'scope', 'management', 'nextExecution', 'createdBy', 'actions'];
|
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
private apiUrl: string;
|
private apiUrl = `${this.baseUrl}/command-tasks`;
|
||||||
|
|
||||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||||
private configService: ConfigService,
|
private joyrideService: JoyrideService) {}
|
||||||
private joyrideService: JoyrideService) {
|
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadTasks();
|
this.loadTasks();
|
||||||
|
|
@ -64,83 +42,48 @@ export class CommandsTaskComponent implements OnInit {
|
||||||
(data) => {
|
(data) => {
|
||||||
this.tasks = data['hydra:member'];
|
this.tasks = data['hydra:member'];
|
||||||
this.length = data['hydra:totalItems'];
|
this.length = data['hydra:totalItems'];
|
||||||
this.updateDisplayedColumns();
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error(this.translate.instant('fetchingTasksError'), error);
|
console.error('Error fetching tasks', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewTaskDetails(task: any): void {
|
||||||
updateDisplayedColumns(): void {
|
this.dialog.open(DetailTaskComponent, {
|
||||||
const hasNonClientTasks = this.tasks.some(task => task.scope !== 'clients');
|
width: '800px',
|
||||||
|
data: { task },
|
||||||
if (hasNonClientTasks) {
|
}).afterClosed().subscribe(() => this.loadTasks());
|
||||||
this.displayedColumns = ['id', 'name', 'scope', 'organizationalUnit', 'management', 'nextExecution', 'createdBy', 'actions'];
|
|
||||||
} else {
|
|
||||||
this.displayedColumns = ['id', 'name', 'scope', 'management', 'nextExecution', 'createdBy', 'actions'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getScopeTranslation(scope: string): string {
|
|
||||||
const translations: { [key: string]: string } = {
|
|
||||||
'organizational-unit': 'Unidad Organizativa',
|
|
||||||
'classrooms-group': 'Grupo de aulas',
|
|
||||||
'classroom': 'Aula',
|
|
||||||
'clients-group': 'Grupo de clientes',
|
|
||||||
'clients': 'Clientes'
|
|
||||||
};
|
|
||||||
return translations[scope] || scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
getScopeColor(scope: string): string {
|
|
||||||
const colors: { [key: string]: string } = {
|
|
||||||
'organizational-unit': 'primary',
|
|
||||||
'classrooms-group': 'accent',
|
|
||||||
'classroom': 'warn',
|
|
||||||
'clients-group': 'accent',
|
|
||||||
'clients': 'primary'
|
|
||||||
};
|
|
||||||
return colors[scope] || 'primary';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openCreateTaskModal(): void {
|
openCreateTaskModal(): void {
|
||||||
this.dialog.open(CreateTaskComponent, {
|
this.dialog.open(CreateTaskComponent, {
|
||||||
width: '800px',
|
width: '800px',
|
||||||
}).afterClosed().subscribe(result => {
|
}).afterClosed().subscribe(() => this.loadTasks());
|
||||||
if (result) {
|
|
||||||
this.loadTasks();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
editTask(task: any): void {
|
editTask(task: any): void {
|
||||||
this.dialog.open(CreateTaskComponent, {
|
this.dialog.open(CreateTaskComponent, {
|
||||||
width: '800px',
|
width: '800px',
|
||||||
data: { task },
|
data: { task },
|
||||||
}).afterClosed().subscribe(result => {
|
}).afterClosed().subscribe(() => this.loadTasks());
|
||||||
if (result) {
|
|
||||||
this.loadTasks();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTask(task: any): void {
|
deleteTask(task: any): void {
|
||||||
this.dialog.open(DeleteModalComponent, {
|
this.dialog.open(DeleteModalComponent, {
|
||||||
width: '300px',
|
width: '300px',
|
||||||
data: { name: task.name },
|
data: { name: task.createdBy },
|
||||||
}).afterClosed().subscribe((result) => {
|
}).afterClosed().subscribe((result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
this.http.delete(`${this.apiUrl}/${task.uuid}`).subscribe({
|
this.http.delete(`${this.apiUrl}/${task.uuid}`).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.success(this.translate.instant('taskDeletedSuccessfully'));
|
this.toastService.success('Tarea eliminada con éxito');
|
||||||
this.loadTasks();
|
this.loadTasks();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error(this.translate.instant('errorDeletingTask'), error);
|
console.error('Error al eliminar la tarea:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -153,38 +96,6 @@ export class CommandsTaskComponent implements OnInit {
|
||||||
this.loadTasks();
|
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 {
|
iniciarTour(): void {
|
||||||
this.joyrideService.startTour({
|
this.joyrideService.startTour({
|
||||||
steps: [
|
steps: [
|
||||||
|
|
@ -199,5 +110,5 @@ export class CommandsTaskComponent implements OnInit {
|
||||||
themeColor: '#3f51b5'
|
themeColor: '#3f51b5'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
.dialog-title {
|
|
||||||
color: #333;
|
|
||||||
font-weight: 600;
|
|
||||||
border-bottom: 2px solid #e0e0e0;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-content {
|
|
||||||
padding: 40px;
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-half {
|
|
||||||
width: 48%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-time {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-label {
|
|
||||||
display: block;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekday-toggle-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekday-toggle {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 2px solid #e0e0e0;
|
|
||||||
background: white;
|
|
||||||
border-radius: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekday-toggle:hover {
|
|
||||||
border-color: #2196f3;
|
|
||||||
background: #f3f8ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekday-toggle.selected {
|
|
||||||
background: #2196f3;
|
|
||||||
color: white;
|
|
||||||
border-color: #2196f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.month-toggle-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.month-toggle {
|
|
||||||
padding: 6px 10px;
|
|
||||||
border: 2px solid #e0e0e0;
|
|
||||||
background: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
min-width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.month-toggle:hover {
|
|
||||||
border-color: #4caf50;
|
|
||||||
background: #f1f8e9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.month-toggle.selected {
|
|
||||||
background: #4caf50;
|
|
||||||
color: white;
|
|
||||||
border-color: #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.enabled-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-icon {
|
|
||||||
color: #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resumen mejorado */
|
|
||||||
.summary-card {
|
|
||||||
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
|
|
||||||
border: 1px solid #bbdefb;
|
|
||||||
border-radius: 12px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
color: #1976d2;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-title mat-icon {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-summary {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 1px solid rgba(25, 118, 210, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-icon {
|
|
||||||
color: #1976d2;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-text {
|
|
||||||
color: #333;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Acciones */
|
|
||||||
.action-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ordinary-button {
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ordinary-button:hover {
|
|
||||||
background: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button {
|
|
||||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 24px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button:hover:not(:disabled) {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button:disabled {
|
|
||||||
background: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button mat-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.custom-time {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-half {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekday-toggle-group {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.month-toggle-row {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button, .ordinary-button {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
<h2 mat-dialog-title class="dialog-title">
|
|
||||||
{{ (data.schedule ? 'editSchedule' : 'createSchedule') | translate }} {{ 'taskSchedule' | translate }}
|
|
||||||
</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>{{ 'repetition' | translate }}</mat-label>
|
|
||||||
<mat-select formControlName="recurrenceType">
|
|
||||||
<mat-option *ngFor="let opt of recurrenceTypes" [value]="opt.value">{{ opt.label }}</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="w-full" *ngIf="form.get('recurrenceType')?.value === 'none'">
|
|
||||||
<mat-label>{{ 'executionDate' | translate }}</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>{{ 'executionTime' | translate }}</mat-label>
|
|
||||||
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
|
|
||||||
<mat-hint>{{ 'timeFormatHint' | translate }}</mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
|
|
||||||
<label class="section-label">{{ 'labelWeekDays' | translate }}:</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>
|
|
||||||
|
|
||||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
|
|
||||||
<label class="section-label">{{ 'labelMonths' | translate }}:</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>
|
|
||||||
|
|
||||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
|
|
||||||
<mat-form-field appearance="fill" class="w-half">
|
|
||||||
<mat-label>{{ 'startDate' | translate }}</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>{{ 'endDate' | translate }}</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" class="enabled-checkbox">
|
|
||||||
<mat-icon class="checkbox-icon">power_settings_new</mat-icon>
|
|
||||||
{{ 'enableSchedule' | translate }}
|
|
||||||
</mat-checkbox>
|
|
||||||
|
|
||||||
<mat-card *ngIf="summaryText" class="summary-card">
|
|
||||||
<mat-card-header>
|
|
||||||
<mat-card-title class="summary-title">
|
|
||||||
<mat-icon color="primary">info</mat-icon>
|
|
||||||
{{ 'scheduleSummaryTitle' | translate }}
|
|
||||||
</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="schedule-summary">
|
|
||||||
<div class="summary-item">
|
|
||||||
<mat-icon class="summary-icon">schedule</mat-icon>
|
|
||||||
<span class="summary-text">{{ summaryText }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item" *ngIf="nextExecutionDate">
|
|
||||||
<mat-icon class="summary-icon">next_plan</mat-icon>
|
|
||||||
<span class="summary-text">{{ 'nextExecution' | translate }}: {{ nextExecutionDate | date:'full' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item" *ngIf="executionCount">
|
|
||||||
<mat-icon class="summary-icon">repeat</mat-icon>
|
|
||||||
<span class="summary-text">{{ 'willExecuteTimes' | translate:{ count: executionCount } }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</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" [disabled]="!form.valid" (click)="onSubmit()">
|
|
||||||
<mat-icon>save</mat-icon>
|
|
||||||
{{ 'labelSaveButton' | 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,388 +0,0 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
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{
|
|
||||||
public translate = inject(TranslateService);
|
|
||||||
form: FormGroup;
|
|
||||||
baseUrl: string;
|
|
||||||
apiUrl: string;
|
|
||||||
recurrenceTypes = [
|
|
||||||
{ value: 'none', label : this.translate.instant('withoutRepetition') },
|
|
||||||
{ value: 'custom', label : this.translate.instant('customRepeat') }
|
|
||||||
];
|
|
||||||
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 } = {};
|
|
||||||
nextExecutionDate: Date | null = null;
|
|
||||||
executionCount: number = 0;
|
|
||||||
|
|
||||||
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(this.translate.instant('errorLoadingScheduleData'), error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatExecutionTime(time: string | Date): string {
|
|
||||||
if (typeof time === 'string') {
|
|
||||||
const hhmmMatch = time.match(/^\d{2}:\d{2}/);
|
|
||||||
if (hhmmMatch) {
|
|
||||||
return hhmmMatch[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = new Date(time);
|
|
||||||
if (!isNaN(parsed.getTime())) {
|
|
||||||
const utcHours = parsed.getUTCHours();
|
|
||||||
const utcMinutes = parsed.getUTCMinutes();
|
|
||||||
|
|
||||||
const currentDate = new Date();
|
|
||||||
currentDate.setUTCHours(utcHours, utcMinutes, 0, 0);
|
|
||||||
|
|
||||||
const hours = String(currentDate.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(currentDate.getMinutes()).padStart(2, '0');
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time instanceof Date && !isNaN(time.getTime())) {
|
|
||||||
const hours = String(time.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(time.getMinutes()).padStart(2, '0');
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
convertDateToLocalISO(date: Date): string {
|
|
||||||
date = new Date(date);
|
|
||||||
const adjustedDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
||||||
return adjustedDate.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
convertTimeToUTC(timeString: string): string {
|
|
||||||
const today = new Date();
|
|
||||||
const [hours, minutes] = timeString.split(':');
|
|
||||||
|
|
||||||
const localDateTime = new Date(
|
|
||||||
today.getFullYear(),
|
|
||||||
today.getMonth(),
|
|
||||||
today.getDate(),
|
|
||||||
parseInt(hours, 10),
|
|
||||||
parseInt(minutes, 10),
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
return localDateTime.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onSubmit() {
|
|
||||||
const formData = this.form.value;
|
|
||||||
|
|
||||||
if (formData.recurrenceType === 'custom') {
|
|
||||||
const selectedDayNames = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
|
|
||||||
const selectedMonthNames = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
|
|
||||||
|
|
||||||
if (selectedDayNames.length === 0) {
|
|
||||||
this.toastr.error(this.translate.instant('youMustSelectAtLeastOne1'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedMonthNames.length === 0) {
|
|
||||||
this.toastr.error(this.translate.instant('youMustSelectAtLeastOne2'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initDate = formData.recurrenceDetails?.initDate;
|
|
||||||
const endDate = formData.recurrenceDetails?.endDate;
|
|
||||||
|
|
||||||
if (!initDate || !endDate) {
|
|
||||||
this.toastr.error(this.translate.instant('youMustSpecifyStartDate'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new Date(endDate) <= new Date(initDate)) {
|
|
||||||
this.toastr.error(this.translate.instant('theEndDateMust'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: any = {
|
|
||||||
commandTask: this.data.task['@id'],
|
|
||||||
executionDate: formData.recurrenceType === 'none' ? this.convertDateToLocalISO(formData.executionDate) : null,
|
|
||||||
executionTime: this.convertTimeToUTC(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(this.translate.instant('updatedTaskScheduleWith'));
|
|
||||||
this.dialogRef.close(true);
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.toastr.error(this.translate.instant('errorUpdatingTask'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.http.post<any>(this.apiUrl, payload).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.toastr.success(this.translate.instant('taskScheduleCreatedWith'));
|
|
||||||
this.dialogRef.close(true);
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.toastr.error(this.translate.instant('errorCreatingTask'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
this.calculateNextExecutionAndCount();
|
|
||||||
|
|
||||||
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}.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateNextExecutionAndCount(): void {
|
|
||||||
const recurrence = this.form.get('recurrenceType')?.value;
|
|
||||||
const time = this.form.get('executionTime')?.value;
|
|
||||||
|
|
||||||
if (recurrence === 'none') {
|
|
||||||
const execDate = this.form.get('executionDate')?.value;
|
|
||||||
if (execDate && time) {
|
|
||||||
this.nextExecutionDate = new Date(execDate);
|
|
||||||
const [hours, minutes] = time.split(':');
|
|
||||||
this.nextExecutionDate.setHours(parseInt(hours), parseInt(minutes), 0, 0);
|
|
||||||
this.executionCount = 1;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDateValue = this.form.get('recurrenceDetails.initDate')?.value;
|
|
||||||
const endDateValue = this.form.get('recurrenceDetails.endDate')?.value;
|
|
||||||
|
|
||||||
const selectedDayNames = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
|
|
||||||
const selectedMonthNames = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
|
|
||||||
|
|
||||||
if (!startDateValue || !endDateValue || selectedDayNames.length === 0 || selectedMonthNames.length === 0) {
|
|
||||||
this.nextExecutionDate = null;
|
|
||||||
this.executionCount = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDate = new Date(startDateValue);
|
|
||||||
const endDate = new Date(endDateValue);
|
|
||||||
|
|
||||||
const selectedDayIndices = new Set<number>(selectedDayNames.map(name => this.getDayIndexFromName(name)));
|
|
||||||
const selectedMonthIndices = new Set<number>(selectedMonthNames.map(name => this.getMonthIndexFromName(name)));
|
|
||||||
|
|
||||||
this.executionCount = this.calculateExecutionCountExact(startDate, endDate, selectedDayIndices, selectedMonthIndices);
|
|
||||||
|
|
||||||
this.nextExecutionDate = this.findNextExecutionDate(startDate, endDate, time, selectedDayIndices, selectedMonthIndices);
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateExecutionCountExact(startDate: Date, endDate: Date, selectedDays: Set<number>, selectedMonths: Set<number>): number {
|
|
||||||
const cursor = new Date(startDate);
|
|
||||||
cursor.setHours(0, 0, 0, 0);
|
|
||||||
const end = new Date(endDate);
|
|
||||||
end.setHours(23, 59, 59, 999);
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
while (cursor <= end) {
|
|
||||||
if (selectedMonths.has(cursor.getMonth()) && selectedDays.has(cursor.getDay())) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
cursor.setDate(cursor.getDate() + 1);
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private findNextExecutionDate(startDate: Date, endDate: Date, time: string, selectedDays: Set<number>, selectedMonths: Set<number>): Date | null {
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const startCandidate = new Date(Math.max(new Date(startDate).setHours(0, 0, 0, 0), new Date(now).setHours(0, 0, 0, 0)));
|
|
||||||
const endLimit = new Date(endDate);
|
|
||||||
endLimit.setHours(23, 59, 59, 999);
|
|
||||||
|
|
||||||
const [hours, minutes] = (time || '00:00').split(':');
|
|
||||||
|
|
||||||
const candidate = new Date(startCandidate);
|
|
||||||
while (candidate <= endLimit) {
|
|
||||||
if (selectedMonths.has(candidate.getMonth()) && selectedDays.has(candidate.getDay())) {
|
|
||||||
const candidateDateTime = new Date(candidate);
|
|
||||||
candidateDateTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
|
|
||||||
if (candidateDateTime >= now) {
|
|
||||||
return candidateDateTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
candidate.setDate(candidate.getDate() + 1);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDayIndexFromName(dayName: string): number {
|
|
||||||
const mapping: { [key: string]: number } = {
|
|
||||||
sunday: 0,
|
|
||||||
monday: 1,
|
|
||||||
tuesday: 2,
|
|
||||||
wednesday: 3,
|
|
||||||
thursday: 4,
|
|
||||||
friday: 5,
|
|
||||||
saturday: 6
|
|
||||||
};
|
|
||||||
return mapping[dayName.toLowerCase()] ?? -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMonthIndexFromName(monthName: string): number {
|
|
||||||
const mapping: { [key: string]: number } = {
|
|
||||||
january: 0,
|
|
||||||
february: 1,
|
|
||||||
march: 2,
|
|
||||||
april: 3,
|
|
||||||
may: 4,
|
|
||||||
june: 5,
|
|
||||||
july: 6,
|
|
||||||
august: 7,
|
|
||||||
september: 8,
|
|
||||||
october: 9,
|
|
||||||
november: 10,
|
|
||||||
december: 11
|
|
||||||
};
|
|
||||||
return mapping[monthName.toLowerCase()] ?? -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,363 +0,0 @@
|
||||||
.dialog-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #666;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-icon:hover {
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-with-action {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Category cards ===== */
|
|
||||||
.categories-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 18px 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-left: 5px solid;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-card:hover {
|
|
||||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-icon mat-icon {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 26px;
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-info h3 {
|
|
||||||
margin: 0 0 4px 0;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-info p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #777;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-arrow {
|
|
||||||
color: #bbb;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-card:hover .category-arrow {
|
|
||||||
transform: translateX(4px);
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-icon {
|
|
||||||
color: #90a4ae;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Info panel (for assistant-required categories) ===== */
|
|
||||||
.info-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 24px 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-panel-icon .big-info-icon {
|
|
||||||
font-size: 56px;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-panel-content {
|
|
||||||
max-width: 520px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-panel-content h3 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-panel-content > p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0 0 24px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-steps {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
padding: 20px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-step {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-number-badge {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #1976d2;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 13px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-supported-ops {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-supported-label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #555;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-op-chip {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 14px;
|
|
||||||
margin: 4px 6px 4px 0;
|
|
||||||
background: #f0f4f8;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-op-chip mat-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Action selector within category ===== */
|
|
||||||
.actions-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-left: 4px solid;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
background: #fff;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-card:hover {
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-card mat-icon:first-child {
|
|
||||||
font-size: 24px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-card span {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Form sections ===== */
|
|
||||||
.action-form {
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-field {
|
|
||||||
width: 200px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-width {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.half-width {
|
|
||||||
width: calc(50% - 8px);
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== System action info ===== */
|
|
||||||
.system-action-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 32px 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-action-info .big-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-action-info p {
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Script section ===== */
|
|
||||||
.script-type-selector {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-command-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
background-color: #eaeff6;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-command-container textarea {
|
|
||||||
font-family: monospace;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-container {
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #eaeff6;
|
|
||||||
border-radius: 12px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-content {
|
|
||||||
flex: 2;
|
|
||||||
min-width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-params {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 35%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-preview {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-family: monospace;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
min-height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Action buttons ===== */
|
|
||||||
.action-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 1em;
|
|
||||||
padding: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Responsive ===== */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.radio-row {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-steps {
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
<h2 mat-dialog-title class="dialog-title">
|
|
||||||
<mat-icon *ngIf="selectedCategory" class="back-icon" (click)="goBackToCategories()">arrow_back</mat-icon>
|
|
||||||
<span *ngIf="!selectedCategory">{{ 'addActionTo' | translate }}: {{ data.commandTask?.name }}</span>
|
|
||||||
<span *ngIf="selectedCategory && !selectedAction && isInfoOnlyCategory()" class="title-with-action">
|
|
||||||
<mat-icon [style.color]="getSelectedCategoryColor()">{{ categories | keyvalue : undefined }}</mat-icon>
|
|
||||||
<ng-container *ngFor="let cat of categories">
|
|
||||||
<ng-container *ngIf="cat.id === selectedCategory">
|
|
||||||
<mat-icon [style.color]="cat.color">{{ cat.icon }}</mat-icon>
|
|
||||||
{{ cat.labelKey | translate }}
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</span>
|
|
||||||
<span *ngIf="selectedAction" class="title-with-action">
|
|
||||||
<mat-icon [style.color]="getSelectedCategoryColor()">{{ getSelectedActionIcon() }}</mat-icon>
|
|
||||||
{{ getSelectedActionLabel() }}
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<mat-dialog-content class="dialog-content">
|
|
||||||
|
|
||||||
<!-- PASO 1: Selector de categorías -->
|
|
||||||
<div *ngIf="!selectedCategory" class="categories-grid">
|
|
||||||
<div *ngFor="let category of categories"
|
|
||||||
class="category-card"
|
|
||||||
[style.border-left-color]="category.color"
|
|
||||||
(click)="selectCategory(category.id)">
|
|
||||||
<div class="category-icon" [style.background-color]="category.color">
|
|
||||||
<mat-icon>{{ category.icon }}</mat-icon>
|
|
||||||
</div>
|
|
||||||
<div class="category-info">
|
|
||||||
<h3>{{ category.labelKey | translate }}</h3>
|
|
||||||
<p>{{ category.descriptionKey | translate }}</p>
|
|
||||||
</div>
|
|
||||||
<mat-icon class="category-arrow" *ngIf="!category.infoOnly">chevron_right</mat-icon>
|
|
||||||
<mat-icon class="category-arrow info-icon" *ngIf="category.infoOnly">info_outline</mat-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PANEL INFORMATIVO: categorías que requieren contexto de equipo -->
|
|
||||||
<div *ngIf="selectedCategory && isInfoOnlyCategory()" class="info-panel">
|
|
||||||
<div class="info-panel-icon">
|
|
||||||
<ng-container *ngFor="let cat of categories">
|
|
||||||
<mat-icon *ngIf="cat.id === selectedCategory" [style.color]="cat.color" class="big-info-icon">{{ cat.icon }}</mat-icon>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-panel-content">
|
|
||||||
<h3>{{ 'assistantRequiredTitle' | translate }}</h3>
|
|
||||||
<p>{{ 'assistantRequiredDesc' | translate }}</p>
|
|
||||||
|
|
||||||
<div class="info-steps">
|
|
||||||
<div class="info-step">
|
|
||||||
<div class="step-number-badge">1</div>
|
|
||||||
<span>{{ 'assistantStep1' | translate }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-step">
|
|
||||||
<div class="step-number-badge">2</div>
|
|
||||||
<span>{{ 'assistantStep2' | translate }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-step">
|
|
||||||
<div class="step-number-badge">3</div>
|
|
||||||
<span>{{ 'assistantStep3' | translate }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-supported-ops">
|
|
||||||
<p class="info-supported-label">{{ 'labelSupportedOps' | translate }}:</p>
|
|
||||||
<ng-container *ngFor="let cat of categories">
|
|
||||||
<ng-container *ngIf="cat.id === selectedCategory">
|
|
||||||
<div *ngFor="let action of cat.actions" class="info-op-chip">
|
|
||||||
<mat-icon [style.color]="cat.color">{{ action.icon }}</mat-icon>
|
|
||||||
<span>{{ action.labelKey | translate }}</span>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PASO 2: Selector de acción dentro de categoría (solo si tiene >1 acción y no es infoOnly) -->
|
|
||||||
<div *ngIf="selectedCategory && !selectedAction && !isInfoOnlyCategory()" class="actions-grid">
|
|
||||||
<div class="actions-list">
|
|
||||||
<ng-container *ngFor="let category of categories">
|
|
||||||
<ng-container *ngIf="category.id === selectedCategory">
|
|
||||||
<div *ngFor="let action of category.actions"
|
|
||||||
class="action-card"
|
|
||||||
[style.border-left-color]="category.color"
|
|
||||||
(click)="selectAction(action.type)">
|
|
||||||
<mat-icon [style.color]="category.color">{{ action.icon }}</mat-icon>
|
|
||||||
<span>{{ action.labelKey | translate }}</span>
|
|
||||||
<mat-icon class="category-arrow">chevron_right</mat-icon>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PASO 3: Formulario según acción seleccionada -->
|
|
||||||
<div *ngIf="selectedAction" class="action-form">
|
|
||||||
|
|
||||||
<!-- Orden de ejecución (común a todas) -->
|
|
||||||
<mat-form-field appearance="fill" class="order-field">
|
|
||||||
<mat-label>{{ 'executionOrder' | translate }}</mat-label>
|
|
||||||
<input matInput type="number" [(ngModel)]="executionOrder" min="0">
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- ========== SYSTEM CONTROL (power-on, shutdown, reboot) ========== -->
|
|
||||||
<div *ngIf="selectedAction === 'power-on' || selectedAction === 'shutdown' || selectedAction === 'reboot'"
|
|
||||||
class="form-section system-info">
|
|
||||||
<div class="system-action-info">
|
|
||||||
<mat-icon [style.color]="getSelectedCategoryColor()" class="big-icon">{{ getSelectedActionIcon() }}</mat-icon>
|
|
||||||
<p>{{ 'systemActionHint' | translate }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ========== LOGIN ========== -->
|
|
||||||
<div *ngIf="selectedAction === 'login'" class="form-section">
|
|
||||||
<div class="system-action-info" style="margin-bottom: 16px;">
|
|
||||||
<mat-icon [style.color]="getSelectedCategoryColor()" class="big-icon">login</mat-icon>
|
|
||||||
</div>
|
|
||||||
<mat-form-field appearance="fill" class="half-width">
|
|
||||||
<mat-label>{{ 'labelDisk' | translate }}</mat-label>
|
|
||||||
<input matInput type="number" [(ngModel)]="loginDisk" min="1">
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field appearance="fill" class="half-width">
|
|
||||||
<mat-label>{{ 'partition' | translate }}</mat-label>
|
|
||||||
<input matInput type="number" [(ngModel)]="loginPartition" min="1">
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ========== RUN SCRIPT ========== -->
|
|
||||||
<div *ngIf="selectedAction === 'run-script'" class="form-section">
|
|
||||||
<div class="script-type-selector">
|
|
||||||
<mat-radio-group [(ngModel)]="commandType" class="radio-row">
|
|
||||||
<mat-radio-button value="new">
|
|
||||||
<mat-icon>edit</mat-icon> {{ 'newScript' | translate }}
|
|
||||||
</mat-radio-button>
|
|
||||||
<mat-radio-button value="existing">
|
|
||||||
<mat-icon>storage</mat-icon> {{ 'savedScript' | translate }}
|
|
||||||
</mat-radio-button>
|
|
||||||
</mat-radio-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="commandType === 'new'" class="new-command-container">
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
|
||||||
<mat-label>{{ 'enterScript' | translate }}</mat-label>
|
|
||||||
<textarea matInput [(ngModel)]="newScript" rows="6" [placeholder]="'writeScriptHere' | translate"></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
<button mat-flat-button color="primary" (click)="saveNewScript()">{{ 'buttonSaveScript' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="commandType === 'existing'">
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
|
||||||
<mat-label>{{ 'selectScriptToRun' | translate }}</mat-label>
|
|
||||||
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
|
|
||||||
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
|
|
||||||
<div class="script-content">
|
|
||||||
<h3>{{ 'labelScript' | translate }}:</h3>
|
|
||||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
|
||||||
</div>
|
|
||||||
<div class="script-params" *ngIf="parameterNames.length > 0">
|
|
||||||
<h3>{{ 'enterParameters' | translate }}:</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]="('valueFor' | translate) + ' ' + paramName">
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</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()" *ngIf="selectedAction">{{ 'labelSaveButton' | translate }}</button>
|
|
||||||
</mat-dialog-actions>
|
|
||||||
|
|
@ -1,93 +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";
|
|
||||||
import {MatRadioModule} from "@angular/material/radio";
|
|
||||||
import {MatCheckboxModule} from "@angular/material/checkbox";
|
|
||||||
|
|
||||||
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,
|
|
||||||
MatRadioModule,
|
|
||||||
MatCheckboxModule,
|
|
||||||
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,318 +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";
|
|
||||||
import {TranslateService} from "@ngx-translate/core";
|
|
||||||
|
|
||||||
interface ActionCategory {
|
|
||||||
id: string;
|
|
||||||
icon: string;
|
|
||||||
labelKey: string;
|
|
||||||
descriptionKey: string;
|
|
||||||
color: string;
|
|
||||||
infoOnly: boolean;
|
|
||||||
actions: ActionOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActionOption {
|
|
||||||
type: string;
|
|
||||||
icon: string;
|
|
||||||
labelKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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>();
|
|
||||||
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);
|
|
||||||
commandTask: any;
|
|
||||||
|
|
||||||
selectedCategory: string = '';
|
|
||||||
selectedAction: string = '';
|
|
||||||
loginDisk: number = 1;
|
|
||||||
loginPartition: number = 1;
|
|
||||||
|
|
||||||
categories: ActionCategory[] = [
|
|
||||||
{
|
|
||||||
id: 'image-ops',
|
|
||||||
icon: 'perm_media',
|
|
||||||
labelKey: 'catImageOps',
|
|
||||||
descriptionKey: 'catImageOpsDesc',
|
|
||||||
color: '#1565c0',
|
|
||||||
infoOnly: true,
|
|
||||||
actions: [
|
|
||||||
{ type: 'deploy-image', icon: 'cloud_download', labelKey: 'deployImage' },
|
|
||||||
{ type: 'create-image', icon: 'add_photo_alternate', labelKey: 'createImage' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'partition',
|
|
||||||
icon: 'storage',
|
|
||||||
labelKey: 'catPartition',
|
|
||||||
descriptionKey: 'catPartitionDesc',
|
|
||||||
color: '#2e7d32',
|
|
||||||
infoOnly: true,
|
|
||||||
actions: [
|
|
||||||
{ type: 'partition-assistant', icon: 'view_column', labelKey: 'partitionAndFormat' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'system',
|
|
||||||
icon: 'settings_power',
|
|
||||||
labelKey: 'catSystemControl',
|
|
||||||
descriptionKey: 'catSystemControlDesc',
|
|
||||||
color: '#e65100',
|
|
||||||
infoOnly: false,
|
|
||||||
actions: [
|
|
||||||
{ type: 'power-on', icon: 'power', labelKey: 'powerOnDevice' },
|
|
||||||
{ type: 'shutdown', icon: 'power_off', labelKey: 'shutdownDevice' },
|
|
||||||
{ type: 'reboot', icon: 'restart_alt', labelKey: 'rebootDevice' },
|
|
||||||
{ type: 'login', icon: 'login', labelKey: 'loginSession' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'script',
|
|
||||||
icon: 'code',
|
|
||||||
labelKey: 'catCustomScript',
|
|
||||||
descriptionKey: 'catCustomScriptDesc',
|
|
||||||
color: '#6a1b9a',
|
|
||||||
infoOnly: false,
|
|
||||||
actions: [
|
|
||||||
{ type: 'run-script', icon: 'terminal', labelKey: 'runScript' },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
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,
|
|
||||||
private translate: TranslateService,
|
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any
|
|
||||||
) {
|
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.commandTask = this.data.commandTask;
|
|
||||||
|
|
||||||
this.loadScripts();
|
|
||||||
this.form = this.fb.group({
|
|
||||||
content: [''],
|
|
||||||
order: [''],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
|
|
||||||
selectCategory(categoryId: string): void {
|
|
||||||
const cat = this.categories.find(c => c.id === categoryId);
|
|
||||||
if (cat?.infoOnly) {
|
|
||||||
this.selectedCategory = categoryId;
|
|
||||||
this.selectedAction = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.selectedCategory = categoryId;
|
|
||||||
if (cat && cat.actions.length === 1) {
|
|
||||||
this.selectAction(cat.actions[0].type);
|
|
||||||
} else {
|
|
||||||
this.selectedAction = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAction(actionType: string): void {
|
|
||||||
this.selectedAction = actionType;
|
|
||||||
}
|
|
||||||
|
|
||||||
goBackToCategories(): void {
|
|
||||||
this.selectedCategory = '';
|
|
||||||
this.selectedAction = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
goBackToActions(): void {
|
|
||||||
this.selectedAction = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
isInfoOnlyCategory(): boolean {
|
|
||||||
const cat = this.categories.find(c => c.id === this.selectedCategory);
|
|
||||||
return cat?.infoOnly || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onScriptChange(): void {
|
|
||||||
if (this.selectedScript) {
|
|
||||||
this.scriptContent = this.selectedScript.script;
|
|
||||||
|
|
||||||
const matches = this.scriptContent.match(/@(\w+)/g) || [];
|
|
||||||
const uniqueParams = Array.from(new Set(matches.map((m: string) => 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveNewScript(): void {
|
|
||||||
if (!this.newScript.trim()) {
|
|
||||||
this.toastService.error(this.translate.instant('youMustEnterAScriptBefore'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dialogRef = this.dialog.open(SaveScriptComponent, {
|
|
||||||
width: '400px',
|
|
||||||
data: this.newScript
|
|
||||||
});
|
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
|
||||||
if (result) {
|
|
||||||
this.toastService.success(this.translate.instant('scriptSavedSuccessfully'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit(): void {
|
|
||||||
if (!this.selectedAction) {
|
|
||||||
this.toastService.error(this.translate.instant('selectActionTypeError'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.executionOrder === null || this.executionOrder === undefined) {
|
|
||||||
this.toastService.error(this.translate.instant('executionOrderRequired'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let type = '';
|
|
||||||
let content = '';
|
|
||||||
|
|
||||||
switch (this.selectedAction) {
|
|
||||||
case 'run-script':
|
|
||||||
if (!this.validateScript()) return;
|
|
||||||
type = 'run-script';
|
|
||||||
content = this.commandType === 'existing' ? this.scriptContent : this.newScript;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'power-on':
|
|
||||||
case 'shutdown':
|
|
||||||
case 'reboot':
|
|
||||||
type = this.selectedAction;
|
|
||||||
content = this.selectedAction;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'login':
|
|
||||||
type = 'login';
|
|
||||||
content = 'login';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this.toastService.error(this.translate.instant('unrecognizedActionType'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: any = {
|
|
||||||
commandTask: this.commandTask['@id'],
|
|
||||||
content: content,
|
|
||||||
order: this.executionOrder,
|
|
||||||
type: type,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.selectedAction === 'login') {
|
|
||||||
payload.parameters = { disk: this.loginDisk, partition: this.loginPartition };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.http.post(`${this.baseUrl}/command-task-scripts`, payload).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.toastService.success(this.translate.instant('actionAddedSuccess'));
|
|
||||||
this.dialogRef.close(true);
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
this.toastService.error(error.error['hydra:description']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateScript(): boolean {
|
|
||||||
if (this.commandType === 'new') {
|
|
||||||
if (!this.newScript.trim()) {
|
|
||||||
this.toastService.error(this.translate.instant('youMustEnterAScript'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (this.commandType === 'existing') {
|
|
||||||
if (!this.selectedScript) {
|
|
||||||
this.toastService.error(this.translate.instant('youMustSelectAScript'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onCancel(): void {
|
|
||||||
this.dialogRef.close(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedCategoryColor(): string {
|
|
||||||
const cat = this.categories.find(c => c.id === this.selectedCategory);
|
|
||||||
return cat?.color || '#1976d2';
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedActionLabel(): string {
|
|
||||||
for (const cat of this.categories) {
|
|
||||||
const action = cat.actions.find(a => a.type === this.selectedAction);
|
|
||||||
if (action) return this.translate.instant(action.labelKey);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedActionIcon(): string {
|
|
||||||
for (const cat of this.categories) {
|
|
||||||
const action = cat.actions.find(a => a.type === this.selectedAction);
|
|
||||||
if (action) return action.icon;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,213 +6,23 @@
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-task {
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-width {
|
.full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
mat-form-field {
|
mat-form-field {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-form-field.mat-form-field-disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-form-field.mat-form-field-disabled .mat-form-field-label {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-weight: 500;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para la selección de clientes */
|
|
||||||
.clients-selection {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clients-selection h4 {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre-selected-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
background-color: #e3f2fd;
|
|
||||||
border: 1px solid #2196f3;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #1976d2;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre-selected-info mat-icon {
|
|
||||||
color: #2196f3;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-clients {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clients-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-count {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clients-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-card {
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-card.pre-selected {
|
|
||||||
border-color: #4caf50;
|
|
||||||
background-color: #e8f5e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-card.pre-selected:hover {
|
|
||||||
border-color: #45a049;
|
|
||||||
background-color: #d4edda;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-card mat-card-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-name {
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-details {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-ip {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-status {
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-og-live {
|
|
||||||
background-color: #4caf50;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-offline {
|
|
||||||
background-color: #f44336;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-unknown {
|
|
||||||
background-color: #ff9800;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-icon {
|
|
||||||
color: #1976d2;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.clients-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clients-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,137 +1,106 @@
|
||||||
<h2 mat-dialog-title class="dialog-title">
|
<h2 mat-dialog-title class="dialog-title">{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}</h2>
|
||||||
{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<mat-dialog-content class="dialog-content">
|
<form [formGroup]="taskForm" class="task-form">
|
||||||
@if (loading) {
|
<mat-dialog-content>
|
||||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!loading) {
|
<h3 class="section-title">Información</h3>
|
||||||
<div class="task-form">
|
<mat-divider></mat-divider>
|
||||||
@if (data?.source === 'assistant') {
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-button-toggle-group [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode" appearance="standard">
|
<mat-label>Información</mat-label>
|
||||||
<mat-button-toggle value="create">
|
<textarea matInput formControlName="notes" placeholder="Ingresa tus notas aquí"></textarea>
|
||||||
<mat-icon>add_task</mat-icon>
|
</mat-form-field>
|
||||||
{{ 'labelCreateTask' | translate }}
|
|
||||||
</mat-button-toggle>
|
|
||||||
<mat-button-toggle value="add">
|
|
||||||
<mat-icon>playlist_add</mat-icon>
|
|
||||||
{{ 'addToExistingTask' | translate }}
|
|
||||||
</mat-button-toggle>
|
|
||||||
</mat-button-toggle-group>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (taskMode === 'add') {
|
<h3 class="section-title">{{ 'informationSectionTitle' | translate }}</h3>
|
||||||
<div class="select-task">
|
<mat-divider></mat-divider>
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'selectTask' | translate }}</mat-label>
|
<mat-label>{{ 'informationLabel' | translate }}</mat-label>
|
||||||
<mat-select [(ngModel)]="selectedExistingTask" name="existingTask">
|
<textarea matInput formControlName="notes" placeholder="{{ 'notesPlaceholder' | translate }}"></textarea>
|
||||||
@for (task of existingTasks; track task) {
|
</mat-form-field>
|
||||||
<mat-option [value]="task">{{ task.name }}</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
|
||||||
<mat-label>{{ 'executionOrder' | translate }}</mat-label>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="number"
|
|
||||||
[(ngModel)]="executionOrder"
|
|
||||||
name="executionOrder"
|
|
||||||
min="1"
|
|
||||||
[placeholder]="'enterOrder' | translate"
|
|
||||||
>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (taskMode === 'create' && taskForm && !loading) {
|
<h3 class="section-title">{{ 'commandSelectionSectionTitle' | translate }}</h3>
|
||||||
<form [formGroup]="taskForm" class="task-form">
|
<mat-divider></mat-divider>
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
|
<mat-label>{{ 'selectCommandsLabel' | translate }}</mat-label>
|
||||||
<input matInput formControlName="name" [placeholder]="'nameLabel' | translate">
|
<mat-select formControlName="commandGroup" (selectionChange)="onCommandGroupChange()">
|
||||||
@if (taskForm.get('name')?.invalid) {
|
<mat-option *ngFor="let group of availableCommandGroups" [value]="group.uuid">
|
||||||
<mat-error>{{ 'requiredFieldError' | translate }}</mat-error>
|
{{ group.name }}
|
||||||
}
|
</mat-option>
|
||||||
</mat-form-field>
|
</mat-select>
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-error *ngIf="taskForm.get('commandGroup')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||||
<mat-label>{{ 'labelNotes' | translate }}</mat-label>
|
</mat-form-field>
|
||||||
<textarea matInput formControlName="notes" [placeholder]="'notesPlaceholder' | translate"></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
@if (!editing) {
|
|
||||||
<mat-checkbox formControlName="scheduleAfterCreate" class="schedule-checkbox">
|
|
||||||
{{ 'scheduleAfterCreate' | translate }}
|
|
||||||
</mat-checkbox>
|
|
||||||
}
|
|
||||||
<mat-divider style="margin: 15px 0;"></mat-divider>
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
|
||||||
<mat-label>{{ 'labelScope' | translate }}</mat-label>
|
|
||||||
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)"
|
|
||||||
[disabled]="data?.clients && data.clients.length >= 1">
|
|
||||||
<mat-option value="organizational-unit">{{ 'organizationalUnit' | translate }}</mat-option>
|
|
||||||
<mat-option value="classrooms-group">{{ 'scopeClassroomsGroup' | translate }}</mat-option>
|
|
||||||
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
|
|
||||||
<mat-option value="clients-group">{{ 'clientGroupsTitle' | translate }}</mat-option>
|
|
||||||
<mat-option value="clients">{{ 'labelClients' | translate }}</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
@if (taskForm.get('scope')?.value !== 'clients') {
|
|
||||||
<app-ou-select
|
|
||||||
class="full-width"
|
|
||||||
formControlName="organizationalUnit"
|
|
||||||
[units]="availableOrganizationalUnits"
|
|
||||||
[label]="'labelOrganizationalUnit'">
|
|
||||||
</app-ou-select>
|
|
||||||
}
|
|
||||||
@if (taskForm.get('scope')?.value === 'clients') {
|
|
||||||
<div class="clients-selection">
|
|
||||||
<h4>{{ 'selectedClients' | translate }}</h4>
|
|
||||||
@if (data?.selectedClients && data.selectedClients.length > 0) {
|
|
||||||
<div class="pre-selected-info">
|
|
||||||
<mat-icon>info</mat-icon>
|
|
||||||
<span>{{ 'preSelectedInfo' | translate }}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="clients-list">
|
|
||||||
<div class="clients-grid">
|
|
||||||
@for (client of clients; track client) {
|
|
||||||
<mat-card
|
|
||||||
class="client-card"
|
|
||||||
[class.pre-selected]="isClientPreSelected(client)"
|
|
||||||
>
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="client-info">
|
|
||||||
<div class="client-name">{{ client.name || client.hostname }}</div>
|
|
||||||
<div class="client-details">
|
|
||||||
<span class="client-ip">{{ client.ip }}</span>
|
|
||||||
<span class="client-status" [class]="'status-' + client.status">
|
|
||||||
{{ client.status }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<mat-icon class="selected-icon">
|
|
||||||
check_circle
|
|
||||||
</mat-icon>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
</mat-dialog-content>
|
|
||||||
|
|
||||||
<mat-dialog-actions class="action-container">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
<mat-label>{{ 'selectIndividualCommandsLabel' | translate }}</mat-label>
|
||||||
<button
|
<mat-select formControlName="extraCommands" multiple>
|
||||||
class="submit-button"
|
<mat-option *ngFor="let command of availableIndividualCommands" [value]="command.uuid">
|
||||||
(click)="taskMode === 'create' ? saveTask() : addToExistingTask()"
|
{{ command.name }}
|
||||||
>
|
</mat-option>
|
||||||
{{ 'labelSaveButton' | translate }}
|
</mat-select>
|
||||||
</button>
|
</mat-form-field>
|
||||||
</mat-dialog-actions>
|
|
||||||
|
<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-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,16 +1,8 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { Component, OnInit, Inject } from '@angular/core';
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
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 { ToastrService } from 'ngx-toastr';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
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";
|
|
||||||
import {ShowTaskScriptComponent} from "../show-task-script/show-task-script.component";
|
|
||||||
import { DataService as GroupsDataService } from "../../../groups/services/data.service";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-task',
|
selector: 'app-create-task',
|
||||||
|
|
@ -18,343 +10,232 @@ import { DataService as GroupsDataService } from "../../../groups/services/data.
|
||||||
styleUrls: ['./create-task.component.css']
|
styleUrls: ['./create-task.component.css']
|
||||||
})
|
})
|
||||||
export class CreateTaskComponent implements OnInit {
|
export class CreateTaskComponent implements OnInit {
|
||||||
public translate = inject(TranslateService);
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
baseUrl: string;
|
|
||||||
taskForm: FormGroup;
|
taskForm: FormGroup;
|
||||||
availableCommandGroups: any[] = [];
|
availableCommandGroups: any[] = [];
|
||||||
selectedGroupCommands: any[] = [];
|
selectedGroupCommands: any[] = [];
|
||||||
availableIndividualCommands: any[] = [];
|
availableIndividualCommands: any[] = [];
|
||||||
apiUrl: string;
|
apiUrl = `${this.baseUrl}/command-tasks`;
|
||||||
editing: boolean = false;
|
editing: boolean = false;
|
||||||
availableOrganizationalUnits: any[] = [];
|
availableOrganizationalUnits: any[] = [];
|
||||||
clients: any[] = [];
|
selectedUnitChildren: any[] = [];
|
||||||
allOrganizationalUnits: any[] = [];
|
|
||||||
loading: boolean = false;
|
|
||||||
taskMode: 'create' | 'add' = 'create';
|
|
||||||
existingTasks: any[] = [];
|
|
||||||
selectedExistingTask: string | null = null;
|
|
||||||
executionOrder: number | null = null;
|
|
||||||
selectedClients: any[] = [];
|
selectedClients: any[] = [];
|
||||||
|
selectedClientIds: Set<string> = new Set();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private configService: ConfigService,
|
|
||||||
private toastr: ToastrService,
|
private toastr: ToastrService,
|
||||||
private dialog: MatDialog,
|
|
||||||
public dialogRef: MatDialogRef<CreateTaskComponent>,
|
public dialogRef: MatDialogRef<CreateTaskComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
@Inject(MAT_DIALOG_DATA) public data: any
|
||||||
private dataService: GroupsDataService
|
|
||||||
) {
|
) {
|
||||||
this.baseUrl = this.configService.apiUrl;
|
|
||||||
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
|
||||||
|
|
||||||
let initialScope = '';
|
|
||||||
if (this.data?.selectedClients && this.data.selectedClients.length > 0) {
|
|
||||||
initialScope = 'clients';
|
|
||||||
} else if (this.data?.scope) {
|
|
||||||
initialScope = this.data.scope;
|
|
||||||
} else if (this.data?.runScriptContext) {
|
|
||||||
initialScope = this.data.runScriptContext.type || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.taskForm = this.fb.group({
|
this.taskForm = this.fb.group({
|
||||||
scope: [initialScope, Validators.required],
|
commandGroup: ['', Validators.required],
|
||||||
name: ['', Validators.required],
|
extraCommands: [[]],
|
||||||
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null],
|
date: ['', Validators.required],
|
||||||
|
time: ['', Validators.required],
|
||||||
notes: [''],
|
notes: [''],
|
||||||
scheduleAfterCreate: [false]
|
organizationalUnit: ['', Validators.required],
|
||||||
|
selectedChild: [''],
|
||||||
|
selectedClients: [[]]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.data?.selectedClients && Array.isArray(this.data.selectedClients)) {
|
|
||||||
this.selectedClients = [...this.data.selectedClients];
|
|
||||||
this.clients = [...this.data.selectedClients];
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.onScopeChange(initialScope);
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loading = true;
|
this.loadCommandGroups();
|
||||||
const observables = [
|
this.loadIndividualCommands();
|
||||||
this.loadCommandGroups(),
|
this.loadOrganizationalUnits();
|
||||||
this.loadIndividualCommands(),
|
if (this.data && this.data.task) {
|
||||||
this.loadOrganizationalUnits(),
|
this.editing = true;
|
||||||
this.startUnitsFilter(),
|
this.loadTaskData(this.data.task);
|
||||||
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;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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(this.translate.instant('errorLoadingData'));
|
|
||||||
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(this.translate.instant('errorLoadingTasks'));
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onScopeChange(scope: string): void {
|
|
||||||
if (scope === 'clients') {
|
|
||||||
if (this.data?.selectedClients && this.data.selectedClients.length > 0) {
|
|
||||||
this.selectedClients = [...this.data.selectedClients];
|
|
||||||
this.clients = [...this.data.selectedClients];
|
|
||||||
} else {
|
|
||||||
this.toastr.error(this.translate.instant('thereAreNoPreselectedClientsFor'));
|
|
||||||
this.taskForm.get('scope')?.setValue('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.taskForm.get('organizationalUnit')?.setValue('');
|
|
||||||
this.taskForm.get('organizationalUnit')?.clearValidators();
|
|
||||||
} else {
|
|
||||||
this.filterUnits(scope).subscribe(filteredUnits => {
|
|
||||||
this.availableOrganizationalUnits = filteredUnits;
|
|
||||||
if (!this.data?.organizationalUnit) {
|
|
||||||
this.taskForm.get('organizationalUnit')?.setValue('');
|
|
||||||
}
|
|
||||||
this.taskForm.get('organizationalUnit')?.setValidators(Validators.required);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.taskForm.get('organizationalUnit')?.updateValueAndValidity();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isClientPreSelected(client: any): boolean {
|
loadCommandGroups(): void {
|
||||||
return this.data?.selectedClients && this.data.selectedClients.some((c: any) => c.uuid === client.uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(this.translate.instant('errorWhenFilteringUnits'));
|
|
||||||
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) => {
|
|
||||||
this.http.get<any>(`${this.baseUrl}/command-groups`).subscribe(
|
this.http.get<any>(`${this.baseUrl}/command-groups`).subscribe(
|
||||||
(data) => {
|
(data) => {
|
||||||
this.availableCommandGroups = data['hydra:member'];
|
this.availableCommandGroups = data['hydra:member'];
|
||||||
resolve();
|
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.toastr.error(this.translate.instant('errorLoadingGroups'));
|
this.toastr.error('Error al cargar los grupos de comandos');
|
||||||
reject(error);
|
}
|
||||||
});
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadIndividualCommands(): Promise<void> {
|
loadIndividualCommands(): void {
|
||||||
return new Promise((resolve, reject) => {
|
this.http.get<any>(`${this.baseUrl}/commands`).subscribe(
|
||||||
this.http.get<any>(`${this.baseUrl}/commands`).subscribe(
|
(data) => {
|
||||||
(data) => {
|
this.availableIndividualCommands = data['hydra:member'];
|
||||||
this.availableIndividualCommands = data['hydra:member'];
|
},
|
||||||
resolve();
|
(error) => {
|
||||||
},
|
this.toastr.error('Error al cargar los comandos individuales');
|
||||||
(error) => {
|
}
|
||||||
this.toastr.error(this.translate.instant('errorLoadingCommands'));
|
);
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOrganizationalUnits(): Promise<void> {
|
loadOrganizationalUnits(): void {
|
||||||
return new Promise((resolve, reject) => {
|
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
|
||||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=100`).subscribe(
|
(data) => {
|
||||||
(data) => {
|
this.availableOrganizationalUnits = data['hydra:member'].filter((unit: any) => unit['type'] === 'organizational-unit');
|
||||||
this.allOrganizationalUnits = data['hydra:member'].map((unit: any) => ({
|
},
|
||||||
...unit,
|
(error) => {
|
||||||
path: this.dataService.getOrganizationalUnitPath(unit, data['hydra:member'])
|
this.toastr.error('Error al cargar las unidades organizacionales');
|
||||||
}));
|
}
|
||||||
this.availableOrganizationalUnits = [...this.allOrganizationalUnits];
|
);
|
||||||
resolve();
|
}
|
||||||
},
|
|
||||||
(error) => {
|
loadTaskData(task: any): void {
|
||||||
this.toastr.error(this.translate.instant('errorLoadingUnits'));
|
this.taskForm.patchValue({
|
||||||
reject(error);
|
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() {
|
toggleSelectAll() {
|
||||||
if (!this.selectedExistingTask) {
|
const allSelected = this.areAllSelected();
|
||||||
this.toastr.error(this.translate.instant('youMustSelectAnExistingTask'));
|
if (allSelected) {
|
||||||
return;
|
this.selectedClientIds.clear();
|
||||||
|
} else {
|
||||||
|
this.selectedClients.forEach(client => this.selectedClientIds.add(client.uuid));
|
||||||
}
|
}
|
||||||
|
this.taskForm.get('selectedClients')!.setValue(Array.from(this.selectedClientIds));
|
||||||
if (this.executionOrder == null || this.executionOrder < 1) {
|
|
||||||
this.toastr.error(this.translate.instant('youMustEnterAnOrderOf'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
taskId: this.selectedExistingTask,
|
|
||||||
executionOrder: this.executionOrder
|
|
||||||
};
|
|
||||||
|
|
||||||
this.toastr.success(this.translate.instant('taskUpdatedSuccessfully'));
|
|
||||||
this.dialogRef.close(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
saveTask(): void {
|
||||||
if (this.taskForm.invalid) {
|
if (this.taskForm.invalid) {
|
||||||
this.toastr.error(this.translate.instant('pleaseFillOutAll'));
|
this.toastr.error('Por favor, rellene todos los campos obligatorios');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = this.taskForm.value;
|
const formData = this.taskForm.value;
|
||||||
const scope = formData.scope;
|
const dateTime = this.combineDateAndTime(formData.date, formData.time);
|
||||||
|
const selectedCommands = formData.extraCommands && formData.extraCommands.length > 0
|
||||||
if (scope === 'clients' && this.selectedClients.length === 0) {
|
? formData.extraCommands.map((id: any) => `/commands/${id}`)
|
||||||
this.toastr.error(this.translate.instant('youMustSelectAtLeastOne'));
|
: null;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scope !== 'clients' && !formData.organizationalUnit) {
|
|
||||||
this.toastr.error(this.translate.instant('youMustSelectAnOrganizationalUnit'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
name: formData.name,
|
commandGroups: formData.commandGroup ? [`/command-groups/${formData.commandGroup}`] : null,
|
||||||
scope: formData.scope,
|
dateTime: dateTime,
|
||||||
notes: formData.notes || '',
|
notes: formData.notes || '',
|
||||||
|
clients: Array.from(this.selectedClientIds).map((uuid: string) => `/clients/${uuid}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (scope === 'clients') {
|
if (selectedCommands) {
|
||||||
payload.clients = this.selectedClients.map(client => client['@id'] ? client['@id'] : client.uuid);
|
payload.commands = selectedCommands;
|
||||||
payload.organizationalUnit = null;
|
|
||||||
} else {
|
|
||||||
let ou = formData.organizationalUnit;
|
|
||||||
if (typeof ou === 'number' || (typeof ou === 'string' && !ou.includes('/'))) {
|
|
||||||
const selectedUnit = this.availableOrganizationalUnits.find((u: any) => u.id === Number(ou));
|
|
||||||
ou = selectedUnit ? (selectedUnit['@id'] || `/organizational-units/${selectedUnit.uuid}`) : ou;
|
|
||||||
}
|
|
||||||
payload.organizationalUnit = ou;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
const taskId = this.data.task.uuid;
|
const taskId = this.data.task.uuid;
|
||||||
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
|
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastr.success(this.translate.instant('taskUpdatedSuccessfully'));
|
this.toastr.success('Tarea actualizada con éxito');
|
||||||
this.dialogRef.close(true);
|
this.dialogRef.close(true);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.toastr.error(this.translate.instant('errorUpdatingTask'));
|
this.toastr.error('Error al actualizar la tarea');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.http.post<any>(this.apiUrl, payload).subscribe({
|
this.http.post<any>(this.apiUrl, payload).subscribe({
|
||||||
next: response => {
|
next: () => {
|
||||||
this.toastr.success(this.translate.instant('taskCreatedSuccessfully'));
|
this.toastr.success('Tarea creada con éxito');
|
||||||
this.dialogRef.close(response);
|
this.dialogRef.close(true);
|
||||||
if (this.data?.source === 'assistant') {
|
|
||||||
if (formData.scheduleAfterCreate) {
|
|
||||||
this.openScheduleDialog(response);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.openPostCreationFlow(response, formData.scheduleAfterCreate);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.toastr.error(this.translate.instant('errorCreatingTask'));
|
this.toastr.error('Error al crear la tarea');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private openScheduleDialog(task: any): void {
|
combineDateAndTime(date: string, time: string): string {
|
||||||
this.dialog.open(CreateTaskScheduleComponent, {
|
const dateObj = new Date(date);
|
||||||
width: '800px',
|
const [hours, minutes] = time.split(':').map(Number);
|
||||||
data: { task: task }
|
dateObj.setHours(hours, minutes, 0);
|
||||||
}).afterClosed().subscribe(result => {
|
return dateObj.toISOString();
|
||||||
if (result) {
|
|
||||||
this.toastr.success(this.translate.instant('successfullyScheduledTask'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private openPostCreationFlow(task: any, scheduleAfterCreate: boolean): void {
|
|
||||||
const actionsDialog = this.dialog.open(ShowTaskScriptComponent, {
|
|
||||||
width: '85vw',
|
|
||||||
height: '85vh',
|
|
||||||
maxWidth: '85vw',
|
|
||||||
maxHeight: '85vh',
|
|
||||||
data: { commandTask: task }
|
|
||||||
});
|
|
||||||
|
|
||||||
actionsDialog.afterClosed().subscribe(() => {
|
|
||||||
if (scheduleAfterCreate) {
|
|
||||||
this.dialog.open(CreateTaskScheduleComponent, {
|
|
||||||
width: '800px',
|
|
||||||
data: { task: task }
|
|
||||||
}).afterClosed().subscribe(result => {
|
|
||||||
if (result) {
|
|
||||||
this.toastr.success(this.translate.instant('successfullyScheduledTask'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
this.dialogRef.close(false);
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,39 +8,48 @@
|
||||||
|
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<p><strong>{{ 'taskId' | translate }}:</strong> {{ task.uuid }}</p>
|
<p><strong>{{ 'taskId' | translate }}:</strong> {{ task.uuid }}</p>
|
||||||
|
<p><strong>{{ 'status' | translate }}:</strong> {{ task.status }}</p>
|
||||||
<p><strong>{{ 'creationDate' | translate }}:</strong> {{ task.createdAt | date: 'short' }}</p>
|
<p><strong>{{ 'creationDate' | translate }}:</strong> {{ task.createdAt | date: 'short' }}</p>
|
||||||
<p><strong>{{ 'labelNotes' | translate }}:</strong> {{ task.notes }}</p>
|
<p><strong>{{ 'notes' | translate }}:</strong> {{ task.notes }}</p>
|
||||||
|
|
||||||
<h3>{{ 'commandsToExecute' | translate }}</h3>
|
<h3>{{ 'includedCommandGroups' | translate }}</h3>
|
||||||
<table mat-table [dataSource]="task.commandTaskScripts || []" class="mat-elevation-z8" *ngIf="task.commandTaskScripts?.length > 0">
|
<table mat-table [dataSource]="task.commandGroups" class="mat-elevation-z8">
|
||||||
<ng-container matColumnDef="order">
|
<ng-container matColumnDef="name">
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'labelOrder' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef> {{ 'commandGroupColumn' | translate }} </th>
|
||||||
<td mat-cell *matCellDef="let script">{{ script.order }}</td>
|
<td mat-cell *matCellDef="let group"> {{ group.name }} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="uuid">
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'labelType' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||||
<td mat-cell *matCellDef="let script">{{ script.type }}</td>
|
<td mat-cell *matCellDef="let group"> {{ group.uuid }} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="content">
|
<tr mat-header-row *matHeaderRowDef="['name', 'uuid']"></tr>
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'labelContent' | translate }}</th>
|
<tr mat-row *matRowDef="let row; columns: ['name', 'uuid'];"></tr>
|
||||||
<td mat-cell *matCellDef="let script">
|
|
||||||
<span *ngIf="script.content">{{ script.content }}</span>
|
|
||||||
<span *ngIf="!script.content && script.parameters">
|
|
||||||
{{ 'configuredParameters' | translate }}</span>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="['order', 'type', 'content']"></tr>
|
|
||||||
<tr mat-row *matRowDef="let row; columns: ['order', 'type', 'content'];"></tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p *ngIf="!task.commandTaskScripts?.length">{{ 'noActionsConfigured' | translate }}</p>
|
<h3>{{ 'commandsToExecute' | translate }}</h3>
|
||||||
|
<div *ngFor="let group of task.commandGroups">
|
||||||
|
<p><strong>{{ 'group' | translate }}: </strong>{{ group.name }}</p>
|
||||||
|
<table mat-table [dataSource]="group.commands" class="mat-elevation-z8">
|
||||||
|
<ng-container matColumnDef="commandName">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> {{ 'commandColumn' | translate }} </th>
|
||||||
|
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="commandUuid">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||||
|
<td mat-cell *matCellDef="let command"> {{ command.uuid }} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="['commandName', 'commandUuid']"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: ['commandName', 'commandUuid'];"></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<div class="task-actions">
|
<div class="task-actions">
|
||||||
<button class="ordinary-button" (click)="closeDialog()">{{ 'buttonCancel' | translate }}</button>
|
<button class="ordinary-button" (click)="closeDialog()">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,404 +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 {
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-header-cell {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #495057;
|
|
||||||
padding: 16px 8px;
|
|
||||||
border-bottom: 2px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-cell {
|
|
||||||
padding: 16px 8px;
|
|
||||||
border-bottom: 1px solid #f1f3f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-type-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-chip {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
min-width: 200px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-icon {
|
|
||||||
margin-right: 12px;
|
|
||||||
font-size: 20px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-label {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-details {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.8;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Colores para diferentes tipos de recurrencia */
|
|
||||||
.recurrence-none {
|
|
||||||
background-color: #667eea;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-daily {
|
|
||||||
background-color: #f093fb;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-weekly {
|
|
||||||
background-color: #4facfe;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-monthly {
|
|
||||||
background-color: #43e97b;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-yearly {
|
|
||||||
background-color: #fa709a;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-custom {
|
|
||||||
background-color: #ff9a9e;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el campo executionTime */
|
|
||||||
.time-display {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background-color: #667eea;
|
|
||||||
color: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-value {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el campo nextExecution */
|
|
||||||
.next-execution {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background-color: #4facfe;
|
|
||||||
color: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.execution-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el campo daysOfWeek */
|
|
||||||
.days-display {
|
|
||||||
}
|
|
||||||
|
|
||||||
.days-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #495057;
|
|
||||||
}
|
|
||||||
|
|
||||||
.days-icon {
|
|
||||||
margin-right: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: #4facfe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.days-count {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.days-chips {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-chip {
|
|
||||||
background-color: #4facfe;
|
|
||||||
color: white;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
min-width: 32px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el campo months */
|
|
||||||
.months-display {
|
|
||||||
}
|
|
||||||
|
|
||||||
.months-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #495057;
|
|
||||||
}
|
|
||||||
|
|
||||||
.months-icon {
|
|
||||||
margin-right: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: #43e97b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.months-count {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.months-chips {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.month-chip {
|
|
||||||
background-color: #43e97b;
|
|
||||||
color: white;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
min-width: 32px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el campo enabled */
|
|
||||||
.status-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-active {
|
|
||||||
background-color: #43e97b;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-inactive {
|
|
||||||
background-color: #ff9a9e;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para datos no definidos */
|
|
||||||
.no-data {
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-data-text {
|
|
||||||
color: #6c757d;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el header de acciones */
|
|
||||||
.header-actions {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button mat-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el contenedor de búsqueda */
|
|
||||||
.search-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-string {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el paginador */
|
|
||||||
.paginator-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para los botones de acción en la tabla */
|
|
||||||
.mat-column-actions .mat-cell {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-column-actions button {
|
|
||||||
margin: 0 4px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-column-actions button:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.recurrence-chip {
|
|
||||||
min-width: 160px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-label {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrence-details {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-string {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
<app-loading [isLoading]="loading"></app-loading>
|
|
||||||
|
|
||||||
<h2 mat-dialog-title>{{ 'manageTaskSchedules' | translate }} {{ data.commandTask?.name }}</h2>
|
|
||||||
|
|
||||||
<mat-dialog-content>
|
|
||||||
<div class="header-actions">
|
|
||||||
<button (click)="createNewSchedule()" class="action-button">
|
|
||||||
<mat-icon>add</mat-icon>
|
|
||||||
{{ 'newSchedule' | translate }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-container">
|
|
||||||
<mat-form-field appearance="fill" class="search-string">
|
|
||||||
<mat-label>{{ 'labelSearchSchedule' | translate }}</mat-label>
|
|
||||||
<input matInput [placeholder]="'searchPlaceholder' | translate" [(ngModel)]="filters['name']"
|
|
||||||
(keyup.enter)="loadData()">
|
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
|
||||||
<button *ngIf="filters['name']" mat-icon-button matSuffix (click)="filters['name'] = ''; loadData()">
|
|
||||||
<mat-icon>close</mat-icon>
|
|
||||||
</button>
|
|
||||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<app-loading [isLoading]="loading"></app-loading>
|
|
||||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
|
||||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
|
||||||
<td mat-cell *matCellDef="let schedule">
|
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'recurrenceType'">
|
|
||||||
<div class="recurrence-type-container">
|
|
||||||
<div class="recurrence-chip" [ngClass]="'recurrence-' + column.cell(schedule).type">
|
|
||||||
<mat-icon class="recurrence-icon">{{ column.cell(schedule).icon }}</mat-icon>
|
|
||||||
<div class="recurrence-content">
|
|
||||||
<div class="recurrence-label">{{ column.cell(schedule).label }}</div>
|
|
||||||
<div class="recurrence-details">{{ column.cell(schedule).details }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'executionTime'">
|
|
||||||
<div class="time-display">
|
|
||||||
<mat-icon class="time-icon">schedule</mat-icon>
|
|
||||||
<span class="time-value">{{ column.cell(schedule) }}</span>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'nextExecution'">
|
|
||||||
<div class="next-execution">
|
|
||||||
<mat-icon class="execution-icon">schedule</mat-icon>
|
|
||||||
<span>{{ column.cell(schedule) || ('notCalculated' | translate) }}</span>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'daysOfWeek'">
|
|
||||||
<div class="days-display" *ngIf="column.cell(schedule)">
|
|
||||||
<div class="days-chips">
|
|
||||||
<span *ngFor="let day of column.cell(schedule).days"
|
|
||||||
class="day-chip">{{ day }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="no-data" *ngIf="!column.cell(schedule)">
|
|
||||||
<span class="no-data-text">{{ 'notDefined' | translate }}</span>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'months'">
|
|
||||||
<div class="months-display" *ngIf="column.cell(schedule)">
|
|
||||||
<div class="months-chips">
|
|
||||||
<span *ngFor="let month of column.cell(schedule).months"
|
|
||||||
class="month-chip">{{ month }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="no-data" *ngIf="!column.cell(schedule)">
|
|
||||||
<span class="no-data-text">{{ 'notDefined' | translate }}</span>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef !== 'recurrenceType' && column.columnDef !== 'executionTime' &&
|
|
||||||
column.columnDef !== 'daysOfWeek' && column.columnDef !== 'months' &&
|
|
||||||
column.columnDef !== 'nextExecution' && column.columnDef !== 'enabled'">
|
|
||||||
{{ column.cell(schedule) }}
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'enabled'">
|
|
||||||
<div class="status-container">
|
|
||||||
<div class="status-indicator" [ngClass]="schedule.enabled ? 'status-active' : 'status-inactive'">
|
|
||||||
<mat-icon class="status-icon">{{ schedule.enabled ? 'check_circle' : 'cancel' }}</mat-icon>
|
|
||||||
<span class="status-text">{{ (schedule.enabled ? 'active' : 'inactive') | translate }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
|
||||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
|
||||||
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
|
|
||||||
<button mat-icon-button color="primary" (click)="editSchedule(schedule)" [matTooltip]="'editScheduleTooltip' | translate">
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button mat-icon-button color="warn" (click)="deleteSchedule(schedule)" [matTooltip]="'deleteScheduleTooltip' | translate">
|
|
||||||
<mat-icon>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">
|
|
||||||
<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()">{{ 'buttonClose' | translate }}</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,419 +0,0 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
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{
|
|
||||||
public translate = inject(TranslateService);
|
|
||||||
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: 'labelId', cell: (schedule: any) => schedule.id },
|
|
||||||
{ columnDef: 'recurrenceType', header: 'typeOfProgramming', cell: (schedule: any) => this.getRecurrenceTypeDisplay(schedule) },
|
|
||||||
{ columnDef: 'executionTime', header: 'executionTime', cell: (schedule: any) => this.formatExecutionTimeForDisplay(schedule.executionTime) },
|
|
||||||
{ columnDef: 'nextExecution', header: 'nextExecution', cell: (schedule: any) => this.calculateNextExecution(schedule) },
|
|
||||||
{ columnDef: 'daysOfWeek', header: 'daysOfTheWeek', cell: (schedule: any) => this.formatDaysOfWeek(schedule.recurrenceDetails?.daysOfWeek) },
|
|
||||||
{ columnDef: 'months', header: 'labelMonths', cell: (schedule: any) => this.formatMonths(schedule.recurrenceDetails?.months) },
|
|
||||||
{ columnDef: 'enabled', header: 'state', cell: (schedule: any) => schedule.enabled }
|
|
||||||
];
|
|
||||||
|
|
||||||
displayedColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions'];
|
|
||||||
allColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions'];
|
|
||||||
compactColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', '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.updateDisplayedColumns();
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
this.loading = false;
|
|
||||||
this.toastService.error(this.translate.instant('errorLoadingProgramming'));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createNewSchedule(): void {
|
|
||||||
this.dialog.open(CreateTaskScheduleComponent, {
|
|
||||||
width: '800px',
|
|
||||||
data: { task: this.data.commandTask }
|
|
||||||
}).afterClosed().subscribe(result => {
|
|
||||||
if (result) {
|
|
||||||
this.loadData();
|
|
||||||
this.toastService.success(this.translate.instant('programmingCreatedCorrectly'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
editSchedule(schedule: any): void {
|
|
||||||
this.dialog.open(CreateTaskScheduleComponent, {
|
|
||||||
width: '800px',
|
|
||||||
data: { schedule: schedule, task: this.data.commandTask }
|
|
||||||
}).afterClosed().subscribe(result => {
|
|
||||||
if (result) {
|
|
||||||
this.loadData();
|
|
||||||
this.toastService.success(this.translate.instant('programmingUpdatedCorrectly'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSchedule(schedule: any): void {
|
|
||||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
|
||||||
width: '300px',
|
|
||||||
data: { name: 'programación de tarea' }
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
|
||||||
if (result) {
|
|
||||||
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
|
|
||||||
() => {
|
|
||||||
this.toastService.success(this.translate.instant('programmingRemovedCorrectly'));
|
|
||||||
this.loadData();
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
this.toastService.error(error.error['hydra:description'] || 'Error al eliminar la programación');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateNextExecution(schedule: any): string {
|
|
||||||
if (!schedule.enabled) {
|
|
||||||
return 'Deshabilitada';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (schedule.recurrenceType === 'none') {
|
|
||||||
if (schedule.executionDate && schedule.executionTime) {
|
|
||||||
const executionDate = new Date(schedule.executionDate);
|
|
||||||
|
|
||||||
const executionTimeDate = new Date(schedule.executionTime);
|
|
||||||
const utcHours = executionTimeDate.getUTCHours();
|
|
||||||
const utcMinutes = executionTimeDate.getUTCMinutes();
|
|
||||||
|
|
||||||
const tempDate = new Date();
|
|
||||||
tempDate.setUTCHours(utcHours, utcMinutes, 0, 0);
|
|
||||||
const localHours = tempDate.getHours();
|
|
||||||
const localMinutes = tempDate.getMinutes();
|
|
||||||
|
|
||||||
const fullExecutionDateTime = new Date(
|
|
||||||
executionDate.getFullYear(),
|
|
||||||
executionDate.getMonth(),
|
|
||||||
executionDate.getDate(),
|
|
||||||
localHours,
|
|
||||||
localMinutes,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
if (fullExecutionDateTime < now) {
|
|
||||||
return 'Ya ejecutada';
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.datePipe.transform(fullExecutionDateTime, 'dd/MM/yyyy HH:mm') || 'Fecha inválida';
|
|
||||||
}
|
|
||||||
return 'Sin fecha/hora definida';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schedule.recurrenceDetails) {
|
|
||||||
const startDate = schedule.recurrenceDetails.initDate ? new Date(schedule.recurrenceDetails.initDate) : null;
|
|
||||||
const endDate = schedule.recurrenceDetails.endDate ? new Date(schedule.recurrenceDetails.endDate) : null;
|
|
||||||
const days = Array.isArray(schedule.recurrenceDetails.daysOfWeek) ? schedule.recurrenceDetails.daysOfWeek : [];
|
|
||||||
const months = Array.isArray(schedule.recurrenceDetails.months) ? schedule.recurrenceDetails.months : [];
|
|
||||||
|
|
||||||
if (!startDate || !endDate || days.length === 0 || months.length === 0) {
|
|
||||||
return 'Configuración incompleta';
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedDayIndices = new Set<number>(days.map((d: string) => this.getDayIndexFromName(d)));
|
|
||||||
const selectedMonthIndices = new Set<number>(months.map((m: string) => this.getMonthIndexFromName(m)));
|
|
||||||
|
|
||||||
const executionTimeDate = new Date(schedule.executionTime);
|
|
||||||
const utcHours = executionTimeDate.getUTCHours();
|
|
||||||
const utcMinutes = executionTimeDate.getUTCMinutes();
|
|
||||||
|
|
||||||
const tempDate = new Date();
|
|
||||||
tempDate.setUTCHours(utcHours, utcMinutes, 0, 0);
|
|
||||||
const hours = tempDate.getHours();
|
|
||||||
const minutes = tempDate.getMinutes();
|
|
||||||
|
|
||||||
const next = this.findNextExecutionDate(startDate, endDate, hours, minutes, selectedDayIndices, selectedMonthIndices);
|
|
||||||
if (next) {
|
|
||||||
return this.datePipe.transform(next, 'dd/MM/yyyy HH:mm') || 'Fecha inválida';
|
|
||||||
}
|
|
||||||
return 'No hay próximas ejecuciones';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Configuración incompleta';
|
|
||||||
} catch (error) {
|
|
||||||
return 'Error en cálculo';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatExecutionTimeForDisplay(time: string | Date): string {
|
|
||||||
if (typeof time === 'string') {
|
|
||||||
const hhmmMatch = time.match(/^\d{2}:\d{2}/);
|
|
||||||
if (hhmmMatch) {
|
|
||||||
return hhmmMatch[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = new Date(time);
|
|
||||||
if (!isNaN(parsed.getTime())) {
|
|
||||||
const utcHours = parsed.getUTCHours();
|
|
||||||
const utcMinutes = parsed.getUTCMinutes();
|
|
||||||
|
|
||||||
const currentDate = new Date();
|
|
||||||
currentDate.setUTCHours(utcHours, utcMinutes, 0, 0);
|
|
||||||
|
|
||||||
const hours = String(currentDate.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(currentDate.getMinutes()).padStart(2, '0');
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time instanceof Date && !isNaN(time.getTime())) {
|
|
||||||
const hours = String(time.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(time.getMinutes()).padStart(2, '0');
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseExecutionTime(executionTime: any): [number, number] {
|
|
||||||
if (typeof executionTime === 'string') {
|
|
||||||
const match = executionTime.match(/^(\d{2}):(\d{2})/);
|
|
||||||
if (match) {
|
|
||||||
return [parseInt(match[1], 10), parseInt(match[2], 10)];
|
|
||||||
}
|
|
||||||
const parsed = new Date(executionTime);
|
|
||||||
if (!isNaN(parsed.getTime())) {
|
|
||||||
const utcHours = parsed.getUTCHours();
|
|
||||||
const utcMinutes = parsed.getUTCMinutes();
|
|
||||||
|
|
||||||
const currentDate = new Date();
|
|
||||||
currentDate.setUTCHours(utcHours, utcMinutes, 0, 0);
|
|
||||||
|
|
||||||
return [currentDate.getHours(), currentDate.getMinutes()];
|
|
||||||
}
|
|
||||||
return [0, 0];
|
|
||||||
}
|
|
||||||
if (executionTime instanceof Date && !isNaN(executionTime.getTime())) {
|
|
||||||
return [executionTime.getHours(), executionTime.getMinutes()];
|
|
||||||
}
|
|
||||||
return [0, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private findNextExecutionDate(startDate: Date, endDate: Date, hours: number, minutes: number, selectedDays: Set<number>, selectedMonths: Set<number>): Date | null {
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const startCandidate = new Date(Math.max(new Date(startDate).setHours(0, 0, 0, 0), new Date(now).setHours(0, 0, 0, 0)));
|
|
||||||
const endLimit = new Date(endDate);
|
|
||||||
endLimit.setHours(23, 59, 59, 999);
|
|
||||||
|
|
||||||
const cursor = new Date(startCandidate);
|
|
||||||
while (cursor <= endLimit) {
|
|
||||||
if (selectedMonths.has(cursor.getMonth()) && selectedDays.has(cursor.getDay())) {
|
|
||||||
const candidateDateTime = new Date(cursor);
|
|
||||||
candidateDateTime.setHours(hours, minutes, 0, 0);
|
|
||||||
if (candidateDateTime >= now) {
|
|
||||||
return candidateDateTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cursor.setDate(cursor.getDate() + 1);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDayIndexFromName(dayName: string): number {
|
|
||||||
const mapping: { [key: string]: number } = {
|
|
||||||
sunday: 0,
|
|
||||||
monday: 1,
|
|
||||||
tuesday: 2,
|
|
||||||
wednesday: 3,
|
|
||||||
thursday: 4,
|
|
||||||
friday: 5,
|
|
||||||
saturday: 6
|
|
||||||
};
|
|
||||||
return mapping[dayName?.toLowerCase?.()] ?? -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMonthIndexFromName(monthName: string): number {
|
|
||||||
const mapping: { [key: string]: number } = {
|
|
||||||
january: 0,
|
|
||||||
february: 1,
|
|
||||||
march: 2,
|
|
||||||
april: 3,
|
|
||||||
may: 4,
|
|
||||||
june: 5,
|
|
||||||
july: 6,
|
|
||||||
august: 7,
|
|
||||||
september: 8,
|
|
||||||
october: 9,
|
|
||||||
november: 10,
|
|
||||||
december: 11
|
|
||||||
};
|
|
||||||
return mapping[monthName?.toLowerCase?.()] ?? -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDisplayedColumns(): void {
|
|
||||||
const hasRecurrent = this.dataSource.data.some((s: any) => s.recurrenceType !== 'none');
|
|
||||||
this.displayedColumns = hasRecurrent ? this.allColumns : this.compactColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageChange(event: any): void {
|
|
||||||
this.page = event.pageIndex;
|
|
||||||
this.itemsPerPage = event.pageSize;
|
|
||||||
this.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
onNoClick(): void {
|
|
||||||
this.dialogRef.close(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecurrenceTypeDisplay(schedule: any): any {
|
|
||||||
if (schedule.recurrenceType === 'none') {
|
|
||||||
return {
|
|
||||||
type: 'none',
|
|
||||||
label : this.translate.instant('uniqueExecution'),
|
|
||||||
icon: 'event',
|
|
||||||
color: 'primary',
|
|
||||||
details: schedule.executionDate ?
|
|
||||||
`Fecha: ${this.datePipe.transform(schedule.executionDate, 'dd/MM/yyyy')}` :
|
|
||||||
'Sin fecha definida'
|
|
||||||
};
|
|
||||||
} else if (schedule.recurrenceType === 'daily') {
|
|
||||||
return {
|
|
||||||
type: 'daily',
|
|
||||||
label: 'Diaria',
|
|
||||||
icon: 'repeat',
|
|
||||||
color: 'accent',
|
|
||||||
details: 'Todos los días'
|
|
||||||
};
|
|
||||||
} else if (schedule.recurrenceType === 'weekly') {
|
|
||||||
return {
|
|
||||||
type: 'weekly',
|
|
||||||
label: 'Semanal',
|
|
||||||
icon: 'view_week',
|
|
||||||
color: 'accent',
|
|
||||||
details: schedule.recurrenceDetails?.daysOfWeek?.length > 0 ?
|
|
||||||
`Días: ${schedule.recurrenceDetails.daysOfWeek.join(', ')}` :
|
|
||||||
'Todos los días'
|
|
||||||
};
|
|
||||||
} else if (schedule.recurrenceType === 'monthly') {
|
|
||||||
return {
|
|
||||||
type: 'monthly',
|
|
||||||
label: 'Mensual',
|
|
||||||
icon: 'calendar_month',
|
|
||||||
color: 'accent',
|
|
||||||
details: schedule.recurrenceDetails?.months?.length > 0 ?
|
|
||||||
`Meses: ${schedule.recurrenceDetails.months.join(', ')}` :
|
|
||||||
'Todos los meses'
|
|
||||||
};
|
|
||||||
} else if (schedule.recurrenceType === 'yearly') {
|
|
||||||
return {
|
|
||||||
type: 'yearly',
|
|
||||||
label: 'Anual',
|
|
||||||
icon: 'event_note',
|
|
||||||
color: 'accent',
|
|
||||||
details: 'Una vez al año'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
type: 'custom',
|
|
||||||
label: 'Personalizada',
|
|
||||||
icon: 'settings',
|
|
||||||
color: 'warn',
|
|
||||||
details: 'Configuración especial'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDaysOfWeek(days: string[]): any {
|
|
||||||
if (!days || days.length === 0) return null;
|
|
||||||
|
|
||||||
const dayMap: { [key: string]: string } = {
|
|
||||||
'monday': 'Lun',
|
|
||||||
'tuesday': 'Mar',
|
|
||||||
'wednesday': 'Mié',
|
|
||||||
'thursday': 'Jue',
|
|
||||||
'friday': 'Vie',
|
|
||||||
'saturday': 'Sáb',
|
|
||||||
'sunday': 'Dom'
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
days: days.map(day => dayMap[day.toLowerCase()] || day),
|
|
||||||
count: days.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
formatMonths(months: string[]): any {
|
|
||||||
if (!months || months.length === 0) return null;
|
|
||||||
|
|
||||||
const monthMap: { [key: string]: string } = {
|
|
||||||
'january': 'Ene',
|
|
||||||
'february': 'Feb',
|
|
||||||
'march': 'Mar',
|
|
||||||
'april': 'Abr',
|
|
||||||
'may': 'May',
|
|
||||||
'june': 'Jun',
|
|
||||||
'july': 'Jul',
|
|
||||||
'august': 'Ago',
|
|
||||||
'september': 'Sep',
|
|
||||||
'october': 'Oct',
|
|
||||||
'november': 'Nov',
|
|
||||||
'december': 'Dic'
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
months: months.map(month => monthMap[month.toLowerCase()] || month),
|
|
||||||
count: months.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,414 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el header de acciones */
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 16px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 24px;
|
|
||||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
|
|
||||||
background: linear-gradient(135deg, #45a049 0%, #3d8b40 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button mat-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el contenido del script */
|
|
||||||
.script-content {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-content:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
border-color: #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
|
||||||
color: white;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-title {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-body {
|
|
||||||
padding: 16px;
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #333;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el botón de parámetros */
|
|
||||||
.parameters-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 2px 6px rgba(33, 150, 243, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameters-button:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
|
|
||||||
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameters-button mat-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameters-button span {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mejoras en la tabla */
|
|
||||||
.mat-table {
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-header-cell {
|
|
||||||
background: #f5f5f5;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-row:hover {
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.search-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-string {
|
|
||||||
min-width: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-body {
|
|
||||||
max-height: 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para los chips de tipo */
|
|
||||||
.type-chip {
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-script {
|
|
||||||
background: #e8f5e8;
|
|
||||||
color: #2e7d32;
|
|
||||||
border: 1px solid #c8e6c9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-command {
|
|
||||||
background: #fff3e0;
|
|
||||||
color: #f57c00;
|
|
||||||
border: 1px solid #ffcc80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-python {
|
|
||||||
background: #e3f2fd;
|
|
||||||
color: #1976d2;
|
|
||||||
border: 1px solid #bbdefb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-bash {
|
|
||||||
background: #f3e5f5;
|
|
||||||
color: #7b1fa2;
|
|
||||||
border: 1px solid #e1bee7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-default {
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #616161;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Badge de paso */
|
|
||||||
.step-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-number {
|
|
||||||
background-color: #3f51b5;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 15px;
|
|
||||||
box-shadow: 0 2px 4px rgba(63, 81, 181, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Display de tipo de acción */
|
|
||||||
.type-display {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vista previa del script en celda (una sola línea, altura de fila fija) */
|
|
||||||
.script-preview-cell {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
max-width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-preview-text {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-family: 'Monaco', 'Menlo', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-content-hint {
|
|
||||||
color: #999;
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hint secuencial */
|
|
||||||
.sequential-hint {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 24px;
|
|
||||||
color: #1565c0;
|
|
||||||
background: #e3f2fd;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0 24px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sequential-hint mat-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Timeline resumen */
|
|
||||||
.timeline-summary {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px 24px;
|
|
||||||
background: #fafafa;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 14px;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #d0d0d0;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-number {
|
|
||||||
background: #3f51b5;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-label {
|
|
||||||
color: #444;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-arrow {
|
|
||||||
color: #9e9e9e;
|
|
||||||
font-size: 20px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
<app-loading [isLoading]="loading"></app-loading>
|
|
||||||
|
|
||||||
<h2 mat-dialog-title>{{ 'taskActionsTitle' | translate }}: {{ data.commandTask?.name }}</h2>
|
|
||||||
|
|
||||||
<div *ngIf="dataSource.data.length > 1" class="sequential-hint">
|
|
||||||
<mat-icon>info</mat-icon>
|
|
||||||
<span>{{ 'taskActionsSequentialHint' | translate }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<mat-dialog-content>
|
|
||||||
<div class="header-actions">
|
|
||||||
<button (click)="createNewScript()" class="action-button">
|
|
||||||
<mat-icon>add</mat-icon>
|
|
||||||
{{ 'newAction' | translate }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="dataSource.data.length > 0 && !loading" class="timeline-summary">
|
|
||||||
<ng-container *ngFor="let item of dataSource.data; let last = last">
|
|
||||||
<div class="timeline-step">
|
|
||||||
<span class="timeline-number">{{ item.order }}</span>
|
|
||||||
<span class="timeline-label">{{ getTypeDisplay(item.type).label }}</span>
|
|
||||||
</div>
|
|
||||||
<mat-icon *ngIf="!last" class="timeline-arrow">arrow_forward</mat-icon>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<app-loading [isLoading]="loading"></app-loading>
|
|
||||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
|
||||||
|
|
||||||
<ng-container matColumnDef="order">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'executionOrder' | translate }}</th>
|
|
||||||
<td mat-cell *matCellDef="let item">
|
|
||||||
<div class="step-badge">
|
|
||||||
<span class="step-number">{{ item.order }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="type">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'actionType' | translate }}</th>
|
|
||||||
<td mat-cell *matCellDef="let item">
|
|
||||||
<div class="type-display">
|
|
||||||
<mat-icon class="type-icon">{{ getTypeDisplay(item.type).icon }}</mat-icon>
|
|
||||||
<span>{{ getTypeDisplay(item.type).label }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="content">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'labelScript' | translate }}</th>
|
|
||||||
<td mat-cell *matCellDef="let item">
|
|
||||||
<div *ngIf="item.content" class="script-preview-cell">
|
|
||||||
<span class="script-preview-text">{{ item.content }}</span>
|
|
||||||
<button mat-icon-button (click)="openScriptModal(item.content)" [matTooltip]="'viewScript' | translate">
|
|
||||||
<mat-icon>visibility</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span *ngIf="!item.content" class="no-content-hint">
|
|
||||||
({{ 'viewParameters' | translate }})
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="parameters">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'viewParameters' | translate }}</th>
|
|
||||||
<td mat-cell *matCellDef="let item">
|
|
||||||
<button class="parameters-button" (click)="openParametersModal(item.parameters)"
|
|
||||||
*ngIf="item.parameters && (item.parameters | json) !== '{}'">
|
|
||||||
<mat-icon>settings</mat-icon>
|
|
||||||
<span>{{ 'viewParameters' | translate }}</span>
|
|
||||||
</button>
|
|
||||||
<span *ngIf="!item.parameters || (item.parameters | json) === '{}'" class="no-content-hint">—</span>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
|
||||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
|
||||||
<td mat-cell *matCellDef="let item" style="text-align: center;">
|
|
||||||
<button mat-icon-button color="warn" (click)="deleteTaskScript(item)">
|
|
||||||
<mat-icon>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">
|
|
||||||
<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()">{{ 'buttonClose' | translate }}</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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue