Compare commits
No commits in common. "main" and "feature-ux" have entirely different histories.
main
...
feature-ux
449
CHANGELOG.md
449
CHANGELOG.md
|
|
@ -1,452 +1,4 @@
|
|||
# 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`.
|
||||
|
|
@ -802,4 +354,3 @@
|
|||
### Fixed
|
||||
- Resolved an issue that prevented editing software profiles correctly.
|
||||
- Fixed a bug where newly created commands failed to execute in the commands section.
|
||||
|
||||
|
|
|
|||
|
|
@ -61,19 +61,6 @@ pipeline {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -110,38 +97,20 @@ pipeline {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// Solo lanzar cuando el build sea exitoso y en la rama main
|
||||
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}")
|
||||
]
|
||||
build job: 'Aptly publish nightly repository',
|
||||
wait: false,
|
||||
parameters: [
|
||||
string(name: 'TRIGGERED_BY', value: "${env.JOB_NAME}-${env.BUILD_NUMBER}")
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
always {
|
||||
notifyBuildStatus('opengnsys@qindel.com')
|
||||
|
|
|
|||
|
|
@ -22,19 +22,11 @@ restore_config_if_modified() {
|
|||
|
||||
if [ -f "$backup" ]; then
|
||||
if ! cmp -s "$new" "$backup"; then
|
||||
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
|
||||
echo ">>> Archivo modificado por el usuario detectado en $new"
|
||||
echo " - Guardando archivo nuevo como ${new}.new"
|
||||
mv -f "$new" "${new}.new"
|
||||
echo " - Restaurando archivo anterior desde backup"
|
||||
mv -f "$backup" "$new"
|
||||
else
|
||||
echo ">>> El archivo $new no ha cambiado desde la última versión, eliminando backup"
|
||||
rm -f "$backup"
|
||||
|
|
|
|||
|
|
@ -43,5 +43,3 @@ Thumbs.db
|
|||
|
||||
test-results/
|
||||
|
||||
.e2e-state.json
|
||||
playwright-report/
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"version": "1.14.0",
|
||||
"version": "0.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "og-webconsole",
|
||||
"version": "1.14.0",
|
||||
"version": "0.5.0",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^18.0.0",
|
||||
"@angular/cdk": "~18.0.0",
|
||||
|
|
@ -35,7 +35,6 @@
|
|||
"@angular/compiler-cli": "^18.0.0",
|
||||
"@angular/localize": "^18.1.0",
|
||||
"@ngx-env/builder": "^18.0.1",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"jasmine-core": "~5.1.0",
|
||||
|
|
@ -273,7 +272,6 @@
|
|||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.1.0.tgz",
|
||||
"integrity": "sha512-K0BhvZ/SIVoGXZVuh1KOJDdgcGlHfFGMGrs58utndndAb+gYXReMfz4GR5cQs2OObH6TKmIOY2EH7Og1CY2tsw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
|
@ -358,7 +356,6 @@
|
|||
"version": "18.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.0.6.tgz",
|
||||
"integrity": "sha512-9CmlQ8uZ0cHuj+1jDdabbFw90QpjrJzh1/cgeHN/8vAGE4ranxX3l6cqpVxuoWRLkB2ykTHt2xeoTM8y0d9ERA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
|
@ -408,7 +405,6 @@
|
|||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-18.1.0.tgz",
|
||||
"integrity": "sha512-noHDLarQSCZZh7hyNd0HR61Fut+q4QCVq9qc/jKPglfbV/6nPujQSmSpT+rNJlNuBOrCLuvH/CNBNbiqii+x3g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
|
@ -424,7 +420,6 @@
|
|||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.1.0.tgz",
|
||||
"integrity": "sha512-JRQzVTeJGSfRLY+dx+gwu/hPQVB8K+5pW12Z42M9x/HBgGW4in0cO2zHkeQPvImqm0nak82Us1Hyf5C+qTlMMQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
|
@ -445,7 +440,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.1.0.tgz",
|
||||
"integrity": "sha512-BBsogLPJwxkPh7f8RVHsxyyqNE8XpHbAanjB5fAwnU4W6Sw1kR5rFzkeZM3xaRm2MDiC8DovIl6hlf+s/mKYOw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
|
|
@ -473,7 +467,6 @@
|
|||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-18.1.0.tgz",
|
||||
"integrity": "sha512-/57/s7CD/0CwlN+3FlhVmx7ypCWXjKi5UKtnlBAUg0D1denIf6ADxwTHFZABYZcYBqOTJgeQUtUw9u/A+0CIlg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
|
@ -489,7 +482,6 @@
|
|||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.1.0.tgz",
|
||||
"integrity": "sha512-m+7m9wa+n5dEacd458eSZsZTz0B+HbOtr7/uqM0YTMQaPrhwl1epG5Y103mB6yr00JiJcLNlPLjP888cHFjldQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
|
@ -508,7 +500,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.1.0.tgz",
|
||||
"integrity": "sha512-84D06p2Th5NxoJZzsSIn4FkTJGImj7rtNnvyTrHvHdomzzUKwiBOXDB2FiCLDstND0DsCtgjD/uBJivg77z9tg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@types/babel__core": "7.20.5",
|
||||
|
|
@ -597,7 +588,6 @@
|
|||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.1.0.tgz",
|
||||
"integrity": "sha512-jCmxthiI4Zef54crckNht60xwfIsuciGeyZvb7SsXna2maLW9fA4uz1VhZqIWTiBnHwNynVlyfBX3/jBD7S9+g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
|
@ -619,7 +609,6 @@
|
|||
"version": "18.1.0",
|
||||
"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==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
|
@ -677,7 +666,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz",
|
||||
"integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
|
|
@ -3229,7 +3217,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.0.7.tgz",
|
||||
"integrity": "sha512-GFcigCxJTKCH3aECzMIu4FhgLJWnFvMXzpI4CCSoELWFtkOOU2P+goYA61+OKpGrB8fPE7q6n8zAXBSlZRrHjQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^2.3.7",
|
||||
"@inquirer/confirm": "^3.1.11",
|
||||
|
|
@ -4531,6 +4518,7 @@
|
|||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4547,6 +4535,7 @@
|
|||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4563,6 +4552,7 @@
|
|||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4579,6 +4569,7 @@
|
|||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4595,6 +4586,7 @@
|
|||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4611,6 +4603,7 @@
|
|||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4627,6 +4620,7 @@
|
|||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4643,6 +4637,7 @@
|
|||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4659,6 +4654,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4675,6 +4671,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4691,6 +4688,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4707,6 +4705,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4723,6 +4722,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4739,6 +4739,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4755,6 +4756,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4771,6 +4773,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4787,6 +4790,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4803,6 +4807,7 @@
|
|||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4819,6 +4824,7 @@
|
|||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4835,6 +4841,7 @@
|
|||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4851,6 +4858,7 @@
|
|||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4867,6 +4875,7 @@
|
|||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -4883,6 +4892,7 @@
|
|||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -5296,22 +5306,6 @@
|
|||
"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": {
|
||||
"version": "4.18.0",
|
||||
"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",
|
||||
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
|
|
@ -6195,7 +6188,6 @@
|
|||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -6269,7 +6261,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz",
|
||||
"integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
|
|
@ -6675,7 +6666,6 @@
|
|||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001640",
|
||||
"electron-to-chromium": "^1.4.820",
|
||||
|
|
@ -7735,7 +7725,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -9821,8 +9810,7 @@
|
|||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
|
||||
"integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "27.5.1",
|
||||
|
|
@ -9971,7 +9959,6 @@
|
|||
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz",
|
||||
"integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"body-parser": "^1.19.0",
|
||||
|
|
@ -10272,7 +10259,6 @@
|
|||
"resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
|
||||
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"copy-anything": "^2.0.1",
|
||||
"parse-node-version": "^1.0.1",
|
||||
|
|
@ -12202,53 +12188,6 @@
|
|||
"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": {
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
|
|
@ -12268,7 +12207,6 @@
|
|||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
|
|
@ -12885,7 +12823,6 @@
|
|||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
|
|
@ -13866,7 +13803,6 @@
|
|||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz",
|
||||
"integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
|
|
@ -13919,7 +13855,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
|
@ -14052,8 +13987,7 @@
|
|||
"node_modules/tslib": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
||||
"peer": true
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
},
|
||||
"node_modules/tuf-js": {
|
||||
"version": "2.2.1",
|
||||
|
|
@ -14105,7 +14039,6 @@
|
|||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -14351,7 +14284,6 @@
|
|||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz",
|
||||
"integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.38",
|
||||
|
|
@ -14453,7 +14385,6 @@
|
|||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz",
|
||||
"integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
"@types/estree": "^1.0.5",
|
||||
|
|
@ -14530,7 +14461,6 @@
|
|||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz",
|
||||
"integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/bonjour": "^3.5.13",
|
||||
"@types/connect-history-api-fallback": "^1.5.4",
|
||||
|
|
@ -14720,7 +14650,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
|
@ -15102,8 +15031,7 @@
|
|||
"node_modules/zone.js": {
|
||||
"version": "0.14.7",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.7.tgz",
|
||||
"integrity": "sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg==",
|
||||
"peer": true
|
||||
"integrity": "sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
{
|
||||
"name": "og-webconsole",
|
||||
"version": "1.26.0",
|
||||
"version": "0.5.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"e2e": "playwright test",
|
||||
"e2e:headed": "playwright test --headed --slow-mo=400",
|
||||
"e2e:ui": "playwright test --ui",
|
||||
"e2e:report": "playwright show-report"
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
|
@ -41,7 +37,6 @@
|
|||
"@angular/compiler-cli": "^18.0.0",
|
||||
"@angular/localize": "^18.1.0",
|
||||
"@ngx-env/builder": "^18.0.1",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"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'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -34,12 +34,9 @@ import {
|
|||
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
||||
import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component";
|
||||
import { MenusComponent } from "./components/menus/menus.component";
|
||||
import { ImportLegacyComponent } from './components/admin/import-legacy/import-legacy.component';
|
||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
|
||||
import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
|
||||
import { StatusComponent } from "./components/ogdhcp/status/status.component";
|
||||
import {
|
||||
|
||||
RunScriptAssistantComponent
|
||||
} from "./components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component";
|
||||
import { roleGuard } from './guards/role.guard';
|
||||
|
|
@ -47,8 +44,6 @@ 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 = [
|
||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||
{
|
||||
|
|
@ -57,25 +52,22 @@ const routes: Routes = [
|
|||
{ path: 'users', component: UsersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'env-vars', component: EnvVarsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'roles', component: RolesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'import-legacy', component: ImportLegacyComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'groups', component: GroupsComponent },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'pxe', component: PxeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'dhcp-servers', component: DhcpServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'boot-servers', component: BootServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'boot-servers/:uuid', component: BootServerDashboardComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'ogdhcp-status', component: StatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
||||
{ path: 'clients/run-script', component: RunScriptAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
||||
{ path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/run-script', component: RunScriptAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'repositories', component: RepositoriesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'software', component: SoftwareComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
|
|
@ -85,7 +77,6 @@ const routes: Routes = [
|
|||
{ 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'] } },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,5 @@
|
|||
|
||||
.content {
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ export class AppComponent {
|
|||
this.translateService.use(savedLanguage);
|
||||
}
|
||||
ngOnInit() {
|
||||
// Language is already handled in constructor
|
||||
sessionStorage.setItem('language', 'es');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,10 +58,10 @@ import { MatProgressSpinner } from "@angular/material/progress-spinner";
|
|||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu";
|
||||
import { MatAutocomplete, MatAutocompleteTrigger } from "@angular/material/autocomplete";
|
||||
import { 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 { MatTabsModule } from "@angular/material/tabs";
|
||||
import { MatTooltipModule } from "@angular/material/tooltip";
|
||||
import { MatTab, MatTabGroup } from "@angular/material/tabs";
|
||||
import { MatTooltip } from "@angular/material/tooltip";
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
|
|
@ -141,19 +141,14 @@ import { InputDialogComponent } from './components/task-logs/input-dialog/input-
|
|||
import { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
|
||||
import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
|
||||
import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||
import { StatusComponent } from "./components/ogdhcp/status/status.component";
|
||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import { 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 { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component";
|
||||
import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component';
|
||||
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component';
|
||||
import { 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 { 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';
|
||||
|
|
@ -174,23 +169,19 @@ import { ShowTaskScheduleComponent } from './components/commands/commands-task/s
|
|||
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';
|
||||
|
|
@ -198,23 +189,10 @@ import { HardwareProfileComponent } from './components/hardware-profile/hardware
|
|||
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) {
|
||||
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();
|
||||
}
|
||||
|
|
@ -240,7 +218,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
DeleteModalComponent,
|
||||
DeleteResultsModalComponent,
|
||||
QueueConfirmationModalComponent,
|
||||
PluginCommandModalComponent,
|
||||
ClassroomViewComponent,
|
||||
ClientViewComponent,
|
||||
ShowOrganizationalUnitComponent,
|
||||
|
|
@ -256,8 +233,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
OgbootStatusComponent,
|
||||
OgDhcpSubnetsComponent,
|
||||
CreateSubnetComponent,
|
||||
DhcpServersComponent,
|
||||
ManageDhcpServerComponent,
|
||||
AddClientsToSubnetComponent,
|
||||
CommandsComponent,
|
||||
CommandDetailComponent,
|
||||
|
|
@ -276,8 +251,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
DetailTaskComponent,
|
||||
TaskLogsComponent,
|
||||
ServerInfoDialogComponent,
|
||||
SyncSubnetDialogComponent,
|
||||
SyncResultDialogComponent,
|
||||
StatusComponent,
|
||||
ImagesComponent,
|
||||
CreateImageComponent,
|
||||
SoftwareComponent,
|
||||
|
|
@ -323,21 +297,18 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
ShowTaskScriptComponent,
|
||||
CreateTaskScriptComponent,
|
||||
ViewParametersModalComponent,
|
||||
ViewScriptModalComponent,
|
||||
OutputDialogComponent,
|
||||
ClientTaskLogsComponent,
|
||||
BootSoPartitionComponent,
|
||||
RemoveCacheImageComponent,
|
||||
ChangeParentComponent,
|
||||
SoftwareProfilePartitionComponent,
|
||||
ExecuteTaskNowComponent,
|
||||
ClientPendingTasksComponent,
|
||||
ModalOverlayComponent,
|
||||
ScrollToTopComponent,
|
||||
CreateTagModalComponent,
|
||||
CreateBranchModalComponent,
|
||||
ClientLogsModalComponent,
|
||||
ClientDiskLogsModalComponent,
|
||||
SafePipe,
|
||||
BackupRepositoryModalComponent,
|
||||
HardwareComponent,
|
||||
|
|
@ -346,20 +317,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
HardwareCollectionModalComponent,
|
||||
HardwareTypeComponent,
|
||||
ShowDetailsComponent,
|
||||
AssetManagerModalComponent,
|
||||
BootServersComponent,
|
||||
ManageBootServerComponent,
|
||||
BootServerDashboardComponent,
|
||||
OgcoreStatusComponent,
|
||||
GlobalOgbootStatusComponent,
|
||||
OgrepositoryStatusComponent,
|
||||
OgdhcpStatusComponent,
|
||||
StatusLogsComponent,
|
||||
ImportLegacyComponent,
|
||||
PageNotFoundComponent,
|
||||
OuSelectComponent,
|
||||
PluginsComponent,
|
||||
PluginSettingsComponent
|
||||
AssetManagerModalComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
|
@ -384,7 +342,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
MatProgressBarModule,
|
||||
MatStepperModule,
|
||||
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,
|
||||
NgxChartsModule,
|
||||
MatDatepickerModule,
|
||||
|
|
@ -408,7 +366,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
progressAnimation: 'increasing',
|
||||
closeButton: true
|
||||
}
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger, NgOptimizedImage, DiskPartitionSelectorComponent
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger, NgOptimizedImage
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
<div class="env-settings">
|
||||
<div class="header-container">
|
||||
<div class="header-container-title">
|
||||
<h2>{{ 'editEnvVarsTitle' | translate }}</h2>
|
||||
<h2>Editar Variables de Entorno</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-table [dataSource]="envVars" class="mat-elevation-z8">
|
||||
<!-- Nombre de la variable -->
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
<!-- Valor de la variable -->
|
||||
<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">
|
||||
<!-- Si es booleano, usamos checkbox -->
|
||||
<mat-checkbox *ngIf="isBoolean(variable.value)"
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
</mat-table>
|
||||
|
||||
<div class="actions">
|
||||
<button class="action-button" (click)="loadEnvVars()">{{ 'buttonReload' | translate }}</button>
|
||||
<button class="submit-button" (click)="saveEnvVars()">{{ 'buttonSaveChanges' | translate }}</button>
|
||||
<button class="action-button" (click)="loadEnvVars()">Recargar</button>
|
||||
<button class="submit-button" (click)="saveEnvVars()">Guardar Cambios</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
|
|
@ -11,7 +9,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrl: './env-vars.component.css'
|
||||
})
|
||||
export class EnvVarsComponent {
|
||||
public translate = inject(TranslateService);
|
||||
envVars: { name: string; value: string }[] = [];
|
||||
displayedColumns: string[] = ['name', 'value'];
|
||||
private apiUrl: string;
|
||||
|
|
@ -34,7 +31,7 @@ export class EnvVarsComponent {
|
|||
this.envVars = Object.entries(response.vars).map(([name, value]) => ({ name, value }));
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastService.error(this.translate.instant('couldNotBeLoaded'));
|
||||
this.toastService.error('No se pudieron cargar las variables de entorno.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -51,11 +48,11 @@ export class EnvVarsComponent {
|
|||
|
||||
this.http.post(this.apiUrl, { vars }).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('environmentVariablesSavedCorrectly'));
|
||||
this.toastService.success('Variables de entorno guardadas correctamente.');
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(this.translate.instant('errorSavingVariables'), err);
|
||||
this.toastService.error(this.translate.instant('couldNotBeLoaded'));
|
||||
console.error('Error al guardar las variables de entorno:', err);
|
||||
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="orgOperator">{{ 'checkboxOrgOperator' | 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>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
|
@ -14,7 +12,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./add-role-modal.component.css']
|
||||
})
|
||||
export class AddRoleModalComponent {
|
||||
public translate = inject(TranslateService);
|
||||
roleForm: FormGroup<any>;
|
||||
roleId: string | null = null;
|
||||
baseUrl: string;
|
||||
|
|
@ -59,7 +56,7 @@ export class AddRoleModalComponent {
|
|||
this.roleId = response['@id'];
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(this.translate.instant('errorFetchingRemoteCalendar'), err);
|
||||
console.error('Error fetching remote calendar:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -88,23 +85,23 @@ export class AddRoleModalComponent {
|
|||
if (this.roleId) {
|
||||
this.http.put(`${this.baseUrl}${this.roleId}`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success(this.translate.instant('roleUpdatedSuccessfully'));
|
||||
this.toastService.success('Rol actualizado correctamente');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(this.translate.instant('errorEditingRole1'), error);
|
||||
console.error(this.translate.instant('errorEditingRole'), error);
|
||||
this.toastService.error('Error al editar el rol:', error);
|
||||
console.error('Error al editar el rol', error);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/user-groups`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success(this.translate.instant('roleAddedSuccessfully'));
|
||||
this.toastService.success('Rol añadido correctamente');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(this.translate.instant('errorAddingRole1'), error);
|
||||
console.error(this.translate.instant('errorAddingRole'), error);
|
||||
this.toastService.error('Error al añadir rol:', error);
|
||||
console.error('Error al añadir rol', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
|
|
@ -10,7 +8,6 @@ import { ConfigService } from '@services/config.service';
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
|
||||
|
|
@ -34,7 +31,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('errorFetchingUserGroups'), error);
|
||||
console.error('Error fetching user groups', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
@ -50,7 +47,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('errorFetchingUserGroup'), error);
|
||||
console.error('Error fetching user group', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'labelSearchRole' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']"
|
||||
<mat-label>{{ 'searchRoleLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
|
||||
(keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
|
|
@ -24,12 +24,12 @@
|
|||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let role"> {{ column.cell(role) }} </td>
|
||||
</ng-container>
|
||||
|
||||
<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;">
|
||||
<button mat-icon-button color="primary" (click)="editRole(role)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
|
@ -17,7 +15,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./roles.component.css']
|
||||
})
|
||||
export class RolesComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string = this.configService.apiUrl;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
filters: { [key: string]: string } = {};
|
||||
|
|
@ -30,17 +27,17 @@ export class RolesComponent implements OnInit {
|
|||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'labelId',
|
||||
header: 'ID',
|
||||
cell: (role: any) => `${role.id}`
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'labelName',
|
||||
header: 'Nombre',
|
||||
cell: (role: any) => `${role.name}`
|
||||
},
|
||||
{
|
||||
columnDef: 'permissions',
|
||||
header: 'permissions',
|
||||
header: 'Permisos',
|
||||
cell: (role: any) => `${role.permissions.join(', ')}`
|
||||
}
|
||||
];
|
||||
|
|
@ -69,7 +66,7 @@ export class RolesComponent implements OnInit {
|
|||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('errorFetchingRoles'), error);
|
||||
console.error('Error fetching roles', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
|
|
@ -111,10 +108,10 @@ export class RolesComponent implements OnInit {
|
|||
this.http.delete(apiUrl).subscribe({
|
||||
next: () => {
|
||||
this.search();
|
||||
this.toastService.success(this.translate.instant('roleDeletedSuccessfully'));
|
||||
this.toastService.success('Role deleted successfully');
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(this.translate.instant('errorDeletingRole'), error);
|
||||
this.toastService.error('Error deleting role:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,61 +4,40 @@
|
|||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="userForm" class="user-form">
|
||||
<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>
|
||||
</mat-form-field>
|
||||
<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>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelRole' | translate }}</mat-label>
|
||||
<mat-select formControlName="role" required>
|
||||
@for (group of userGroups; track group['@id']) {
|
||||
<mat-option [value]="group['@id']">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
}
|
||||
<mat-option *ngFor="let group of userGroups" [value]="group['@id']">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</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-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>{{ 'cardView' | translate }}</mat-label>
|
||||
<mat-label>Vista tarjetas</mat-label>
|
||||
<mat-select formControlName="groupsView" required>
|
||||
@for (option of views; track option.value) {
|
||||
<mat-option [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
}
|
||||
<mat-option *ngFor="let option of views" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</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>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="action-container">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
|
|
@ -7,7 +5,6 @@ import { ToastrService } from "ngx-toastr";
|
|||
import { HttpClient } from "@angular/common/http";
|
||||
import { DataService } from "../data.service";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { DataService as GroupsDataService } from '../../../../groups/services/data.service';
|
||||
|
||||
interface UserGroup {
|
||||
'@id': string;
|
||||
|
|
@ -21,7 +18,6 @@ interface UserGroup {
|
|||
styleUrls: ['./add-user-modal.component.css']
|
||||
})
|
||||
export class AddUserModalComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
@Output() userAdded = new EventEmitter<void>();
|
||||
@Output() userEdited = new EventEmitter<void>();
|
||||
|
|
@ -37,28 +33,21 @@ export class AddUserModalComponent implements OnInit {
|
|||
{ value: 'list', name: 'Listado' },
|
||||
];
|
||||
|
||||
readonly availableLanguages: { code: string; label: string }[];
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<AddUserModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private http: HttpClient,
|
||||
private fb: FormBuilder,
|
||||
private dataService: DataService,
|
||||
private groupsDataService: GroupsDataService,
|
||||
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({
|
||||
username: ['', Validators.required],
|
||||
password: ['', Validators.required],
|
||||
role: ['', Validators.required],
|
||||
groupsView: ['card', Validators.required],
|
||||
clientsPerPage: [null, [Validators.min(1)]],
|
||||
language: ['es'],
|
||||
organizationalUnits: [[]]
|
||||
});
|
||||
|
||||
|
|
@ -73,13 +62,7 @@ export class AddUserModalComponent implements OnInit {
|
|||
});
|
||||
|
||||
this.dataService.getOrganizationalUnits().subscribe((data) => {
|
||||
const allUnits = data['hydra:member'];
|
||||
this.organizationalUnits = allUnits
|
||||
.filter((item: any) => item.type === 'organizational-unit')
|
||||
.map((unit: any) => ({
|
||||
...unit,
|
||||
path: this.groupsDataService.getOrganizationalUnitPath(unit, allUnits)
|
||||
}));
|
||||
this.organizationalUnits = data['hydra:member'].filter((item: any) => item.type === 'organizational-unit');
|
||||
});
|
||||
|
||||
if (this.data) {
|
||||
|
|
@ -99,9 +82,7 @@ export class AddUserModalComponent implements OnInit {
|
|||
username: response.username,
|
||||
role: response.userGroups.length > 0 ? response.userGroups[0]['@id'] : null,
|
||||
organizationalUnits: organizationalUnitIds,
|
||||
groupsView: response.groupsView,
|
||||
clientsPerPage: response.clientsPerPage ?? null,
|
||||
language: response.language ?? 'es'
|
||||
groupsView: response.groupsView
|
||||
});
|
||||
|
||||
this.userId = response['@id'];
|
||||
|
|
@ -111,7 +92,7 @@ export class AddUserModalComponent implements OnInit {
|
|||
},
|
||||
error: (err) => {
|
||||
this.loading = false;
|
||||
console.error(this.translate.instant('fetchingUserError'), err);
|
||||
console.error('Error fetching user:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -122,22 +103,12 @@ export class AddUserModalComponent implements OnInit {
|
|||
|
||||
onSubmit(): void {
|
||||
if (this.userForm.valid) {
|
||||
const mappedOUs = (this.userForm.value.organizationalUnits || []).map((ou: any) => {
|
||||
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,
|
||||
allowedOrganizationalUnits: mappedOUs,
|
||||
allowedOrganizationalUnits: this.userForm.value.organizationalUnits,
|
||||
enabled: true,
|
||||
userGroups: [this.userForm.value.role],
|
||||
groupsView: this.userForm.value.groupsView,
|
||||
clientsPerPage: this.userForm.value.clientsPerPage || null,
|
||||
language: this.userForm.value.language || null
|
||||
groupsView: this.userForm.value.groupsView
|
||||
};
|
||||
|
||||
if (!this.userId && this.userForm.value.password) {
|
||||
|
|
@ -151,7 +122,7 @@ export class AddUserModalComponent implements OnInit {
|
|||
if (this.userId) {
|
||||
this.http.put(`${this.baseUrl}${this.userId}`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success(this.translate.instant('userEditedSuccessfully'));
|
||||
this.toastService.success('Usuario editado correctamente');
|
||||
this.userEdited.emit();
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
|
|
@ -164,7 +135,7 @@ export class AddUserModalComponent implements OnInit {
|
|||
} else {
|
||||
this.http.post(`${this.baseUrl}/users`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success(this.translate.instant('userAddedSuccessfully'));
|
||||
this.toastService.success('Usuario añadido correctamente');
|
||||
this.userAdded.emit();
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<h1 mat-dialog-title>{{ 'changePassword' | translate }}</h1>
|
||||
<h1 mat-dialog-title>{{ 'dialogTitleChangePassword' | translate }}</h1>
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="userForm" class="user-form">
|
||||
<mat-form-field class="form-field">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, EventEmitter, Inject, Output } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
|
@ -12,7 +10,6 @@ import {ToastrService} from "ngx-toastr";
|
|||
styleUrl: './change-password-modal.component.css'
|
||||
})
|
||||
export class ChangePasswordModalComponent {
|
||||
public translate = inject(TranslateService);
|
||||
@Output() userEdited = new EventEmitter<void>();
|
||||
userForm: FormGroup;
|
||||
passwordMismatch: boolean = false;
|
||||
|
|
@ -58,10 +55,10 @@ export class ChangePasswordModalComponent {
|
|||
next: () => {
|
||||
this.userEdited.emit();
|
||||
this.dialogRef.close();
|
||||
this.toastService.success(this.translate.instant('passwordChangedSuccessfully'));
|
||||
this.toastService.success('Contraseña cambiada con éxito');
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(this.translate.instant('errorChangingPassword'), err);
|
||||
console.error('Error changing password:', err);
|
||||
this.toastService.error(err.error['hydra:description']);
|
||||
this.resetPasswordError = err.error.message;
|
||||
this.updateError = true;
|
||||
|
|
@ -69,7 +66,7 @@ export class ChangePasswordModalComponent {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
console.error(this.translate.instant('formIsInvalid'));
|
||||
console.error('Form is invalid');
|
||||
this.passwordMismatch = this.userForm.hasError('mismatch');
|
||||
this.updateError = true;
|
||||
this.loading = false
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||
|
|
@ -11,7 +9,6 @@ import { ConfigService } from '@services/config.service';
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
|
||||
|
|
@ -35,7 +32,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('fetchingUsersError'), error);
|
||||
console.error('Error fetching users', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
@ -51,7 +48,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('errorFetchingUserGroup'), error);
|
||||
console.error('Error fetching user group', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
@ -67,7 +64,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('errorFetchingUserGroups'), error);
|
||||
console.error('Error fetching user groups', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
@ -83,7 +80,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('errorFetchingOrganizationalUnits'), error);
|
||||
console.error('Error fetching organizational units', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'labelSearch' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']"
|
||||
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
|
||||
(keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let user">
|
||||
<ng-container *ngIf="column.columnDef === 'groupsView'">
|
||||
<mat-chip>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
</ng-container>
|
||||
|
||||
<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;">
|
||||
<button mat-icon-button color="primary" (click)="editUser(user)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
|
@ -15,7 +13,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./users.component.css']
|
||||
})
|
||||
export class UsersComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
|
@ -28,32 +25,27 @@ export class UsersComponent implements OnInit {
|
|||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'labelId',
|
||||
header: 'ID',
|
||||
cell: (user: any) => `${user.id}`
|
||||
},
|
||||
{
|
||||
columnDef: 'username',
|
||||
header: 'userName',
|
||||
header: 'Nombre de Usuario',
|
||||
cell: (user: any) => `${user.username}`
|
||||
},
|
||||
{
|
||||
columnDef: 'groupsView',
|
||||
header: 'groupView',
|
||||
header: 'Vista de Grupos',
|
||||
cell: (user: any) => `${user.groupsView}`
|
||||
},
|
||||
{
|
||||
columnDef: 'clientsPerPage',
|
||||
header: 'clientsPerPage',
|
||||
cell: (user: any) => user.clientsPerPage != null ? `${user.clientsPerPage}` : '-'
|
||||
},
|
||||
{
|
||||
columnDef: 'allowedOrganizationalUnits',
|
||||
header: 'allowedOrganizationalUnits',
|
||||
header: 'Unidades Organizacionales Permitidas',
|
||||
cell: (user: any) => `${user.allowedOrganizationalUnits.map((unit: { name: any; }) => unit.name).join(', ')}`
|
||||
},
|
||||
{
|
||||
columnDef: 'roles',
|
||||
header: 'labelRoles',
|
||||
header: 'Roles',
|
||||
cell: (user: any) => `${user.roles.join(', ')}`
|
||||
}
|
||||
];
|
||||
|
|
@ -82,7 +74,7 @@ export class UsersComponent implements OnInit {
|
|||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('fetchingUsersError'), error);
|
||||
console.error('Error fetching users', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
|
|
@ -126,11 +118,11 @@ export class UsersComponent implements OnInit {
|
|||
this.http.delete(apiUrl).subscribe({
|
||||
next: () => {
|
||||
console.log('User deleted successfully');
|
||||
this.toastService.success(this.translate.instant('userDeletedSuccessfully'));
|
||||
this.toastService.success('User deleted successfully');
|
||||
this.search();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('errorDeletingUser'), error);
|
||||
console.error('Error deleting user:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
<div class="search-container">
|
||||
<mat-form-field joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}" appearance="fill"
|
||||
class="search-string">
|
||||
<mat-label>{{ 'labelSearchCalendar' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']"
|
||||
<mat-label>{{ 'searchCalendarLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
|
||||
(keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef === 'isDefault' || column.columnDef === 'installed'">
|
||||
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
</ng-container>
|
||||
|
||||
<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;">
|
||||
<button mat-icon-button color="primary" (click)="editCalendar(calendar)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { DatePipe } from "@angular/common";
|
||||
|
|
@ -19,7 +17,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrl: './calendar.component.css'
|
||||
})
|
||||
export class CalendarComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
images: { downloadUrl: string; name: string; uuid: string }[] = [];
|
||||
|
|
@ -37,22 +34,22 @@ export class CalendarComponent implements OnInit {
|
|||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'labelId',
|
||||
header: 'ID',
|
||||
cell: (remoteCalendar: any) => `${remoteCalendar.id}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'labelName',
|
||||
header: 'Nombre',
|
||||
cell: (remoteCalendar: any) => `${remoteCalendar.name}`
|
||||
},
|
||||
{
|
||||
columnDef: 'rulesLength',
|
||||
header: 'numberOfRules',
|
||||
header: 'Nº de reglas',
|
||||
cell: (remoteCalendar: any) => `${remoteCalendar.remoteCalendarRules.length}`
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
header: 'creationDate',
|
||||
header: 'Fecha de creación',
|
||||
cell: (remoteCalendar: any) => `${this.datePipe.transform(remoteCalendar.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
}
|
||||
];
|
||||
|
|
@ -92,7 +89,7 @@ export class CalendarComponent implements OnInit {
|
|||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error(this.translate.instant('errorFetchingCalendars'), error);
|
||||
console.error('Error fetching calendars', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
|
|
@ -102,7 +99,7 @@ export class CalendarComponent implements OnInit {
|
|||
this.syncUds = true;
|
||||
this.http.post(`${this.apiUrl}/${calendar.uuid}/sync-uds`, {}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('calendarsSynchronizedCorrectly'));
|
||||
this.toastService.success('Calendarios sincronizados correctamente');
|
||||
this.search();
|
||||
this.syncUds = false;
|
||||
},
|
||||
|
|
@ -139,10 +136,10 @@ export class CalendarComponent implements OnInit {
|
|||
this.http.delete(apiUrl).subscribe({
|
||||
next: () => {
|
||||
this.search();
|
||||
this.toastService.success(this.translate.instant('calendarDeletedSuccessfully'));
|
||||
this.toastService.success('Calendar deleted successfully');
|
||||
},
|
||||
error: () => {
|
||||
this.toastService.error(this.translate.instant('errorDeletingCalendar'));
|
||||
this.toastService.error('Error deleting calendar');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
<div *ngIf="!isRemoteAvailable" class="form-group">
|
||||
<mat-label>{{ 'selectWeekDays' | translate }}</mat-label>
|
||||
<p class="custom-text"> {{ 'remotePcCalendarRuleInfo' | translate }}</p>
|
||||
<p class="custom-text"> (Los dias y horas seleccionados se marcarán como aula no disponible para remote pc.) </p>
|
||||
<div class="weekday-toggle-group full-width">
|
||||
<button
|
||||
*ngFor="let day of weekDays"
|
||||
|
|
@ -38,29 +38,31 @@
|
|||
<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>.
|
||||
El aula estará <strong>no disponible</strong> para Remote PC los días:
|
||||
<strong>{{ getSelectedDays().join(', ') }}</strong> de
|
||||
<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRemoteAvailable" class="form-group">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelReason' | translate }}</mat-label>
|
||||
<mat-label>{{ 'reasonLabel' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="availableReason" placeholder="{{ 'reasonPlaceholder' | translate }}" [required]="isRemoteAvailable">
|
||||
<mat-hint>{{ 'reasonAvailableRemotePc' | translate }}</mat-hint>
|
||||
<mat-hint>Razón por la cual el aula SI está disponible para su uso en Remote PC</mat-hint>
|
||||
</mat-form-field>
|
||||
<div class="time-fields">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'startDate' | translate }}</mat-label>
|
||||
<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 #picker1></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'endDate' | translate }}</mat-label>
|
||||
<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 #picker2></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
|
@ -82,9 +84,11 @@
|
|||
<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>
|
||||
El aula estará <strong>disponible</strong> para reserva desde el
|
||||
<strong>{{ availableFromDate | date:'fullDate' }}</strong> hasta el
|
||||
<strong>{{ availableToDate | date:'fullDate' }}</strong>
|
||||
<span *ngIf="busyFromHour && busyToHour">
|
||||
{{ 'inScheduleOf' | translate }}<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
|
||||
en el horario de <strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
|
|
@ -12,7 +10,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrl: './create-calendar-rule.component.css'
|
||||
})
|
||||
export class CreateCalendarRuleComponent {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
name: string = '';
|
||||
remoteCalendarRules: any[] = [];
|
||||
|
|
@ -101,7 +98,7 @@ export class CreateCalendarRuleComponent {
|
|||
this.http.put(`${this.baseUrl}${this.ruleId}`, formData)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success(this.translate.instant('calendarRuleUpdatedSuccessfully'));
|
||||
this.toastService.success('Calendar rule updated successfully');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
|
|
@ -113,7 +110,7 @@ export class CreateCalendarRuleComponent {
|
|||
this.http.post(`${this.baseUrl}/remote-calendar-rules`, formData)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success(this.translate.instant('calendarRuleCreatedSuccessfully'));
|
||||
this.toastService.success('Calendar rule created successfully');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<mat-dialog-content class="form-container">
|
||||
<!-- Campo para el nombre -->
|
||||
<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>
|
||||
<mat-icon *ngIf="isEditMode" matSuffix (click)="submitForm()">mode_edit</mat-icon>
|
||||
</mat-form-field>
|
||||
|
|
@ -28,9 +28,9 @@
|
|||
<mat-icon matListItemIcon>event_available</mat-icon>
|
||||
<div class="text-content">
|
||||
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('remotePcStatusAvailable' | translate) : ('remotePcStatusUnavailable' | translate) }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ 'labelDays' | translate }}<strong>{{ rule.busyWeekDaysMap }}</strong></div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">Días: <strong>{{ rule.busyWeekDaysMap }}</strong></div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">Razón: {{ rule.availableReason }}</div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">{{ 'labelDays' | translate }}<strong>{{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</strong></div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">Días: <strong>{{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</strong></div>
|
||||
<div matListItemLine>Horario: {{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
|
|
@ -50,6 +50,6 @@
|
|||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="submitForm()" [disabled]="!name || name === ''" cdkFocusInitial>
|
||||
{{ 'labelSaveButton' | translate }}
|
||||
{{ 'buttonSave' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
|
|
@ -15,7 +13,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrl: './create-calendar.component.css'
|
||||
})
|
||||
export class CreateCalendarComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
name: string = '';
|
||||
remoteCalendarRules: any[] = [];
|
||||
|
|
@ -53,7 +50,7 @@ export class CreateCalendarComponent implements OnInit {
|
|||
});
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(this.translate.instant('errorFetchingRemoteCalendar'), err);
|
||||
console.error('Error fetching remote calendar:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -78,7 +75,7 @@ export class CreateCalendarComponent implements OnInit {
|
|||
this.http.patch(`${this.baseUrl}${this.calendarId}`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success(this.translate.instant('calendarUpdatedSuccessfully'));
|
||||
this.toastService.success('Calendar updated successfully');
|
||||
this.dialogRef.close(true);
|
||||
this.load()
|
||||
},
|
||||
|
|
@ -91,7 +88,7 @@ export class CreateCalendarComponent implements OnInit {
|
|||
this.http.post(`${this.baseUrl}/remote-calendars`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success(this.translate.instant('calendarCreatedSuccessfully'));
|
||||
this.toastService.success('Calendar created successfully');
|
||||
this.dialogRef.close(true);
|
||||
this.load()
|
||||
},
|
||||
|
|
@ -132,10 +129,10 @@ export class CreateCalendarComponent implements OnInit {
|
|||
next: () => {
|
||||
console.log('Calendar deleted successfully');
|
||||
this.load();
|
||||
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');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
|
|
@ -10,7 +8,6 @@ import { ConfigService } from '@services/config.service';
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
|
||||
|
|
@ -31,7 +28,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('errorFetchingRemoteCalendars'), error);
|
||||
console.error('Error fetching remote calendars', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
@ -47,7 +44,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('fetchingCalendarError'), error);
|
||||
console.error('Error fetching calendar', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
|
||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'labelSearchGroupName' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
||||
<mat-label>{{ 'searchGroupNameLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let commandGroup">
|
||||
<ng-container *ngIf="column.columnDef !== 'commands'">
|
||||
{{ column.cell(commandGroup) }}
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</ng-container>
|
||||
|
||||
<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 }}">
|
||||
<button mat-icon-button color="info" (click)="viewGroupDetails(client)">
|
||||
<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 { HttpClient } from '@angular/common/http';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
|
@ -18,7 +16,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./commands-groups.component.css']
|
||||
})
|
||||
export class CommandsGroupsComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
|
@ -32,22 +29,22 @@ export class CommandsGroupsComponent implements OnInit {
|
|||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'labelId',
|
||||
header: 'ID',
|
||||
cell: (group: any) => `${group.id}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'labelName',
|
||||
header: 'Nombre',
|
||||
cell: (group: any) => `${group.name}`
|
||||
},
|
||||
{
|
||||
columnDef: 'commands',
|
||||
header: 'commandList',
|
||||
header: 'Lista de comandos',
|
||||
cell: (group: any) => `${group.commands}`
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
header: 'creationDate',
|
||||
header: 'Fecha de creación',
|
||||
cell: (group: any) => `${this.datePipe.transform(group.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
}
|
||||
];
|
||||
|
|
@ -72,7 +69,7 @@ export class CommandsGroupsComponent implements OnInit {
|
|||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('fetchingCommandGroupsError'), error);
|
||||
console.error('Error fetching command groups', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
|
|
@ -106,11 +103,11 @@ export class CommandsGroupsComponent implements OnInit {
|
|||
if (result) {
|
||||
this.http.delete(`${this.apiUrl}/${group.uuid}`).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('commandGroupRemovedWith'));
|
||||
this.toastService.success('Grupo de comandos eliminado con éxito');
|
||||
this.search();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('errorDeletingGroup'), error);
|
||||
console.error('Error al eliminar el grupo de comandos:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
|
||||
<form *ngIf="!loading" class="command-group-form" (ngSubmit)="onSubmit()">
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labelGroupName' | translate }}</mat-label>
|
||||
<mat-label>{{ 'groupNameLabel' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="groupName" name="groupName" required />
|
||||
</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="available-commands">
|
||||
|
|
@ -16,12 +16,12 @@
|
|||
<div class="table-wrapper">
|
||||
<table mat-table [dataSource]="availableCommands" class="mat-elevation-z8">
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
<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">
|
||||
<button mat-icon-button type="button" (click)="addCommand(command)">
|
||||
<mat-icon>add</mat-icon>
|
||||
|
|
@ -55,5 +55,5 @@
|
|||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<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>
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
|
@ -13,7 +11,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./create-command-group.component.css']
|
||||
})
|
||||
export class CreateCommandGroupComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
availableCommands: any[] = [];
|
||||
selectedCommands: any[] = [];
|
||||
|
|
@ -51,7 +48,7 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('errorFetchingAvailableCommands'), error);
|
||||
console.error('Error fetching available commands', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
|
|
@ -91,23 +88,23 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
const groupId = this.data.group.uuid;
|
||||
this.http.patch(`${this.baseUrl}/command-groups/${groupId}`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('commandGroupUpdatedWith'));
|
||||
this.toastService.success('Grupo de comandos actualizado con éxito');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('errorUpdatingGroup'), error);
|
||||
this.toastService.error(this.translate.instant('errorUpdatingGroup'));
|
||||
console.error('Error actualizando el grupo de comandos', error);
|
||||
this.toastService.error('Error al actualizar el grupo de comandos');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/command-groups`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('commandGroupCreatedWith'));
|
||||
this.toastService.success('Grupo de comandos creado con éxito');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('errorCreatingGroup'), error);
|
||||
this.toastService.error(this.translate.instant('errorCreatingGroup'));
|
||||
console.error('Error creando el grupo de comandos', error);
|
||||
this.toastService.error('Error al crear el grupo de comandos');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@
|
|||
<h3>{{ 'includedCommands' | translate }}</h3>
|
||||
<table mat-table [dataSource]="data.commands" class="mat-elevation-z8">
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
<form [formGroup]="form">
|
||||
<h4>{{ 'selectClients' | translate }}</h4>
|
||||
<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-option *ngFor="let client of clients" [value]="client.uuid">
|
||||
{{ client.name }}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
|
|
@ -13,7 +11,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./detail-command-group.component.css']
|
||||
})
|
||||
export class DetailCommandGroupComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
form!: FormGroup;
|
||||
clients: any[] = [];
|
||||
|
|
@ -48,7 +45,7 @@ export class DetailCommandGroupComponent implements OnInit {
|
|||
this.loading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('fetchingClients1Error'), error);
|
||||
console.error('Error fetching clients:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
|
|
@ -73,11 +70,11 @@ export class DetailCommandGroupComponent implements OnInit {
|
|||
this.http.post(apiUrl, payload).subscribe({
|
||||
next: () => {
|
||||
this.dialogRef.close();
|
||||
this.toastService.success(this.translate.instant('groupOfCommandsExecutedSuccessfully'));
|
||||
this.toastService.success('Grupo de comandos ejecutado exitosamente');
|
||||
this.loading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('errorExecutingGroupOfCommands'), error);
|
||||
console.error('Error ejecutando grupo de comandos:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'labelSearchTask' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
||||
<mat-label>{{ 'searchTaskLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let task">
|
||||
<ng-container *ngIf="column.columnDef !== 'management' && column.columnDef !== 'scope' && column.columnDef !== 'organizationalUnit'">
|
||||
{{ column.cell(task) }}
|
||||
|
|
@ -50,14 +50,14 @@
|
|||
</button>
|
||||
<button class="action-button script-btn" (click)="openShowScriptDialog(task)">
|
||||
<mat-icon>code</mat-icon>
|
||||
<span>{{ 'actions' | translate }}</span>
|
||||
<span>{{ 'taskActions' | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<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 }}">
|
||||
<button mat-icon-button color="primary" (click)="editTask(task)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
|
@ -18,7 +16,6 @@ import {DatePipe} from "@angular/common";
|
|||
styleUrls: ['./commands-task.component.css']
|
||||
})
|
||||
export class CommandsTaskComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
tasks: any[] = [];
|
||||
filters: { [key: string]: string | boolean } = {};
|
||||
|
|
@ -29,13 +26,13 @@ export class CommandsTaskComponent implements OnInit {
|
|||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
|
||||
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 },
|
||||
{ columnDef: 'id', header: 'ID', cell: (task: any) => task.id },
|
||||
{ columnDef: 'name', header: 'Nombre de tarea', cell: (task: any) => task.name },
|
||||
{ columnDef: 'scope', header: 'Ámbito', cell: (task: any) => task.scope },
|
||||
{ columnDef: 'organizationalUnit', header: 'Unidad Organizativa', cell: (task: any) => task.organizationalUnit?.name || '' },
|
||||
{ columnDef: 'management', header: 'Gestiones', cell: (task: any) => task.schedules },
|
||||
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss') },
|
||||
{ columnDef: 'createdBy', header: 'Creado por', cell: (task: any) => task.createdBy },
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'name', 'scope', 'management', 'nextExecution', 'createdBy', 'actions'];
|
||||
|
|
@ -68,7 +65,7 @@ export class CommandsTaskComponent implements OnInit {
|
|||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('fetchingTasksError'), error);
|
||||
console.error('Error fetching tasks', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
|
|
@ -136,11 +133,11 @@ export class CommandsTaskComponent implements OnInit {
|
|||
if (result) {
|
||||
this.http.delete(`${this.apiUrl}/${task.uuid}`).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('taskDeletedSuccessfully'));
|
||||
this.toastService.success('Tarea eliminada con éxito');
|
||||
this.loadTasks();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('errorDeletingTask'), error);
|
||||
console.error('Error al eliminar la tarea:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
</mat-form-field>
|
||||
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
|
||||
<label class="section-label">{{ 'labelWeekDays' | translate }}:</label>
|
||||
<label class="section-label">{{ 'weekDaysLabel' | translate }}:</label>
|
||||
<div class="weekday-toggle-group">
|
||||
<button
|
||||
*ngFor="let day of weekDays"
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
</div>
|
||||
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
|
||||
<label class="section-label">{{ 'labelMonths' | translate }}:</label>
|
||||
<label class="section-label">{{ 'monthsLabel' | translate }}:</label>
|
||||
<div class="month-toggle-row" *ngFor="let row of monthRows">
|
||||
<button
|
||||
*ngFor="let month of row"
|
||||
|
|
@ -106,6 +106,6 @@
|
|||
<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 }}
|
||||
{{ 'buttonSave' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
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} from "@angular/forms";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
|
|
@ -13,13 +11,12 @@ import {ConfigService} from "@services/config.service";
|
|||
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') }
|
||||
{ value: 'none', label: 'Sin repetición' },
|
||||
{ value: 'custom', label: 'Repetición personalizada' }
|
||||
];
|
||||
weekDays: string[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
isSingleDateSelected: boolean = true;
|
||||
|
|
@ -108,7 +105,7 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
}, {});
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('errorLoadingScheduleData'), error);
|
||||
console.error('Error loading schedule data', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -176,12 +173,12 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
const selectedMonthNames = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
|
||||
|
||||
if (selectedDayNames.length === 0) {
|
||||
this.toastr.error(this.translate.instant('youMustSelectAtLeastOne1'));
|
||||
this.toastr.error('Debe seleccionar al menos un día de la semana');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedMonthNames.length === 0) {
|
||||
this.toastr.error(this.translate.instant('youMustSelectAtLeastOne2'));
|
||||
this.toastr.error('Debe seleccionar al menos un mes');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -189,12 +186,12 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
const endDate = formData.recurrenceDetails?.endDate;
|
||||
|
||||
if (!initDate || !endDate) {
|
||||
this.toastr.error(this.translate.instant('youMustSpecifyStartDate'));
|
||||
this.toastr.error('Debe especificar fecha de inicio y fecha de fin');
|
||||
return;
|
||||
}
|
||||
|
||||
if (new Date(endDate) <= new Date(initDate)) {
|
||||
this.toastr.error(this.translate.instant('theEndDateMust'));
|
||||
this.toastr.error('La fecha de fin debe ser posterior a la fecha de inicio');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -218,21 +215,21 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
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.toastr.success('Programacion de tarea actualizada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error(this.translate.instant('errorUpdatingTask'));
|
||||
this.toastr.error('Error al actualizar la tarea');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.http.post<any>(this.apiUrl, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastr.success(this.translate.instant('taskScheduleCreatedWith'));
|
||||
this.toastr.success('Programacion de tarea creada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error(this.translate.instant('errorCreatingTask'));
|
||||
this.toastr.error('Error al crear la tarea');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,11 +258,6 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
width: calc(50% - 8px);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.radio-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
|
||||
<div class="info-supported-ops">
|
||||
<p class="info-supported-label">{{ 'labelSupportedOps' | translate }}:</p>
|
||||
<p class="info-supported-label">{{ 'supportedOpsLabel' | 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">
|
||||
|
|
@ -104,8 +104,8 @@
|
|||
<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'"
|
||||
<!-- ========== SYSTEM CONTROL (power-on, shutdown, reboot, login) ========== -->
|
||||
<div *ngIf="selectedAction === 'power-on' || selectedAction === 'shutdown' || selectedAction === 'reboot' || selectedAction === 'login'"
|
||||
class="form-section system-info">
|
||||
<div class="system-action-info">
|
||||
<mat-icon [style.color]="getSelectedCategoryColor()" class="big-icon">{{ getSelectedActionIcon() }}</mat-icon>
|
||||
|
|
@ -113,21 +113,6 @@
|
|||
</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">
|
||||
|
|
@ -146,7 +131,7 @@
|
|||
<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>
|
||||
<button mat-flat-button color="primary" (click)="saveNewScript()">{{ 'saveScript' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'existing'">
|
||||
|
|
@ -160,7 +145,7 @@
|
|||
|
||||
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
|
||||
<div class="script-content">
|
||||
<h3>{{ 'labelScript' | translate }}:</h3>
|
||||
<h3>{{ 'scriptLabel' | translate }}:</h3>
|
||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
||||
</div>
|
||||
<div class="script-params" *ngIf="parameterNames.length > 0">
|
||||
|
|
@ -180,5 +165,5 @@
|
|||
|
||||
<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>
|
||||
<button class="submit-button" (click)="onSubmit()" *ngIf="selectedAction">{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -50,8 +50,6 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
|
||||
selectedCategory: string = '';
|
||||
selectedAction: string = '';
|
||||
loginDisk: number = 1;
|
||||
loginPartition: number = 1;
|
||||
|
||||
categories: ActionCategory[] = [
|
||||
{
|
||||
|
|
@ -202,7 +200,7 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
|
||||
saveNewScript(): void {
|
||||
if (!this.newScript.trim()) {
|
||||
this.toastService.error(this.translate.instant('youMustEnterAScriptBefore'));
|
||||
this.toastService.error('Debe ingresar un script antes de guardar.');
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialog.open(SaveScriptComponent, {
|
||||
|
|
@ -211,7 +209,7 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success(this.translate.instant('scriptSavedSuccessfully'));
|
||||
this.toastService.success('Script guardado correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -240,17 +238,13 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
case 'power-on':
|
||||
case 'shutdown':
|
||||
case 'reboot':
|
||||
case 'login':
|
||||
type = this.selectedAction;
|
||||
content = this.selectedAction;
|
||||
break;
|
||||
|
||||
case 'login':
|
||||
type = 'login';
|
||||
content = 'login';
|
||||
break;
|
||||
|
||||
default:
|
||||
this.toastService.error(this.translate.instant('unrecognizedActionType'));
|
||||
this.toastService.error('Tipo de acción no reconocido');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -261,10 +255,6 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
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'));
|
||||
|
|
@ -279,12 +269,12 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
private validateScript(): boolean {
|
||||
if (this.commandType === 'new') {
|
||||
if (!this.newScript.trim()) {
|
||||
this.toastService.error(this.translate.instant('youMustEnterAScript'));
|
||||
this.toastService.error('Debe ingresar un script');
|
||||
return false;
|
||||
}
|
||||
} else if (this.commandType === 'existing') {
|
||||
if (!this.selectedScript) {
|
||||
this.toastService.error(this.translate.instant('youMustSelectAScript'));
|
||||
this.toastService.error('Debe seleccionar un script');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,127 +3,116 @@
|
|||
</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
@if (loading) {
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
}
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
@if (!loading) {
|
||||
<div class="task-form">
|
||||
@if (data?.source === 'assistant') {
|
||||
<mat-button-toggle-group [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode" appearance="standard">
|
||||
<mat-button-toggle value="create">
|
||||
<mat-icon>add_task</mat-icon>
|
||||
{{ '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>
|
||||
}
|
||||
<div class="task-form" *ngIf="!loading">
|
||||
<mat-button-toggle-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode" appearance="standard">
|
||||
<mat-button-toggle value="create">
|
||||
<mat-icon>add_task</mat-icon>
|
||||
{{ 'createTaskLabel' | 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') {
|
||||
<div class="select-task">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectTask' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedExistingTask" name="existingTask">
|
||||
@for (task of existingTasks; track task) {
|
||||
<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"
|
||||
<div *ngIf="taskMode === 'add'" class="select-task">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectTask' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedExistingTask" name="existingTask">
|
||||
<mat-option *ngFor="let task of existingTasks" [value]="task">{{ task.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'executionOrder' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[(ngModel)]="executionOrder"
|
||||
name="executionOrder"
|
||||
min="1"
|
||||
[placeholder]="'enterOrder' | translate"
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" [placeholder]="'nameLabel' | translate">
|
||||
<mat-error *ngIf="taskForm.get('name')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'notesLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="notes" [placeholder]="'notesPlaceholder' | translate"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox *ngIf="!editing" 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>{{ 'scopeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)"
|
||||
[disabled]="data?.clients && data.clients.length >= 1">
|
||||
<mat-option value="organizational-unit">{{ 'scopeOrganizationalUnit' | translate }}</mat-option>
|
||||
<mat-option value="classrooms-group">{{ 'scopeClassroomsGroup' | translate }}</mat-option>
|
||||
<mat-option value="classroom">{{ 'scopeClassroom' | translate }}</mat-option>
|
||||
<mat-option value="clients-group">{{ 'scopeClientsGroup' | translate }}</mat-option>
|
||||
<mat-option value="clients">{{ 'scopeClients' | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field *ngIf="taskForm.get('scope')?.value !== 'clients'" appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div *ngIf="taskForm.get('scope')?.value === 'clients'" class="clients-selection">
|
||||
<h4>{{ 'selectedClients' | translate }}</h4>
|
||||
|
||||
<div *ngIf="data?.selectedClients && data.selectedClients.length > 0" class="pre-selected-info">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>{{ 'preSelectedInfo' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="clients-list">
|
||||
<div class="clients-grid">
|
||||
<mat-card
|
||||
*ngFor="let client of clients"
|
||||
class="client-card"
|
||||
[class.pre-selected]="isClientPreSelected(client)"
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (taskMode === 'create' && taskForm && !loading) {
|
||||
<form [formGroup]="taskForm" class="task-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" [placeholder]="'nameLabel' | translate">
|
||||
@if (taskForm.get('name')?.invalid) {
|
||||
<mat-error>{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelNotes' | translate }}</mat-label>
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
|
|
@ -131,7 +120,7 @@
|
|||
<button
|
||||
class="submit-button"
|
||||
(click)="taskMode === 'create' ? saveTask() : addToExistingTask()"
|
||||
>
|
||||
{{ 'labelSaveButton' | translate }}
|
||||
>
|
||||
{{ 'buttonSave' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||
|
|
@ -10,7 +8,6 @@ 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({
|
||||
selector: 'app-create-task',
|
||||
|
|
@ -18,7 +15,6 @@ import { DataService as GroupsDataService } from "../../../groups/services/data.
|
|||
styleUrls: ['./create-task.component.css']
|
||||
})
|
||||
export class CreateTaskComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
taskForm: FormGroup;
|
||||
availableCommandGroups: any[] = [];
|
||||
|
|
@ -43,8 +39,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
private toastr: ToastrService,
|
||||
private dialog: MatDialog,
|
||||
public dialogRef: MatDialogRef<CreateTaskComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private dataService: GroupsDataService
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
|
|
@ -114,7 +109,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error(this.translate.instant('errorLoadingData'));
|
||||
this.toastr.error('Error al cargar los datos de la tarea');
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
|
|
@ -129,7 +124,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error(this.translate.instant('errorLoadingTasks'));
|
||||
this.toastr.error('Error al cargar las tareas existentes');
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
|
|
@ -142,7 +137,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
this.selectedClients = [...this.data.selectedClients];
|
||||
this.clients = [...this.data.selectedClients];
|
||||
} else {
|
||||
this.toastr.error(this.translate.instant('thereAreNoPreselectedClientsFor'));
|
||||
this.toastr.error('No hay clientes pre-seleccionados para este ámbito');
|
||||
this.taskForm.get('scope')?.setValue('');
|
||||
return;
|
||||
}
|
||||
|
|
@ -173,7 +168,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
this.availableOrganizationalUnits = filteredUnits;
|
||||
resolve();
|
||||
}, error => {
|
||||
this.toastr.error(this.translate.instant('errorWhenFilteringUnits'));
|
||||
this.toastr.error('Error al filtrar las unidades organizacionales');
|
||||
reject(error);
|
||||
});
|
||||
})
|
||||
|
|
@ -192,7 +187,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error(this.translate.instant('errorLoadingGroups'));
|
||||
this.toastr.error('Error al cargar los grupos de comandos');
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
|
@ -206,7 +201,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error(this.translate.instant('errorLoadingCommands'));
|
||||
this.toastr.error('Error al cargar los comandos individuales');
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
|
@ -216,15 +211,12 @@ export class CreateTaskComponent implements OnInit {
|
|||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=100`).subscribe(
|
||||
(data) => {
|
||||
this.allOrganizationalUnits = data['hydra:member'].map((unit: any) => ({
|
||||
...unit,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, data['hydra:member'])
|
||||
}));
|
||||
this.allOrganizationalUnits = data['hydra:member'];
|
||||
this.availableOrganizationalUnits = [...this.allOrganizationalUnits];
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error(this.translate.instant('errorLoadingUnits'));
|
||||
this.toastr.error('Error al cargar las unidades organizacionales');
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
|
|
@ -233,12 +225,12 @@ export class CreateTaskComponent implements OnInit {
|
|||
|
||||
addToExistingTask() {
|
||||
if (!this.selectedExistingTask) {
|
||||
this.toastr.error(this.translate.instant('youMustSelectAnExistingTask'));
|
||||
this.toastr.error('Debes seleccionar una tarea existente.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.executionOrder == null || this.executionOrder < 1) {
|
||||
this.toastr.error(this.translate.instant('youMustEnterAnOrderOf'));
|
||||
this.toastr.error('Debes introducir un orden de ejecución válido (mayor que 0).');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -247,14 +239,14 @@ export class CreateTaskComponent implements OnInit {
|
|||
executionOrder: this.executionOrder
|
||||
};
|
||||
|
||||
this.toastr.success(this.translate.instant('taskUpdatedSuccessfully'));
|
||||
this.toastr.success('Tarea actualizada con éxito');
|
||||
this.dialogRef.close(data);
|
||||
}
|
||||
|
||||
|
||||
saveTask(): void {
|
||||
if (this.taskForm.invalid) {
|
||||
this.toastr.error(this.translate.instant('pleaseFillOutAll'));
|
||||
this.toastr.error('Por favor, rellene todos los campos obligatorios');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -262,12 +254,12 @@ export class CreateTaskComponent implements OnInit {
|
|||
const scope = formData.scope;
|
||||
|
||||
if (scope === 'clients' && this.selectedClients.length === 0) {
|
||||
this.toastr.error(this.translate.instant('youMustSelectAtLeastOne'));
|
||||
this.toastr.error('Debe seleccionar al menos un cliente');
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope !== 'clients' && !formData.organizationalUnit) {
|
||||
this.toastr.error(this.translate.instant('youMustSelectAnOrganizationalUnit'));
|
||||
this.toastr.error('Debe seleccionar una unidad organizativa');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -281,29 +273,24 @@ export class CreateTaskComponent implements OnInit {
|
|||
payload.clients = this.selectedClients.map(client => client['@id'] ? client['@id'] : client.uuid);
|
||||
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;
|
||||
payload.organizationalUnit = formData.organizationalUnit;
|
||||
}
|
||||
|
||||
if (this.editing) {
|
||||
const taskId = this.data.task.uuid;
|
||||
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastr.success(this.translate.instant('taskUpdatedSuccessfully'));
|
||||
this.toastr.success('Tarea actualizada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error(this.translate.instant('errorUpdatingTask'));
|
||||
this.toastr.error('Error al actualizar la tarea');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.http.post<any>(this.apiUrl, payload).subscribe({
|
||||
next: response => {
|
||||
this.toastr.success(this.translate.instant('taskCreatedSuccessfully'));
|
||||
this.toastr.success('Tarea creada con éxito');
|
||||
this.dialogRef.close(response);
|
||||
if (this.data?.source === 'assistant') {
|
||||
if (formData.scheduleAfterCreate) {
|
||||
|
|
@ -314,7 +301,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error(this.translate.instant('errorCreatingTask'));
|
||||
this.toastr.error('Error al crear la tarea');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -326,7 +313,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
data: { task: task }
|
||||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastr.success(this.translate.instant('successfullyScheduledTask'));
|
||||
this.toastr.success('Tarea programada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -347,7 +334,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
data: { task: task }
|
||||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastr.success(this.translate.instant('successfullyScheduledTask'));
|
||||
this.toastr.success('Tarea programada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,26 +9,27 @@
|
|||
<mat-card-content>
|
||||
<p><strong>{{ 'taskId' | translate }}:</strong> {{ task.uuid }}</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>
|
||||
<table mat-table [dataSource]="task.commandTaskScripts || []" class="mat-elevation-z8" *ngIf="task.commandTaskScripts?.length > 0">
|
||||
<ng-container matColumnDef="order">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labelOrder' | translate }}</th>
|
||||
<th mat-header-cell *matHeaderCellDef>Orden</th>
|
||||
<td mat-cell *matCellDef="let script">{{ script.order }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labelType' | translate }}</th>
|
||||
<th mat-header-cell *matHeaderCellDef>Tipo</th>
|
||||
<td mat-cell *matCellDef="let script">{{ script.type }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="content">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labelContent' | translate }}</th>
|
||||
<th mat-header-cell *matHeaderCellDef>Contenido</th>
|
||||
<td mat-cell *matCellDef="let script">
|
||||
<span *ngIf="script.content">{{ script.content }}</span>
|
||||
<span *ngIf="!script.content && script.parameters">
|
||||
{{ 'configuredParameters' | translate }}</span>
|
||||
(parámetros configurados)
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
|
@ -36,7 +37,7 @@
|
|||
<tr mat-row *matRowDef="let row; columns: ['order', 'type', 'content'];"></tr>
|
||||
</table>
|
||||
|
||||
<p *ngIf="!task.commandTaskScripts?.length">{{ 'noActionsConfigured' | translate }}</p>
|
||||
<p *ngIf="!task.commandTaskScripts?.length">No hay acciones configuradas.</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'labelSearchSchedule' | translate }}</mat-label>
|
||||
<mat-label>{{ 'searchScheduleLabel' | translate }}</mat-label>
|
||||
<input matInput [placeholder]="'searchPlaceholder' | translate" [(ngModel)]="filters['name']"
|
||||
(keyup.enter)="loadData()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
<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>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let schedule">
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'recurrenceType'">
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
</ng-container>
|
||||
|
||||
<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 schedule" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editSchedule(schedule)" [matTooltip]="'editScheduleTooltip' | translate">
|
||||
<mat-icon>edit</mat-icon>
|
||||
|
|
@ -120,5 +120,5 @@
|
|||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonClose' | translate }}</button>
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'close' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
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 {MatTableDataSource} from "@angular/material/table";
|
||||
import {Client} from "../../../groups/model/model";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
|
@ -17,7 +15,6 @@ import {DatePipe} from "@angular/common";
|
|||
styleUrl: './show-task-schedule.component.css'
|
||||
})
|
||||
export class ShowTaskScheduleComponent implements OnInit{
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
dataSource = new MatTableDataSource<any>([]);
|
||||
length = 0;
|
||||
|
|
@ -29,13 +26,13 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
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 }
|
||||
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
|
||||
{ columnDef: 'recurrenceType', header: 'Tipo de Programación', cell: (schedule: any) => this.getRecurrenceTypeDisplay(schedule) },
|
||||
{ columnDef: 'executionTime', header: 'Hora de ejecución', cell: (schedule: any) => this.formatExecutionTimeForDisplay(schedule.executionTime) },
|
||||
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (schedule: any) => this.calculateNextExecution(schedule) },
|
||||
{ columnDef: 'daysOfWeek', header: 'Días de la semana', cell: (schedule: any) => this.formatDaysOfWeek(schedule.recurrenceDetails?.daysOfWeek) },
|
||||
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => this.formatMonths(schedule.recurrenceDetails?.months) },
|
||||
{ columnDef: 'enabled', header: 'Estado', cell: (schedule: any) => schedule.enabled }
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions'];
|
||||
|
|
@ -70,7 +67,7 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
},
|
||||
(error) => {
|
||||
this.loading = false;
|
||||
this.toastService.error(this.translate.instant('errorLoadingProgramming'));
|
||||
this.toastService.error('Error al cargar las programaciones');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -82,7 +79,7 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadData();
|
||||
this.toastService.success(this.translate.instant('programmingCreatedCorrectly'));
|
||||
this.toastService.success('Programación creada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -94,7 +91,7 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadData();
|
||||
this.toastService.success(this.translate.instant('programmingUpdatedCorrectly'));
|
||||
this.toastService.success('Programación actualizada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -109,7 +106,7 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
if (result) {
|
||||
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
|
||||
() => {
|
||||
this.toastService.success(this.translate.instant('programmingRemovedCorrectly'));
|
||||
this.toastService.success('Programación eliminada correctamente');
|
||||
this.loadData();
|
||||
},
|
||||
(error) => {
|
||||
|
|
@ -320,7 +317,7 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
if (schedule.recurrenceType === 'none') {
|
||||
return {
|
||||
type: 'none',
|
||||
label : this.translate.instant('uniqueExecution'),
|
||||
label: 'Ejecución única',
|
||||
icon: 'event',
|
||||
color: 'primary',
|
||||
details: schedule.executionDate ?
|
||||
|
|
|
|||
|
|
@ -321,21 +321,18 @@ mat-spinner {
|
|||
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;
|
||||
/* Contenido del script en celda */
|
||||
.script-content-cell {
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.no-content-hint {
|
||||
|
|
|
|||
|
|
@ -48,13 +48,10 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="content">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labelScript' | translate }}</th>
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'scriptLabel' | 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 *ngIf="item.content" class="script-content-cell">
|
||||
<code>{{ item.content }}</code>
|
||||
</div>
|
||||
<span *ngIf="!item.content" class="no-content-hint">
|
||||
({{ 'viewParameters' | translate }})
|
||||
|
|
@ -75,7 +72,7 @@
|
|||
</ng-container>
|
||||
|
||||
<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 item" style="text-align: center;">
|
||||
<button mat-icon-button color="warn" (click)="deleteTaskScript(item)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
|
|
@ -95,5 +92,5 @@
|
|||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonClose' | translate }}</button>
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'close' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,20 @@
|
|||
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 { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dialog";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
import { CreateTaskScheduleComponent } from "../create-task-schedule/create-task-schedule.component";
|
||||
import { CreateTaskScriptComponent } from "../create-task-script/create-task-script.component";
|
||||
import { DeleteModalComponent } from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import { ViewParametersModalComponent } from "./view-parameters-modal/view-parameters-modal.component";
|
||||
import { ViewScriptModalComponent } from "./view-script-modal/view-script-modal.component";
|
||||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
|
||||
import {CreateTaskScriptComponent} from "../create-task-script/create-task-script.component";
|
||||
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {ViewParametersModalComponent} from "./view-parameters-modal/view-parameters-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-task-script',
|
||||
templateUrl: './show-task-script.component.html',
|
||||
styleUrl: './show-task-script.component.css'
|
||||
})
|
||||
export class ShowTaskScriptComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
export class ShowTaskScriptComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
dataSource = new MatTableDataSource<any>([]);
|
||||
length = 0;
|
||||
|
|
@ -29,26 +25,26 @@ export class ShowTaskScriptComponent implements OnInit {
|
|||
filters: { [key: string]: string } = {};
|
||||
|
||||
columns = [
|
||||
{ columnDef: 'id', header: 'labelId', cell: (item: any) => item.id },
|
||||
{ columnDef: 'order', header: 'labelOrder', cell: (item: any) => item.order },
|
||||
{ columnDef: 'type', header: 'labelType', cell: (item: any) => item.type },
|
||||
{ columnDef: 'parameters', header: 'labelParameters', cell: (item: any) => item.parameters },
|
||||
{ columnDef: 'content', header: 'labelContent', cell: (item: any) => item.content },
|
||||
{ columnDef: 'id', header: 'ID', cell: (item: any) => item.id },
|
||||
{ columnDef: 'order', header: 'Orden', cell: (item: any) => item.order },
|
||||
{ columnDef: 'type', header: 'Tipo', cell: (item: any) => item.type },
|
||||
{ columnDef: 'parameters', header: 'Parámetros', cell: (item: any) => item.parameters },
|
||||
{ columnDef: 'content', header: 'Contenido', cell: (item: any) => item.content },
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['order', 'type', 'content', 'parameters', 'actions'];
|
||||
|
||||
typeLabels: { [key: string]: { label: string, icon: string } } = {
|
||||
'partition-and-format': { label: this.translate.instant('partitionAndFormat'), icon: 'storage' },
|
||||
'partition-assistant': { label: this.translate.instant('partitionTitle'), icon: 'storage' },
|
||||
'deploy-image': { label: this.translate.instant('displayImage'), icon: 'cloud_download' },
|
||||
'create-image': { label: this.translate.instant('createImage'), icon: 'cloud_upload' },
|
||||
'run-script': { label: this.translate.instant('runScript'), icon: 'code' },
|
||||
'partition-and-format': { label: 'Particionar y formatear', icon: 'storage' },
|
||||
'partition-assistant': { label: 'Asistente de particionado', icon: 'storage' },
|
||||
'deploy-image': { label: 'Desplegar imagen', icon: 'cloud_download' },
|
||||
'create-image': { label: 'Crear imagen', icon: 'cloud_upload' },
|
||||
'run-script': { label: 'Ejecutar script', icon: 'code' },
|
||||
'reboot': { label: 'Reiniciar', icon: 'restart_alt' },
|
||||
'shutdown': { label: 'Apagar', icon: 'power_off' },
|
||||
'poweron': { label: 'Encender', icon: 'power' },
|
||||
'power-on': { label: 'Encender', icon: 'power' },
|
||||
'login': { label: this.translate.instant('login'), icon: 'login' },
|
||||
'login': { label: 'Iniciar sesión', icon: 'login' },
|
||||
};
|
||||
|
||||
getTypeDisplay(type: string): { label: string, icon: string } {
|
||||
|
|
@ -98,7 +94,7 @@ export class ShowTaskScriptComponent implements OnInit {
|
|||
if (result) {
|
||||
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
|
||||
() => {
|
||||
this.toastService.success(this.translate.instant('removedSuccessfully'));
|
||||
this.toastService.success('Eliminado correctamente');
|
||||
this.loadData();
|
||||
},
|
||||
(error) => {
|
||||
|
|
@ -120,13 +116,6 @@ export class ShowTaskScriptComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
openScriptModal(content: string): void {
|
||||
this.dialog.open(ViewScriptModalComponent, {
|
||||
width: '700px',
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
createNewScript(): void {
|
||||
const dialogRef = this.dialog.open(CreateTaskScriptComponent, {
|
||||
width: '800px',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<h2 mat-dialog-title>{{ 'labelParameters' | translate }}</h2>
|
||||
<h2 mat-dialog-title>Parámetros</h2>
|
||||
<mat-dialog-content>
|
||||
<pre>{{ data | json }}</pre>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">{{ 'buttonClose' | translate }}</button>
|
||||
<button mat-button (click)="close()">Cerrar</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
.script-full-content {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<h2 mat-dialog-title>{{ 'labelScript' | translate }}</h2>
|
||||
<mat-dialog-content>
|
||||
<pre class="script-full-content">{{ data }}</pre>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">{{ 'buttonClose' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-view-script-modal',
|
||||
templateUrl: './view-script-modal.component.html',
|
||||
styleUrl: './view-script-modal.component.css'
|
||||
})
|
||||
export class ViewScriptModalComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ViewScriptModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,14 +6,14 @@
|
|||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'CommandsTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="command-button-row" joyrideStep="addCommandStep" text="{{ 'addCommandStepText' | translate }}">
|
||||
<button *ngIf="auth.isSuperAdmin" class="action-button" (click)="openCreateCommandModal()">{{ 'addCommand' | translate }}</button>
|
||||
<button class="action-button" (click)="openCreateCommandModal()">{{ 'addCommand' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'labelSearchCommand' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
||||
<mat-label>{{ 'searchCommandLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let command">
|
||||
<ng-container *ngIf="column.columnDef !== 'readOnly'">
|
||||
{{ column.cell(command) }}
|
||||
|
|
@ -41,11 +41,11 @@
|
|||
</ng-container>
|
||||
|
||||
<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 command" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<button mat-icon-button color="info" (click)="viewDetails($event, command)"><mat-icon>visibility</mat-icon></button>
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)"><mat-icon>edit</mat-icon></button>
|
||||
<button *ngIf="auth.isSuperAdmin" mat-icon-button color="warn" [disabled]="command.readOnly" (click)="deleteCommand($event, command)"><mat-icon>delete</mat-icon></button>
|
||||
<button mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)"><mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" [disabled]="command.readOnly" (click)="deleteCommand($event, command)"><mat-icon>delete</mat-icon></button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
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 { DatePipe } from '@angular/common';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { AuthService } from '@services/auth.service';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
|
||||
@Component({
|
||||
|
|
@ -19,8 +16,6 @@ import { JoyrideService } from 'ngx-joyride';
|
|||
styleUrls: ['./commands.component.css']
|
||||
})
|
||||
export class CommandsComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
public auth = inject(AuthService);
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
|
@ -34,22 +29,22 @@ export class CommandsComponent implements OnInit {
|
|||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'labelId',
|
||||
header: 'ID',
|
||||
cell: (command: any) => `${command.id}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'labelName',
|
||||
header: 'Nombre',
|
||||
cell: (command: any) => `${command.name}`
|
||||
},
|
||||
{
|
||||
columnDef: 'readOnly',
|
||||
header: 'readOnly',
|
||||
header: 'Solo Lectura',
|
||||
cell: (command: any) => `${command.readOnly}`
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
header: 'creationDate',
|
||||
header: 'Fecha de creación',
|
||||
cell: (command: any) => `${this.datePipe.transform(command.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
}
|
||||
];
|
||||
|
|
@ -74,7 +69,7 @@ export class CommandsComponent implements OnInit {
|
|||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('fetchingCommandsError'), error);
|
||||
console.error('Error fetching commands', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
|
|
@ -111,11 +106,11 @@ export class CommandsComponent implements OnInit {
|
|||
if (result) {
|
||||
this.http.delete(`${this.apiUrl}/${command.uuid}`).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('commandRemovedSuccessfully'));
|
||||
this.toastService.success('Comando eliminado con éxito');
|
||||
this.search();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('errorDeletingCommand'), error);
|
||||
console.error('Error al eliminar el comando:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,28 +2,28 @@
|
|||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="createCommandForm" (ngSubmit)="onSubmit()" class="command-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" placeholder="{{ 'commandNamePlaceholder' | translate }}" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelScript' | translate }}</mat-label>
|
||||
<mat-label>{{ 'scriptLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="script" placeholder="{{ 'commandScriptPlaceholder' | translate }}"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<mat-checkbox formControlName="readOnly">{{ 'labelReadOnly' | translate }}</mat-checkbox>
|
||||
<mat-checkbox formControlName="readOnly">{{ 'readOnlyLabel' | translate }}</mat-checkbox>
|
||||
|
||||
<mat-checkbox formControlName="enabled">{{ 'labelEnabled' | translate }}</mat-checkbox>
|
||||
<mat-checkbox formControlName="enabled">{{ 'enabledLabel' | translate }}</mat-checkbox>
|
||||
|
||||
<div class="checkbox-with-hint">
|
||||
<mat-checkbox formControlName="parameters">{{ 'labelParameters' | translate }}</mat-checkbox>
|
||||
<span class="hint-text">{{ 'scriptParameterHint' | translate }}</span>
|
||||
<mat-checkbox formControlName="parameters">{{ 'parameters' | translate }}</mat-checkbox>
|
||||
<span class="hint-text">Si se selecciona esta opción los parámetros deben indicarse en el script con el símbolo @.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelComments' | translate }}</mat-label>
|
||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="comments" placeholder="{{ 'commentsPlaceholder' | translate }}"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
|
@ -31,5 +31,5 @@
|
|||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onCancel($event)">{{ '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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
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 { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
|
@ -14,7 +12,6 @@ import { ConfigService } from "@services/config.service";
|
|||
styleUrls: ['./create-command.component.css']
|
||||
})
|
||||
export class CreateCommandComponent implements OnInit{
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
createCommandForm: FormGroup<any>;
|
||||
commandId: string | null = null;
|
||||
|
|
@ -59,7 +56,7 @@ export class CreateCommandComponent implements OnInit{
|
|||
this.commandId = response['@id'];
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(this.translate.instant('errorFetchingRemoteCalendar'), err);
|
||||
console.error('Error fetching remote calendar:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -84,7 +81,7 @@ export class CreateCommandComponent implements OnInit{
|
|||
if (this.commandId) {
|
||||
this.http.put(`${this.baseUrl}${this.commandId}`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success(this.translate.instant('commandEditedSuccessfully'));
|
||||
this.toastService.success('Comando editado correctamente');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
(error) => {
|
||||
|
|
@ -94,7 +91,7 @@ export class CreateCommandComponent implements OnInit{
|
|||
} else {
|
||||
this.http.post(`${this.baseUrl}/commands`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success(this.translate.instant('commandAddedSuccessfully'));
|
||||
this.toastService.success('Comando añadido correctamente');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
(error) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
|
|
@ -10,7 +8,6 @@ import { catchError, map } from 'rxjs/operators';
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
|
||||
|
|
@ -37,7 +34,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('fetchingCommandsError'), error);
|
||||
console.error('Error fetching commands', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
@ -53,7 +50,7 @@ export class DataService {
|
|||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(this.translate.instant('errorFetchingUserGroup'), error);
|
||||
console.error('Error fetching user group', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
<div class="modal-container">
|
||||
<h3 class="modal-title">{{ 'commandDetailsTitle' | translate }}</h3>
|
||||
<div class="modal-content">
|
||||
<p><strong>{{ 'labelRoleName' | translate }}:</strong> {{ data.name }}</p>
|
||||
<p><strong>{{ 'labelComments' | translate }}:</strong> {{ data.comments }}</p>
|
||||
<p><strong>{{ 'labelCreatedBy' | translate }}:</strong> {{ data.createdBy }}</p>
|
||||
<p><strong>{{ 'labelCreationDate' | translate }}:</strong> {{ data.createdAt | date:'medium' }}</p>
|
||||
<p><strong>{{ 'nameLabel' | translate }}:</strong> {{ data.name }}</p>
|
||||
<p><strong>{{ 'commentsLabel' | translate }}:</strong> {{ data.comments }}</p>
|
||||
<p><strong>{{ 'createdByLabel' | translate }}:</strong> {{ data.createdBy }}</p>
|
||||
<p><strong>{{ 'creationDateLabel' | translate }}:</strong> {{ data.createdAt | date:'medium' }}</p>
|
||||
|
||||
<div class="script-display">
|
||||
<h4>{{ 'labelScript' | translate }}:</h4>
|
||||
<h4>{{ 'scriptLabel' | translate }}:</h4>
|
||||
<pre>{{ data.script }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="additional-section" *ngIf="showClientSelect">
|
||||
<form [formGroup]="form">
|
||||
<h4>{{ 'selectClients' | translate }}</h4>
|
||||
<h4>{{ 'selectClientsTitle' | translate }}</h4>
|
||||
<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-option *ngFor="let client of clients" [value]="client.uuid">
|
||||
{{ client.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="form.get('selectedClients')?.invalid && form.get('selectedClients')?.touched">
|
||||
{{ 'selectAtLeastOneClient' | translate }}
|
||||
{{ 'selectAtLeastOneClientError' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
|
|
@ -14,7 +12,6 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./command-detail.component.css']
|
||||
})
|
||||
export class CommandDetailComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
form!: FormGroup;
|
||||
clients: any[] = [];
|
||||
|
|
@ -59,7 +56,7 @@ export class CommandDetailComponent implements OnInit {
|
|||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success(this.translate.instant('editedCommand'));
|
||||
this.toastService.success('Comando editado');
|
||||
this.data.command = result;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,10 +99,6 @@
|
|||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-field {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -1,73 +1,100 @@
|
|||
<h2 mat-dialog-title> {{ 'selectPartitionToBootOs' | translate }}</h2>
|
||||
<h2 mat-dialog-title> Seleccionar particion para arrancar SO</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
@if (loading) {
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
} @else {
|
||||
<div class="select-container">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> {{ 'labelClients' | translate }}</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
{{ 'clientListBootOs' | translate }}<mat-icon>desktop_windows</mat-icon>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="toggleSelectAll()">
|
||||
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!loading" class="select-container">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Clientes </mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Listado de clientes para arrancar un SO
|
||||
<mat-icon>desktop_windows</mat-icon>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="clients-grid">
|
||||
@for (client of data.clients; track client.uuid) {
|
||||
<div class="client-item">
|
||||
<div class="client-card"
|
||||
(click)="toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected}" >
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="toggleSelectAll()">
|
||||
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<img
|
||||
[src]="'assets/images/computer_' + client.status + '.svg'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of data.clients" class="client-item">
|
||||
<div class="client-card"
|
||||
(click)="toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected}" >
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name | slice:0:20 }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<img
|
||||
[src]="'assets/images/computer_' + client.status + '.svg'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
|
||||
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
|
||||
<mat-radio-button [value]="client"
|
||||
color="primary"
|
||||
[disabled]="!client.selected"
|
||||
(click)="$event.stopPropagation()">
|
||||
{{ 'labelModel' | translate }}</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name | slice:0:20 }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
</div>
|
||||
}
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
|
||||
<mat-radio-button [value]="client"
|
||||
color="primary"
|
||||
[disabled]="!client.selected"
|
||||
(click)="$event.stopPropagation()">
|
||||
Modelo
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider class="section-divider"></mat-divider>
|
||||
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div class="partition-table-container">
|
||||
<app-disk-partition-selector
|
||||
[(selectedPartition)]="selectedPartition"
|
||||
[partitions]="dataSource.data"
|
||||
[columns]="columns"
|
||||
[displayedColumns]="displayedColumns"
|
||||
[disableCondition]="isPartitionDisabled">
|
||||
</app-disk-partition-selector>
|
||||
</div>
|
||||
}
|
||||
<div *ngIf="!loading" class="partition-table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group
|
||||
[(ngModel)]="selectedPartition"
|
||||
[disabled]="!row.operativeSystem"
|
||||
>
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">{{ 'buttonExecute' | translate }}</button>
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
|
|
@ -13,14 +11,12 @@ import { ToastrService } from "ngx-toastr";
|
|||
styleUrl: './boot-so-partition.component.css'
|
||||
})
|
||||
export class BootSoPartitionComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
selectedPartition: any = null;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
clientId: string | null = null;
|
||||
selectedClients: any[] = [];
|
||||
selectedModelClient: any = null;
|
||||
partitions: any[] = [];
|
||||
filteredPartitions: any[] = [];
|
||||
allSelected: boolean = false;
|
||||
clientData: any[] = [];
|
||||
|
|
@ -28,42 +24,38 @@ export class BootSoPartitionComponent implements OnInit {
|
|||
columns = [
|
||||
{
|
||||
columnDef: 'diskNumber',
|
||||
header: 'labelDisk',
|
||||
header: 'Disco',
|
||||
cell: (partition: any) => partition.diskNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionNumber',
|
||||
header: 'partition',
|
||||
header: 'Particion',
|
||||
cell: (partition: any) => partition.partitionNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'size',
|
||||
header: 'labelDiskSize',
|
||||
header: 'Tamaño',
|
||||
cell: (partition: any) => `${partition.size} MB`
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionCode',
|
||||
header: 'partitionType',
|
||||
header: 'Tipo de partición',
|
||||
cell: (partition: any) => partition.partitionCode
|
||||
},
|
||||
{
|
||||
columnDef: 'filesystem',
|
||||
header: 'fileSystem',
|
||||
header: 'Sistema de ficheros',
|
||||
cell: (partition: any) => partition.filesystem
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'sW',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => partition.operativeSystem?.name
|
||||
}
|
||||
];
|
||||
|
||||
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
|
||||
|
||||
isPartitionDisabled = (partition: any): boolean => {
|
||||
return !partition.operativeSystem;
|
||||
};
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: { clients: any },
|
||||
private dialogRef: MatDialogRef<BootSoPartitionComponent>,
|
||||
|
|
@ -94,14 +86,13 @@ export class BootSoPartitionComponent implements OnInit {
|
|||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.partitions = Array.isArray(response.partitions)
|
||||
? response.partitions.filter((p: any) => p.partitionCode !== 'CACHE' && p.partitionCode !== 'EMPTY')
|
||||
: [];
|
||||
this.dataSource.data = this.partitions;
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('errorLoadingData1'), error);
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -113,7 +104,7 @@ export class BootSoPartitionComponent implements OnInit {
|
|||
|
||||
updateSelectedClients() {
|
||||
this.selectedClients = this.data.clients.filter(
|
||||
(client: { selected: boolean }) => client.selected
|
||||
(client: { selected: boolean; state: string }) => client.selected && client.state === "og-live"
|
||||
);
|
||||
|
||||
if (!this.selectedClients.includes(this.selectedModelClient)) {
|
||||
|
|
@ -124,10 +115,11 @@ export class BootSoPartitionComponent implements OnInit {
|
|||
|
||||
toggleSelectAll() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.data.clients.forEach((client: { selected: boolean }) => {
|
||||
client.selected = this.allSelected;
|
||||
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === "og-live") {
|
||||
client.selected = this.allSelected;
|
||||
}
|
||||
});
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
close() {
|
||||
|
|
@ -141,7 +133,7 @@ export class BootSoPartitionComponent implements OnInit {
|
|||
partition: this.selectedPartition['@id']
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success(this.translate.instant('clientUpdatedSuccessfully'));
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,53 +1,24 @@
|
|||
@switch (buttonType) {
|
||||
@case ('icon') {
|
||||
<button mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu" [disabled]="disabled">
|
||||
<mat-icon>{{ icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@case ('text') {
|
||||
<button class="action-button" [disabled]="clientData.length === 0 || disabled" [matMenuTriggerFor]="commandMenu">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
}
|
||||
@case ('menu-item') {
|
||||
<button mat-menu-item [matMenuTriggerFor]="commandMenu" [disabled]="disabled">
|
||||
<mat-icon>{{ icon }}</mat-icon>
|
||||
<span>{{ buttonText }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
<ng-container [ngSwitch]="buttonType">
|
||||
<button *ngSwitchCase="'icon'" mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu"
|
||||
[disabled]="disabled">
|
||||
<mat-icon>{{ icon }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<button class="action-button" [disabled]="clientData.length === 0 || disabled" *ngSwitchCase="'text'"
|
||||
[matMenuTriggerFor]="commandMenu">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
|
||||
<button mat-menu-item *ngSwitchCase="'menu-item'" [matMenuTriggerFor]="commandMenu" [disabled]="disabled">
|
||||
<mat-icon>{{ icon }}</mat-icon>
|
||||
<span>{{ buttonText }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<mat-menu #commandMenu="matMenu">
|
||||
@for (command of arrayCommands; track command.slug) {
|
||||
<button mat-menu-item [disabled]="command.disabled || (command.slug === 'create-image' && clientData.length > 1)"
|
||||
(click)="onCommandSelect(command.slug)">
|
||||
@if (command.icon) {
|
||||
<mat-icon>{{ command.icon }}</mat-icon>
|
||||
}
|
||||
<span>{{ command.translationKey ? (command.translationKey | translate) : (command.label || command.slug) }}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (pluginGroups.length > 0) {
|
||||
<mat-divider></mat-divider>
|
||||
@for (group of pluginGroups; track group.plugin.name) {
|
||||
<button mat-menu-item [matMenuTriggerFor]="pluginSubMenu">
|
||||
@if (group.plugin.icon) {
|
||||
<mat-icon>{{ group.plugin.icon }}</mat-icon>
|
||||
}
|
||||
<span>{{ group.plugin.titleKey ? (group.plugin.titleKey | translate) : group.plugin.title }}</span>
|
||||
</button>
|
||||
|
||||
<mat-menu #pluginSubMenu="matMenu">
|
||||
@for (cmd of group.commands; track cmd.slug) {
|
||||
<button mat-menu-item [disabled]="cmd.disabled" (click)="onCommandSelect(cmd.slug)">
|
||||
@if (cmd.icon) {
|
||||
<mat-icon>{{ cmd.icon }}</mat-icon>
|
||||
}
|
||||
<span>{{ (cmd.translationKey || cmd.labelKey) ? ((cmd.translationKey || cmd.labelKey) | translate) : cmd.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
}
|
||||
}
|
||||
<button mat-menu-item [disabled]="command.disabled
|
||||
|| (command.slug === 'create-image' && clientData.length > 1)" *ngFor="let command of arrayCommands"
|
||||
(click)="onCommandSelect(command.slug)">
|
||||
{{ command.translationKey | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Router } from "@angular/router";
|
||||
|
|
@ -10,10 +8,6 @@ import { MatDialog } from "@angular/material/dialog";
|
|||
import { RemoveCacheImageComponent } from "./remove-cache-image/remove-cache-image.component";
|
||||
import { AuthService } from '@services/auth.service';
|
||||
import {SoftwareProfilePartitionComponent} from "./software-profile-partition/software-profile-partition.component";
|
||||
import { QueueConfirmationModalComponent } from '../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component';
|
||||
import { PluginCommandModalComponent } from '../../../../shared/plugin-command-modal/plugin-command-modal.component';
|
||||
import { ExecuteTaskNowComponent } from './execute-task-now/execute-task-now.component';
|
||||
import { PluginService, PluginCommand } from '@services/plugin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-command',
|
||||
|
|
@ -21,10 +15,7 @@ import { PluginService, PluginCommand } from '@services/plugin.service';
|
|||
styleUrls: ['./execute-command.component.css']
|
||||
})
|
||||
export class ExecuteCommandComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
@Input() runScriptContext: any = null;
|
||||
@Input() organizationalUnit: any = null;
|
||||
@Input() showOnlyDirectClients: boolean = false;
|
||||
@Input() clientState: string = 'off';
|
||||
@Input() clientData: any[] = [];
|
||||
@Input() buttonType: 'icon' | 'text' | 'menu-item' = 'icon';
|
||||
|
|
@ -33,13 +24,12 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
@Input() disabled: boolean = false;
|
||||
baseUrl: string;
|
||||
loading: boolean = true;
|
||||
isFetchingOuClients: boolean = false;
|
||||
|
||||
arrayCommands: any[] = [
|
||||
{ translationKey: 'executeCommands.powerOn', slug: 'power-on', disabled: false },
|
||||
{ translationKey: 'executeCommands.powerOff', slug: 'power-off', disabled: false },
|
||||
{ translationKey: 'executeCommands.reboot', slug: 'reboot', disabled: false },
|
||||
{ translationKey: 'executeCommands.login', slug: 'login', disabled: false },
|
||||
{ translationKey: 'executeCommands.login', slug: 'login', disabled: true },
|
||||
{ translationKey: 'executeCommands.createImage', slug: 'create-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.deployImage', slug: 'deploy-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.deleteImageCache', slug: 'remove-cache-image', disabled: false },
|
||||
|
|
@ -47,9 +37,6 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
{ translationKey: 'executeCommands.softwareInventory', slug: 'software-inventory', disabled: false },
|
||||
{ translationKey: 'executeCommands.hardwareInventory', slug: 'hardware-inventory', disabled: false },
|
||||
{ translationKey: 'executeCommands.runScript', slug: 'run-script', disabled: false },
|
||||
{ translationKey: 'executeCommands.refresh', slug: 'refresh', disabled: false },
|
||||
{ translationKey: 'executeCommands.deleteLease', slug: 'delete-lease', disabled: false },
|
||||
{ translationKey: 'executeCommands.executeTask', slug: 'execute-task', disabled: false },
|
||||
];
|
||||
|
||||
client: any = {};
|
||||
|
|
@ -61,28 +48,15 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
private toastService: ToastrService,
|
||||
public auth: AuthService,
|
||||
private dialog: MatDialog,
|
||||
public pluginService: PluginService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
pluginGroups: any[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.clientData = this.clientData || [];
|
||||
const allowed = this.getAllowedCommandsByRole();
|
||||
this.arrayCommands = this.arrayCommands.filter(c => allowed.includes(c.slug));
|
||||
|
||||
// Punto de anclaje reactivo para grupos de comandos aportados por plugins
|
||||
this.pluginGroups = this.pluginService.getPluginGroups();
|
||||
this.pluginService.plugins$.subscribe(() => {
|
||||
this.pluginGroups = this.pluginService.getPluginGroups();
|
||||
this.updateCommandStates();
|
||||
});
|
||||
this.pluginService.statusUpdates$.subscribe(() => {
|
||||
this.updateCommandStates();
|
||||
});
|
||||
|
||||
this.updateCommandStates();
|
||||
}
|
||||
|
||||
|
|
@ -101,15 +75,11 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
'power-off',
|
||||
'reboot',
|
||||
'login',
|
||||
'create-image',
|
||||
'deploy-image',
|
||||
'software-inventory',
|
||||
'hardware-inventory',
|
||||
'remove-cache-image',
|
||||
'partition',
|
||||
'refresh',
|
||||
'delete-lease',
|
||||
'execute-task'
|
||||
'partition'
|
||||
],
|
||||
'ou-minimal': [
|
||||
'power-on',
|
||||
|
|
@ -131,127 +101,35 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
states = [this.clientState];
|
||||
}
|
||||
|
||||
const hasClients = this.clientData.length > 0 || !!this.clientState;
|
||||
const allOffOrDisconnected = states.every(state => state === 'off' || state === 'disconnected');
|
||||
const allSameState = states.every(state => state === states[0]);
|
||||
const multipleClients = this.clientData.length > 1;
|
||||
const hasOgLive = states.some(state => state === 'og-live');
|
||||
|
||||
// Acciones que admiten encolado o son independientes del estado del cliente
|
||||
const queueableOrStateAgnostic = [
|
||||
'power-on',
|
||||
'power-off',
|
||||
'reboot',
|
||||
'login',
|
||||
'deploy-image',
|
||||
'partition',
|
||||
'run-script',
|
||||
'execute-task',
|
||||
'delete-lease'
|
||||
];
|
||||
|
||||
this.arrayCommands = this.arrayCommands.map(command => {
|
||||
if (!hasClients) {
|
||||
command.disabled = true;
|
||||
return command;
|
||||
}
|
||||
|
||||
// Restricciones lógicas estrictas
|
||||
if (command.slug === 'create-image') {
|
||||
command.disabled = multipleClients || !hasOgLive;
|
||||
} else if (command.slug === 'hardware-inventory' || command.slug === 'software-inventory') {
|
||||
command.disabled = multipleClients || !hasOgLive;
|
||||
} else if (command.slug === 'remove-cache-image') {
|
||||
command.disabled = !hasOgLive;
|
||||
} else if (command.slug === 'refresh') {
|
||||
command.disabled = !hasOgLive;
|
||||
} else if (queueableOrStateAgnostic.includes(command.slug)) {
|
||||
command.disabled = false;
|
||||
if (allOffOrDisconnected) {
|
||||
command.disabled = command.slug !== 'power-on' && command.slug !== 'create-image';
|
||||
} else if (allSameState) {
|
||||
if (states[0] === 'off' || states[0] === 'disconnected') {
|
||||
command.disabled = !['power-on', 'create-image'].includes(command.slug);
|
||||
} else {
|
||||
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-image', 'partition', 'run-script', 'software-inventory', 'hardware-inventory'].includes(command.slug);
|
||||
}
|
||||
} else {
|
||||
command.disabled = false;
|
||||
}
|
||||
|
||||
return command;
|
||||
});
|
||||
|
||||
if (this.pluginGroups && this.pluginGroups.length > 0) {
|
||||
const clientsToCheck = this.clientData.length > 0
|
||||
? this.clientData
|
||||
: (this.clientState ? [{ status: this.clientState }] : []);
|
||||
|
||||
for (const group of this.pluginGroups) {
|
||||
for (const cmd of group.commands) {
|
||||
cmd.disabled = !hasClients || !this.pluginService.isCommandSupportedForClients(cmd, clientsToCheck, group.plugin.name);
|
||||
|
||||
// Adaptar dinámicamente la etiqueta de activar/desactivar en ogaquarium si hay un solo cliente seleccionado
|
||||
if (cmd.slug === 'ogaquarium-toggle-status' && this.clientData.length === 1) {
|
||||
const client = this.clientData[0];
|
||||
const idKey = client.id ? String(client.id) : null;
|
||||
const uuidKey = client.uuid || client.name;
|
||||
const currentStatus = (idKey ? this.pluginService.getPluginStatus(group.plugin.name, idKey) : null)
|
||||
|| (uuidKey ? this.pluginService.getPluginStatus(group.plugin.name, uuidKey) : null)
|
||||
|| 'DISABLED';
|
||||
|
||||
if (currentStatus === 'DISABLED') {
|
||||
cmd.label = this.translate.instant('ogaquarium.commands.activate') || 'Activar puesto';
|
||||
} else {
|
||||
cmd.label = this.translate.instant('ogaquarium.commands.deactivate') || 'Desactivar puesto';
|
||||
}
|
||||
cmd.labelKey = undefined;
|
||||
}
|
||||
if (command.slug === 'software-inventory') {
|
||||
command.disabled = multipleClients;
|
||||
} else if (
|
||||
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'remove-cache-image', 'run-script', 'create-image'].includes(command.slug)
|
||||
) {
|
||||
command.disabled = false;
|
||||
} else {
|
||||
command.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
|
||||
onCommandSelect(action: any): void {
|
||||
if (this.organizationalUnit && this.organizationalUnit.id) {
|
||||
this.fetchClientsForOuAndExecute(action);
|
||||
return;
|
||||
}
|
||||
|
||||
this.executeAction(action);
|
||||
}
|
||||
|
||||
private fetchClientsForOuAndExecute(action: any): void {
|
||||
if (this.isFetchingOuClients) {
|
||||
return;
|
||||
}
|
||||
this.isFetchingOuClients = true;
|
||||
|
||||
const unitId = this.organizationalUnit.id;
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${unitId}&page=1&itemsPerPage=10000`).subscribe({
|
||||
next: (response) => {
|
||||
this.isFetchingOuClients = false;
|
||||
let allClients: any[] = response['hydra:member'] || [];
|
||||
|
||||
if (this.showOnlyDirectClients && this.organizationalUnit['@id']) {
|
||||
allClients = allClients.filter((client: any) =>
|
||||
client.organizationalUnit?.['@id'] === this.organizationalUnit['@id']
|
||||
);
|
||||
}
|
||||
|
||||
if (allClients.length === 0) {
|
||||
this.toastService.warning(this.translate.instant('thisOrganizationalUnitDoesNotHave') || 'Esta unidad organizativa no tiene clientes.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.clientData = allClients;
|
||||
this.executeAction(action);
|
||||
},
|
||||
error: (error) => {
|
||||
this.isFetchingOuClients = false;
|
||||
this.toastService.error(this.translate.instant('errorLoadingClients1') || 'Error al cargar los clientes');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private executeAction(action: any): void {
|
||||
// Punto de anclaje para ejecución de comandos aportados por plugins
|
||||
const pluginCmd = this.pluginService.findCommand(action);
|
||||
if (pluginCmd) {
|
||||
this.executePluginCommand(pluginCmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'partition') {
|
||||
this.openPartitionAssistant();
|
||||
}
|
||||
|
|
@ -295,53 +173,14 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
if (action === 'software-inventory') {
|
||||
this.softwareInventory();
|
||||
}
|
||||
|
||||
if (action === 'refresh') {
|
||||
this.refreshClient();
|
||||
}
|
||||
|
||||
if (action === 'delete-lease') {
|
||||
this.deleteClientLease();
|
||||
}
|
||||
|
||||
if (action === 'execute-task') {
|
||||
this.openExecuteTaskModal();
|
||||
}
|
||||
}
|
||||
|
||||
openExecuteTaskModal(): void {
|
||||
this.dialog.open(ExecuteTaskNowComponent, {
|
||||
width: '500px',
|
||||
height: 'auto',
|
||||
data: { clients: this.clientData }
|
||||
});
|
||||
}
|
||||
|
||||
rebootClient(): void {
|
||||
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
|
||||
width: '400px',
|
||||
data: { clients: this.clientData }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((queue: boolean | undefined) => {
|
||||
if (queue === undefined) return;
|
||||
|
||||
this.http.post(`${this.baseUrl}/clients/server/reboot`, {
|
||||
clients: this.clientData.map((client: any) => client['@id']),
|
||||
queue
|
||||
}).subscribe(
|
||||
() => { this.toastService.success(this.translate.instant('clientUpdatedSuccessfully')); },
|
||||
(error: any) => { this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente'); }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
refreshClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/refresh`, {
|
||||
this.http.post(`${this.baseUrl}/clients/server/reboot`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success(this.translate.instant('refreshmentRequestSentCorrectly'));
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
|
|
@ -368,7 +207,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success(this.translate.instant('oSBootRequest'));
|
||||
this.toastService.success('Petición de arranque de SO enviada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -392,14 +231,14 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success(this.translate.instant('cacheClearRequest'));
|
||||
this.toastService.success('Petición de borrado de caché de imagen enviada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hardwareInventory(): void {
|
||||
if (this.clientData.length === 0) {
|
||||
this.toastService.error(this.translate.instant('noClientsSelected'));
|
||||
this.toastService.error('No hay clientes seleccionados');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -409,7 +248,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success(this.translate.instant('hardwareInventorySuccessfullyUpdated'));
|
||||
this.toastService.success('Inventario de hardware actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
|
|
@ -419,7 +258,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
|
||||
softwareInventory(): void {
|
||||
if (this.clientData.length === 0) {
|
||||
this.toastService.error(this.translate.instant('noClientsSelected'));
|
||||
this.toastService.error('No hay clientes seleccionados');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -451,7 +290,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success(this.translate.instant('ignitionRequestSentCorrectly'));
|
||||
this.toastService.success('Petición de encendido enviada correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
|
|
@ -460,22 +299,16 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}
|
||||
|
||||
powerOffClient(): void {
|
||||
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
|
||||
width: '400px',
|
||||
data: { clients: this.clientData }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((queue: boolean | undefined) => {
|
||||
if (queue === undefined) return;
|
||||
|
||||
this.http.post(`${this.baseUrl}/clients/server/power-off`, {
|
||||
clients: this.clientData.map((client: any) => client['@id']),
|
||||
queue
|
||||
}).subscribe(
|
||||
() => { this.toastService.success(this.translate.instant('shutdownRequestSentCorrectly')); },
|
||||
(error: any) => { this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente'); }
|
||||
);
|
||||
});
|
||||
this.http.post(`${this.baseUrl}/clients/server/power-off`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Petición de apagado enviada correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
openPartitionAssistant(): void {
|
||||
|
|
@ -538,124 +371,4 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
deleteClientLease(): void {
|
||||
if (this.clientData.length === 0) {
|
||||
this.toastService.error(this.translate.instant('noClientsSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
const requests = this.clientData.map((client: any) =>
|
||||
this.http.delete(`${this.baseUrl}/clients/${client.uuid}/delete-lease`).toPromise()
|
||||
);
|
||||
|
||||
Promise.allSettled(requests).then(results => {
|
||||
const errors = results.filter(r => r.status === 'rejected');
|
||||
if (errors.length === 0) {
|
||||
this.toastService.success(this.translate.instant('dhcpLeasesRemovedSuccessfully'));
|
||||
} else if (errors.length < results.length) {
|
||||
this.toastService.warning(`${results.length - errors.length} lease(s) eliminada(s), ${errors.length} con error`);
|
||||
} else {
|
||||
this.toastService.error(this.translate.instant('errorWhenDeletingLeases'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private executePluginCommand(pluginCmd: PluginCommand): void {
|
||||
if (!this.clientData || this.clientData.length === 0) {
|
||||
this.toastService.warning('No hay ningún equipo seleccionado para ejecutar esta acción.', pluginCmd.label);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasParams = !!(pluginCmd.params && pluginCmd.params.length > 0);
|
||||
const hasConfirm = !!(pluginCmd.confirm || pluginCmd.confirmKey);
|
||||
|
||||
if (hasParams || hasConfirm) {
|
||||
const dialogRef = this.dialog.open(PluginCommandModalComponent, {
|
||||
width: '450px',
|
||||
data: {
|
||||
command: pluginCmd,
|
||||
clients: this.clientData
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (!result || !result.confirmed) {
|
||||
return; // Cancelado por el usuario
|
||||
}
|
||||
const customParams = result.params || {};
|
||||
this.promptQueueAndSendPluginCommand(pluginCmd, customParams);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.promptQueueAndSendPluginCommand(pluginCmd, {});
|
||||
}
|
||||
|
||||
private promptQueueAndSendPluginCommand(pluginCmd: PluginCommand, extraParams: Record<string, any>): void {
|
||||
const requiredOs = (pluginCmd.os || []).map(os => os.toLowerCase());
|
||||
|
||||
// 1. Equipos en un sistema operativo no coincidente (ej. Linux u og-live cuando se requiere Windows)
|
||||
const incompatibleRunningClients = this.clientData.filter(c => {
|
||||
const status = (c.status || '').toLowerCase();
|
||||
const isOnline = status !== 'off' && status !== 'disconnected' && status !== '';
|
||||
if (!isOnline || requiredOs.length === 0) return false;
|
||||
return !requiredOs.some(req => status.includes(req));
|
||||
});
|
||||
|
||||
// 2. Equipos apagados o desconectados
|
||||
const offlineClients = this.clientData.filter(c => {
|
||||
const status = (c.status || '').toLowerCase();
|
||||
return !status || status === 'off' || status === 'disconnected';
|
||||
});
|
||||
|
||||
// Si hay equipos no listos inmediatamente (apagados o en otro SO), ofrecer encolado
|
||||
if (offlineClients.length > 0 || incompatibleRunningClients.length > 0) {
|
||||
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
|
||||
width: '400px',
|
||||
data: { clients: this.clientData }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((queue: boolean | undefined) => {
|
||||
if (queue === undefined) {
|
||||
// El usuario canceló o cerró el modal
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendPluginCommandRequest(pluginCmd, queue, extraParams);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Todos los equipos están en línea en el sistema operativo correcto
|
||||
this.sendPluginCommandRequest(pluginCmd, false, extraParams);
|
||||
}
|
||||
|
||||
private sendPluginCommandRequest(pluginCmd: PluginCommand, queue: boolean, extraParams: Record<string, any> = {}): void {
|
||||
if (!pluginCmd.endpoint) {
|
||||
this.toastService.error('El comando no especifica un endpoint de ejecución', pluginCmd.label);
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanEndpoint = pluginCmd.endpoint.replace(/^\/api/, '');
|
||||
const url = `${this.baseUrl}${cleanEndpoint}`;
|
||||
const clientIds = this.clientData.map(c => c.id || c['@id']);
|
||||
|
||||
this.http.post<any>(url, {
|
||||
clients: clientIds,
|
||||
queue: queue,
|
||||
target_os: pluginCmd.os,
|
||||
...extraParams
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
const msg = response?.message || 'Comando procesado correctamente';
|
||||
this.toastService.success(msg, pluginCmd.label);
|
||||
this.pluginService.loadActivePluginsStatus().subscribe();
|
||||
},
|
||||
error: (error) => {
|
||||
const errorMsg = error?.error?.message || error?.error?.['hydra:description'] || 'Error al comunicar con el servidor';
|
||||
this.toastService.error(errorMsg, pluginCmd.label);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.mat-dialog-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<h2 mat-dialog-title>{{ 'executeTaskNowModalTitle' | translate }}</h2>
|
||||
|
||||
<mat-dialog-content class="form-container">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectTask' | translate }}</mat-label>
|
||||
<input type="text" matInput [formControl]="taskControl" [matAutocomplete]="taskAuto"
|
||||
[placeholder]="'labelSearchTask' | translate" #taskInput
|
||||
#taskAutoTrigger="matAutocompleteTrigger">
|
||||
|
||||
<button type="button" mat-icon-button matSuffix tabindex="-1"
|
||||
[disabled]="tasksLoading || tasks.length === 0"
|
||||
(click)="taskInput.focus(); taskAutoTrigger.openPanel()">
|
||||
<mat-icon>arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-autocomplete #taskAuto="matAutocomplete"
|
||||
[displayWith]="displayFn" autoActiveFirstOption
|
||||
(optionSelected)="onTaskSelected($event.option.value)">
|
||||
<mat-option *ngFor="let task of filteredTasks$ | async" [value]="task">
|
||||
{{ task.name }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<p *ngIf="tasksLoading" class="hint-text">{{ 'loadingDots' | translate }}</p>
|
||||
<p *ngIf="!tasksLoading && tasks.length === 0">{{ 'noTasksAvailableMessage' | translate }}</p>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="closeModal()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="executeNow()" [disabled]="!selectedTask">
|
||||
{{ 'executeButton' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-task-now',
|
||||
templateUrl: './execute-task-now.component.html',
|
||||
styleUrls: ['./execute-task-now.component.css']
|
||||
})
|
||||
export class ExecuteTaskNowComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
tasks: any[] = [];
|
||||
tasksLoading = true;
|
||||
taskControl = new FormControl();
|
||||
filteredTasks$!: Observable<any[]>;
|
||||
selectedTask: any = null;
|
||||
private tasks$ = new BehaviorSubject<any[]>([]);
|
||||
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<ExecuteTaskNowComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTasks();
|
||||
|
||||
this.filteredTasks$ = combineLatest([
|
||||
this.taskControl.valueChanges.pipe(startWith('')),
|
||||
this.tasks$
|
||||
]).pipe(
|
||||
map(([value]) => this._filterTasks(typeof value === 'string' ? value : value?.name))
|
||||
);
|
||||
}
|
||||
|
||||
loadTasks(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/command-tasks?page=1&itemsPerPage=100`).subscribe(
|
||||
response => {
|
||||
this.tasks = response['hydra:member'] || [];
|
||||
this.tasksLoading = false;
|
||||
this.tasks$.next(this.tasks);
|
||||
},
|
||||
error => {
|
||||
this.tasksLoading = false;
|
||||
this.toastService.error(this.translate.instant('errorLoadingTasks'), error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _filterTasks(value: string): any[] {
|
||||
const filterValue = (value || '').toLowerCase();
|
||||
return this.tasks.filter(task => task.name.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
displayFn(task: any): string {
|
||||
return task && task.name ? task.name : '';
|
||||
}
|
||||
|
||||
onTaskSelected(task: any): void {
|
||||
this.selectedTask = task;
|
||||
}
|
||||
|
||||
executeNow(): void {
|
||||
if (!this.selectedTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clients = this.data?.clients || [];
|
||||
|
||||
if (clients.length === 0) {
|
||||
this.toastService.error(this.translate.instant('noClientsSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
clients: clients.map((client: any) => client['@id'] || `/clients/${client.uuid}`)
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/command-tasks/${this.selectedTask.uuid}/execute`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('taskExecutedSuccessfully'));
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastService.error(this.translate.instant('errorExecutingTaskNow'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeModal(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +1,105 @@
|
|||
<h2 mat-dialog-title> {{ 'selectImageToDeleteFromCache' | translate }}</h2>
|
||||
<h2 mat-dialog-title> Seleccionar imagen para eliminar de la cache</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
@if (loading) {
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
}
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
@if (!loading) {
|
||||
<div class="select-container">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> {{ 'labelClients' | translate }}</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
{{ 'clientListBootOs' | translate }}<mat-icon>desktop_windows</mat-icon>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="!loading" class="select-container">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Clientes </mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Listado de clientes para arrancar un SO
|
||||
<mat-icon>desktop_windows</mat-icon>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="toggleSelectAll()">
|
||||
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="toggleSelectAll()">
|
||||
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clients-grid">
|
||||
@for (client of data.clients; track client['@id']) {
|
||||
<div class="client-item">
|
||||
<div class="client-card"
|
||||
(click)="client.status === 'og-live' && toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}" >
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of data.clients" class="client-item">
|
||||
<div class="client-card"
|
||||
(click)="client.status === 'og-live' && toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}" >
|
||||
|
||||
<img
|
||||
[src]="'assets/images/computer_' + client.status + '.svg'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
<img
|
||||
[src]="'assets/images/computer_' + client.status + '.svg'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name | slice:0:20 }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
|
||||
<mat-radio-button [value]="client"
|
||||
color="primary"
|
||||
[disabled]="!client.selected"
|
||||
(click)="$event.stopPropagation()">
|
||||
{{ 'labelModel' | translate }}</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name | slice:0:20 }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div class="partition-table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">{{ 'buttonSelectImage' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group [(ngModel)]="selectedImage">
|
||||
<mat-radio-button [value]="row"></mat-radio-button>
|
||||
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
|
||||
<mat-radio-button [value]="client"
|
||||
color="primary"
|
||||
[disabled]="!client.selected"
|
||||
(click)="$event.stopPropagation()">
|
||||
Modelo
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@for (column of columns; track column.columnDef) {
|
||||
<ng-container [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
{{ column.cell(image) }}
|
||||
</td>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div *ngIf="!loading" class="partition-table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar imagen</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group
|
||||
[(ngModel)]="selectedPartition"
|
||||
[disabled]="!row.operativeSystem"
|
||||
>
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef !== 'size' && column.columnDef !== 'operativeSystem'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'operativeSystem'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.operativeSystem?.name }} </span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.image?.name}} </span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedImage">{{ 'buttonExecute' | translate }}</button>
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {Component, Inject } from '@angular/core';
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
|
|
@ -13,9 +11,8 @@ import {ToastrService} from "ngx-toastr";
|
|||
styleUrl: './remove-cache-image.component.css'
|
||||
})
|
||||
export class RemoveCacheImageComponent {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
selectedImage: any = null;
|
||||
selectedPartition: any = null;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
clientId: string | null = null;
|
||||
selectedClients: any[] = [];
|
||||
|
|
@ -26,14 +23,34 @@ export class RemoveCacheImageComponent {
|
|||
loading: boolean = false;
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'name',
|
||||
cell: (image: any) => image.name
|
||||
columnDef: 'diskNumber',
|
||||
header: 'Disco',
|
||||
cell: (partition: any) => partition.diskNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionNumber',
|
||||
header: 'Particion',
|
||||
cell: (partition: any) => partition.partitionNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'size',
|
||||
header: 'size',
|
||||
cell: (image: any) => this.formatBytes(image.size)
|
||||
header: 'Tamaño',
|
||||
cell: (partition: any) => `${partition.size} MB`
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionCode',
|
||||
header: 'Tipo de partición',
|
||||
cell: (partition: any) => partition.partitionCode
|
||||
},
|
||||
{
|
||||
columnDef: 'filesystem',
|
||||
header: 'Sistema de ficheros',
|
||||
cell: (partition: any) => partition.filesystem
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => partition.operativeSystem?.name
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -77,60 +94,17 @@ export class RemoveCacheImageComponent {
|
|||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
const cachePartitions = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0 && partition.filesystem === 'CACHE' && partition.cacheContent !== "";
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0 && partition.image;
|
||||
});
|
||||
|
||||
let imagesMap: { [key: string]: number } = {};
|
||||
|
||||
cachePartitions.forEach((p: any) => {
|
||||
if (p.cacheContent) {
|
||||
const items = p.cacheContent.split(',');
|
||||
items.forEach((item: string) => {
|
||||
const parts = item.split(':');
|
||||
if (parts.length >= 2) {
|
||||
const filename = parts[0];
|
||||
let size = 0;
|
||||
|
||||
let baseName = filename;
|
||||
if (baseName.endsWith('.img.full.sum')) baseName = baseName.replace('.img.full.sum', '');
|
||||
else if (baseName.endsWith('.img.sum')) baseName = baseName.replace('.img.sum', '');
|
||||
// El tamaño de la imagen que usamos es sólo del fichero .img
|
||||
else if (baseName.endsWith('.img')) {
|
||||
baseName = baseName.replace('.img', '');
|
||||
size = parseInt(parts[1], 10) || 0;
|
||||
}
|
||||
|
||||
if (!imagesMap[baseName]) {
|
||||
imagesMap[baseName] = 0;
|
||||
}
|
||||
imagesMap[baseName] += size;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.dataSource.data = Object.keys(imagesMap).map(name => ({
|
||||
name: name,
|
||||
size: imagesMap[name]
|
||||
}));
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('errorLoadingData1'), error);
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
toggleClientSelection(client: any) {
|
||||
client.selected = !client.selected;
|
||||
this.updateSelectedClients();
|
||||
|
|
@ -164,10 +138,10 @@ export class RemoveCacheImageComponent {
|
|||
this.loading = true;
|
||||
this.http.post(`${this.baseUrl}/clients/server/remove-cache-image`, {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
imageName: this.selectedImage.name
|
||||
partition: this.selectedPartition['@id']
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success(this.translate.instant('clientUpdatedSuccessfully'));
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<h2 mat-dialog-title> {{ 'selectPartitionToInventory' | translate }}</h2>
|
||||
<h2 mat-dialog-title> Seleccionar partición para inventariar</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
|
@ -6,17 +6,50 @@
|
|||
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div *ngIf="!loading" class="partition-table-container">
|
||||
<app-disk-partition-selector
|
||||
[(selectedPartition)]="selectedPartition"
|
||||
[partitions]="dataSource.data"
|
||||
[columns]="columns"
|
||||
[displayedColumns]="displayedColumns"
|
||||
[disableCondition]="isPartitionDisabled">
|
||||
</app-disk-partition-selector>
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar imagen</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group
|
||||
[(ngModel)]="selectedPartition"
|
||||
>
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef !== 'size' && column.columnDef !== 'operativeSystem'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'operativeSystem'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.operativeSystem?.name }} </span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.image?.name}} </span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">{{ 'buttonExecute' | translate }}</button>
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
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 {MatTableDataSource} from "@angular/material/table";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
|
|
@ -13,7 +11,6 @@ import {ToastrService} from "ngx-toastr";
|
|||
styleUrl: './software-profile-partition.component.css'
|
||||
})
|
||||
export class SoftwareProfilePartitionComponent implements OnInit{
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
selectedPartition: any = null;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
|
@ -27,42 +24,38 @@ export class SoftwareProfilePartitionComponent implements OnInit{
|
|||
columns = [
|
||||
{
|
||||
columnDef: 'diskNumber',
|
||||
header: 'labelDisk',
|
||||
header: 'Disco',
|
||||
cell: (partition: any) => partition.diskNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionNumber',
|
||||
header: 'partition',
|
||||
header: 'Particion',
|
||||
cell: (partition: any) => partition.partitionNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'size',
|
||||
header: 'labelDiskSize',
|
||||
header: 'Tamaño',
|
||||
cell: (partition: any) => `${partition.size} MB`
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionCode',
|
||||
header: 'partitionType',
|
||||
header: 'Tipo de partición',
|
||||
cell: (partition: any) => partition.partitionCode
|
||||
},
|
||||
{
|
||||
columnDef: 'filesystem',
|
||||
header: 'fileSystem',
|
||||
header: 'Sistema de ficheros',
|
||||
cell: (partition: any) => partition.filesystem
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'sW',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => partition.operativeSystem?.name
|
||||
}
|
||||
];
|
||||
|
||||
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
|
||||
|
||||
isPartitionDisabled = (partition: any): boolean => {
|
||||
return !partition.operativeSystem;
|
||||
};
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: { client: any },
|
||||
private dialogRef: MatDialogRef<SoftwareProfilePartitionComponent>,
|
||||
|
|
@ -83,11 +76,11 @@ export class SoftwareProfilePartitionComponent implements OnInit{
|
|||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.dataSource.data = response.partitions.filter((p: any) => p.partitionCode !== 'CACHE' && p.partitionCode !== 'EMPTY');
|
||||
this.dataSource.data = response.partitions;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('errorLoadingData1'), error);
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -103,7 +96,7 @@ export class SoftwareProfilePartitionComponent implements OnInit{
|
|||
partition: this.selectedPartition['@id'],
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success(this.translate.instant('successfullyUpdatedSoftwareInventory'));
|
||||
this.toastService.success('Inventario de software actualizado correctamente');
|
||||
this.dialogRef.close(response);
|
||||
},
|
||||
error => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
/* ===== HEADER DE BIENVENIDA ===== */
|
||||
.welcome-header {
|
||||
background: linear-gradient(120deg, #3b3080 0%, #2d2b55 50%, #1e1e2f 100%);
|
||||
background: #3f51b5;
|
||||
color: white;
|
||||
padding: 1.2rem 1.5rem;
|
||||
padding: 2rem 2rem 1.5rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.welcome-header::before {
|
||||
|
|
@ -25,119 +24,62 @@
|
|||
.welcome-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 1.5rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.welcome-icon mat-icon {
|
||||
font-size: 26px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
margin: 0 0 0.2rem 0;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.welcome-description {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
opacity: 0.85;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== PANEL DE VERSIONES ===== */
|
||||
.release-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.release-channel {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.release-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
justify-content: flex-end;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.release-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: default;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.release-chip:hover {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.chip-name {
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chip-version {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.help-button,
|
||||
.refresh-button {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
color: white !important;
|
||||
|
|
@ -146,6 +88,7 @@
|
|||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.help-button:hover,
|
||||
.refresh-button:hover {
|
||||
background: rgba(255, 255, 255, 0.3) !important;
|
||||
transform: translateY(-2px);
|
||||
|
|
@ -161,7 +104,7 @@ mat-dialog-content {
|
|||
}
|
||||
|
||||
.main-content {
|
||||
padding: 1.25rem 1.5rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* ===== SPINNER DE CARGA ===== */
|
||||
|
|
@ -200,19 +143,19 @@ mat-dialog-content {
|
|||
/* ===== RESUMEN DEL SISTEMA ===== */
|
||||
.system-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07);
|
||||
gap: 1rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
|
|
@ -259,8 +202,8 @@ mat-dialog-content {
|
|||
.overview-icon {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -269,31 +212,24 @@ mat-dialog-content {
|
|||
|
||||
.overview-icon mat-icon {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.overview-content h3 {
|
||||
margin: 0 0 0.3rem 0;
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.overview-content p {
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.release-version {
|
||||
margin-top: 0.25rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
color: #667eea !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.click-hint {
|
||||
margin-top: 0.5rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
|
|
@ -475,7 +411,7 @@ mat-dialog-content {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.6rem 1.25rem;
|
||||
padding: 1.5rem 2rem;
|
||||
background: white;
|
||||
border-top: 1px solid #e9ecef;
|
||||
border-radius: 0 0 12px 12px;
|
||||
|
|
@ -511,26 +447,23 @@ mat-dialog-content {
|
|||
/* ===== RESPONSIVE ===== */
|
||||
@media (max-width: 768px) {
|
||||
.welcome-header {
|
||||
padding: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem 1rem 1rem 1rem;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.release-panel {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.release-chips {
|
||||
justify-content: flex-start;
|
||||
max-width: 100%;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
|
|
@ -538,12 +471,12 @@ mat-dialog-content {
|
|||
}
|
||||
|
||||
.system-overview {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
padding: 0.75rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
|
|
@ -617,7 +550,6 @@ mat-dialog-content {
|
|||
.overview-card:nth-child(1) { animation-delay: 0.1s; }
|
||||
.overview-card:nth-child(2) { animation-delay: 0.2s; }
|
||||
.overview-card:nth-child(3) { animation-delay: 0.3s; }
|
||||
.overview-card:nth-child(4) { animation-delay: 0.4s; }
|
||||
|
||||
/* ===== ESTILOS GLOBALES DEL DIALOG ===== */
|
||||
::ng-deep .mat-dialog-container {
|
||||
|
|
@ -770,10 +702,6 @@ mat-dialog-content {
|
|||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.overview-card.clickable.active .release-version {
|
||||
color: rgba(255, 255, 255, 0.85) !important;
|
||||
}
|
||||
|
||||
/* ===== CONTENIDO DINÁMICO ===== */
|
||||
.dynamic-content {
|
||||
background: white;
|
||||
|
|
|
|||
|
|
@ -5,106 +5,292 @@
|
|||
</div>
|
||||
<div class="welcome-text">
|
||||
<h1 class="welcome-title">{{ 'GlobalStatus' | translate }}</h1>
|
||||
<p class="welcome-subtitle">{{ 'welcomeTitle' | translate }}</p>
|
||||
<p class="welcome-description">{{ 'generalPlatformStatus' | translate }}</p>
|
||||
<p class="welcome-subtitle">Bienvenido a la consola de administración de OpenGnsys</p>
|
||||
<p class="welcome-description">Estado general de todos los componentes de la plataforma</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="welcome-actions">
|
||||
<button mat-icon-button class="refresh-button" (click)="refreshAll()" [matTooltip]="'buttonUpdateData' | translate">
|
||||
<button mat-icon-button class="refresh-button" (click)="refreshAll()" matTooltip="Actualizar datos">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-dialog-content [ngClass]="{'loading': loading}">
|
||||
@if(loading) {
|
||||
<div class="spinner-container">
|
||||
<div class="spinner-container" *ngIf="loading">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">{{ 'loadingSystemStatus' | translate }}</p>
|
||||
<p class="loading-text">Cargando estado del sistema...</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if(!loading) {
|
||||
<div class="main-content">
|
||||
<div *ngIf="!loading" class="main-content">
|
||||
<div class="system-overview">
|
||||
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'repositories'}"
|
||||
(click)="selectSection('repositories')">
|
||||
<div class="overview-card clickable"
|
||||
[ngClass]="{'active': selectedSection === 'repositories'}"
|
||||
(click)="selectSection('repositories')">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>cloud</mat-icon>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<h3>{{ 'labelRepository' | translate }}</h3>
|
||||
<p>{{ 'labelTotalColon' | translate }}<span class="status-badge info">{{ repositories.length }}</span></p>
|
||||
@if(releaseInfo?.['ogrepository']) {
|
||||
<p class="release-version">v{{ releaseInfo!['ogrepository'] }}</p>
|
||||
}
|
||||
<h3>Repositorios</h3>
|
||||
<p>Total: <span class="status-badge info">{{ repositories.length }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'ogboot'}"
|
||||
(click)="selectSection('ogboot')">
|
||||
|
||||
<div class="overview-card clickable"
|
||||
[ngClass]="{'active': selectedSection === 'ogboot'}"
|
||||
(click)="selectSection('ogboot')">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>storage</mat-icon>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<h3>{{ 'ogbootServers' | translate }}</h3>
|
||||
<p>{{ 'labelTotalColon' | translate }}<span class="status-badge info">{{ bootServers.length }}</span></p>
|
||||
@if(releaseInfo?.['ogboot']) {
|
||||
<p class="release-version">v{{ releaseInfo!['ogboot'] }}</p>
|
||||
}
|
||||
<h3>OgBoot Server</h3>
|
||||
<p *ngIf="!errorOgBoot">Estado: <span class="status-badge online">Operativo</span></p>
|
||||
<p *ngIf="errorOgBoot">Estado: <span class="status-badge offline">Error</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'dhcp'}"
|
||||
(click)="selectSection('dhcp')">
|
||||
|
||||
<div class="overview-card clickable"
|
||||
[ngClass]="{'active': selectedSection === 'dhcp'}"
|
||||
(click)="selectSection('dhcp')">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>router</mat-icon>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<h3>{{ 'dhcpServers' | translate }}</h3>
|
||||
<p>{{ 'labelTotalColon' | translate }}<span class="status-badge info">{{ dhcps.length }}</span></p>
|
||||
@if(releaseInfo?.['ogdhcp']) {
|
||||
<p class="release-version">v{{ releaseInfo!['ogdhcp'] }}</p>
|
||||
}
|
||||
<h3>DHCP Server</h3>
|
||||
<p *ngIf="!errorDhcp">Estado: <span class="status-badge online">Operativo</span></p>
|
||||
<p *ngIf="errorDhcp">Estado: <span class="status-badge offline">Error</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'ogcore'}"
|
||||
(click)="selectSection('ogcore')">
|
||||
|
||||
<div class="overview-card clickable"
|
||||
[ngClass]="{'active': selectedSection === 'ogcore'}"
|
||||
(click)="selectSection('ogcore')">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>dns</mat-icon>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<h3>{{ 'ogCoreServer' | translate }}</h3>
|
||||
<p>{{ 'serverLogs' | translate }}</p>
|
||||
@if(releaseInfo?.['ogcore']) {
|
||||
<p class="release-version">v{{ releaseInfo!['ogcore'] }}</p>
|
||||
}
|
||||
<h3>OgCore Server</h3>
|
||||
<p>Logs del servidor</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dynamic-content">
|
||||
@if(selectedSection === 'repositories') {
|
||||
<app-ogrepository-status [repositories]="repositories"></app-ogrepository-status>
|
||||
}
|
||||
<div *ngIf="selectedSection === 'repositories'" class="section-content">
|
||||
<div class="repositories-container">
|
||||
<div *ngIf="repositories.length === 0" class="no-repositories">
|
||||
<mat-icon class="no-data-icon">cloud_off</mat-icon>
|
||||
<h3>No hay repositorios disponibles</h3>
|
||||
<p>No se encontraron repositorios configurados en el sistema.</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="repositories.length > 0" class="repositories-selector-container">
|
||||
<div class="repository-selector">
|
||||
<mat-form-field appearance="outline" class="repository-select-field">
|
||||
<mat-label>Seleccionar repositorio</mat-label>
|
||||
<mat-select [(ngModel)]="selectedRepositoryUuid" (selectionChange)="onRepositoryChange($event.value)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository.uuid">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix>storage</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]" class="selected-repository-content">
|
||||
<div class="repository-item">
|
||||
<div class="section-header">
|
||||
<h2>{{ getSelectedRepositoryName() }}</h2>
|
||||
<button class="logs-button" (click)="scrollToLogs()">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>Ver logs</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="repository-content">
|
||||
<app-status-tab [loading]="loading" [diskUsage]="repositoryStatuses[selectedRepositoryUuid].disk"
|
||||
[servicesStatus]="repositoryStatuses[selectedRepositoryUuid].services"
|
||||
[processesStatus]="repositoryStatuses[selectedRepositoryUuid].processes"
|
||||
[ramUsage]="repositoryStatuses[selectedRepositoryUuid].ram" [cpuUsage]="repositoryStatuses[selectedRepositoryUuid].cpu"
|
||||
[diskUsageChartData]="[
|
||||
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].disk.used },
|
||||
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].disk.available }
|
||||
]" [ramUsageChartData]="[
|
||||
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].ram.used },
|
||||
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].ram.available }
|
||||
]" [view]="view" [colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels"
|
||||
[isDhcp]="false" [isRepository]="true">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repository-logs-section" id="repository-logs-section">
|
||||
<div class="logs-header">
|
||||
<h3>Datos del repositorio: {{ getSelectedRepositoryName() }}</h3>
|
||||
<p>Información específica del repositorio seleccionado</p>
|
||||
</div>
|
||||
<div class="logs-container">
|
||||
<iframe
|
||||
[src]="getRepositoryIframeUrl()"
|
||||
width="100%"
|
||||
height="600"
|
||||
frameborder="0"
|
||||
class="repository-logs-iframe">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!selectedRepositoryUuid" class="no-repository-selected">
|
||||
<mat-icon class="no-data-icon">storage</mat-icon>
|
||||
<h3>Selecciona un repositorio</h3>
|
||||
<p>Elige un repositorio de la lista para ver su estado detallado.</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el repositorio seleccionado</p>
|
||||
<button mat-raised-button color="primary" (click)="retryRepositoryStatus(selectedRepositoryUuid)">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(selectedSection === 'ogboot') {
|
||||
<app-global-ogboot-status [bootServers]="bootServers"></app-global-ogboot-status>
|
||||
}
|
||||
<div *ngIf="selectedSection === 'ogboot'" class="section-content">
|
||||
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
|
||||
<div class="section-header">
|
||||
<h2>OgBoot Server</h2>
|
||||
<button class="logs-button" (click)="scrollToLogs()">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>Ver logs</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<app-status-tab [loading]="loadingOgBoot" [diskUsage]="ogBootDiskUsage" [servicesStatus]="ogBootServicesStatus"
|
||||
[installedOgLives]="installedOgLives" [diskUsageChartData]="ogBootDiskUsageChartData" [view]="view"
|
||||
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp"
|
||||
[isRepository]="false">
|
||||
</app-status-tab>
|
||||
|
||||
<div class="logs-section" id="ogboot-logs-section">
|
||||
<div class="logs-header">
|
||||
<h3>Logs de OgBoot</h3>
|
||||
<p>Logs en tiempo real del servidor OgBoot</p>
|
||||
</div>
|
||||
<div class="logs-container">
|
||||
<iframe
|
||||
[src]="getOgBootIframeUrl()"
|
||||
width="100%"
|
||||
height="800"
|
||||
frameborder="0"
|
||||
class="ogboot-logs-iframe">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="loadingOgBoot" class="loading-container">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">Cargando estado de OgBoot...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="errorOgBoot" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el servidor OgBoot</p>
|
||||
<button mat-raised-button color="primary" (click)="loadOgBootStatus()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(selectedSection === 'dhcp') {
|
||||
<app-ogdhcp-status [dhcps]="dhcps"></app-ogdhcp-status>
|
||||
}
|
||||
<div *ngIf="selectedSection === 'dhcp'" class="section-content">
|
||||
<div *ngIf="!errorDhcp && !loadingDhcp" class="tab-content">
|
||||
<div class="section-header">
|
||||
<h2>DHCP Server</h2>
|
||||
<button class="logs-button" (click)="scrollToLogs()">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>Ver logs</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<app-status-tab [loading]="loadingDhcp" [diskUsage]="dhcpDiskUsage" [servicesStatus]="dhcpServicesStatus"
|
||||
[subnets]="subnets" [diskUsageChartData]="dhcpDiskUsageChartData" [view]="view" [colorScheme]="colorScheme"
|
||||
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true" [isRepository]="false">
|
||||
</app-status-tab>
|
||||
|
||||
<div class="logs-section" id="dhcp-logs-section">
|
||||
<div class="logs-header">
|
||||
<h3>Logs de DHCP</h3>
|
||||
<p>Logs en tiempo real del servidor DHCP</p>
|
||||
</div>
|
||||
<div class="logs-container">
|
||||
<iframe
|
||||
[src]="getDhcpIframeUrl()"
|
||||
width="100%"
|
||||
height="800"
|
||||
frameborder="0"
|
||||
class="dhcp-logs-iframe">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="loadingDhcp" class="loading-container">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">Cargando estado de DHCP...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="errorDhcp" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el servidor DHCP</p>
|
||||
<button mat-raised-button color="primary" (click)="loadDhcpStatus()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(selectedSection === 'ogcore') {
|
||||
<app-ogcore-status></app-ogcore-status>
|
||||
}
|
||||
<div *ngIf="selectedSection === 'ogcore'" class="section-content">
|
||||
<div class="tab-content">
|
||||
<div class="section-header">
|
||||
<h2>OgCore Server</h2>
|
||||
</div>
|
||||
|
||||
<div class="logs-section" id="ogcore-logs-section">
|
||||
<div class="logs-header">
|
||||
<h3>Logs de OgCore</h3>
|
||||
<p>Logs en tiempo real del servidor OgCore</p>
|
||||
</div>
|
||||
<div class="logs-container">
|
||||
<iframe
|
||||
[src]="getOgCoreIframeUrl()"
|
||||
width="100%"
|
||||
height="800"
|
||||
frameborder="0"
|
||||
class="ogcore-logs-iframe">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
|
|
@ -114,9 +300,10 @@
|
|||
<div class="action-buttons">
|
||||
<button mat-button class="secondary-button" (click)="refreshAll()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
{{ 'buttonUpdate' | translate }}</button>
|
||||
Actualizar
|
||||
</button>
|
||||
<button mat-raised-button color="primary" [mat-dialog-close]="true">
|
||||
{{ 'buttonClose' | translate }}
|
||||
{{ 'closeButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { AuthService } from '@services/auth.service';
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-global-status',
|
||||
|
|
@ -12,17 +10,15 @@ import { AuthService } from '@services/auth.service';
|
|||
styleUrl: './global-status.component.css'
|
||||
})
|
||||
export class GlobalStatusComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
loading: boolean = false;
|
||||
loadingDhcps: boolean = false;
|
||||
loadingOgBoot: boolean = false;
|
||||
loadingDhcp: boolean = false;
|
||||
loadingRepositories: boolean = false;
|
||||
loadingBootServers: boolean = false;
|
||||
|
||||
errorDhcps: { [key: string]: boolean } = {};
|
||||
errorOgBoot: boolean = false;
|
||||
errorDhcp: boolean = false;
|
||||
errorRepositories: { [key: string]: boolean } = {};
|
||||
errorBootServers: { [key: string]: boolean } = {};
|
||||
|
||||
installedOgLives: any[] = [];
|
||||
subnets: any[] = [];
|
||||
showLabels: boolean = true;
|
||||
isDoughnut: boolean = true;
|
||||
|
|
@ -30,34 +26,27 @@ export class GlobalStatusComponent implements OnInit {
|
|||
domain: ['#df200d', '#26a700']
|
||||
};
|
||||
view: [number, number] = [400, 220];
|
||||
|
||||
repositoriesUrl: string;
|
||||
repositories: any[] = [];
|
||||
repositoryStatuses: { [key: string]: any } = {};
|
||||
lastUpdateTime: string = '';
|
||||
selectedRepositoryUuid: string = '';
|
||||
selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'ogcore';
|
||||
selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'repositories';
|
||||
|
||||
ogBootApiUrl: string;
|
||||
ogBootDiskUsage: any = {};
|
||||
ogBootServicesStatus: any = {};
|
||||
ogBootDiskUsageChartData: any[] = [];
|
||||
|
||||
dhcpsApiUrl: string;
|
||||
dhcps: any[] = [];
|
||||
dhcpStatuses: { [key: string]: any } = {};
|
||||
selectedDhcpUuid: string = '';
|
||||
|
||||
bootServersApiUrl: string;
|
||||
bootServers: any[] = [];
|
||||
bootServerStatuses: { [key: string]: any } = {};
|
||||
selectedBootServerUuid: string = '';
|
||||
|
||||
lastUpdateTime: string = '';
|
||||
dhcpApiUrl: string;
|
||||
dhcpDiskUsage: any = {};
|
||||
dhcpServicesStatus: any = {};
|
||||
dhcpDiskUsageChartData: any[] = [];
|
||||
isDhcp: boolean = false;
|
||||
isRepository: boolean = false;
|
||||
|
||||
ogCoreApiUrl: string;
|
||||
|
||||
releaseInfo: Record<string, string> | null = null;
|
||||
releaseLoading: boolean = false;
|
||||
|
||||
loadingOgBootOgLives: boolean = false;
|
||||
loadingOgBootServices: boolean = false;
|
||||
loadingOgBootDisk: boolean = false;
|
||||
|
|
@ -69,247 +58,319 @@ export class GlobalStatusComponent implements OnInit {
|
|||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
private auth: AuthService
|
||||
private sanitizer: DomSanitizer
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.dhcpsApiUrl = `${this.baseUrl}/dhcp-servers`;
|
||||
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
|
||||
this.bootServersApiUrl = `${this.baseUrl}/boot-servers`;
|
||||
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
|
||||
this.dhcpApiUrl = `${this.baseUrl}/og-dhcp/status`;
|
||||
this.ogCoreApiUrl = `${this.baseUrl}`;
|
||||
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
|
||||
|
||||
this.ogBootDiskUsageChartData = [];
|
||||
this.dhcpDiskUsageChartData = [];
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
this.updateLastUpdateTime();
|
||||
|
||||
this.loadReleaseInfo();
|
||||
this.loadDhcps(false);
|
||||
|
||||
this.loadOgBootStatus();
|
||||
this.loadDhcpStatus();
|
||||
this.loadRepositories(false);
|
||||
this.loadDhcps(true);
|
||||
this.loadRepositories(true);
|
||||
this.loadBootServers(true);
|
||||
|
||||
this.loadBootServers(false);
|
||||
|
||||
this.syncSubnets();
|
||||
}
|
||||
|
||||
get releasePackages(): { key: string; value: string }[] {
|
||||
if (!this.releaseInfo) return [];
|
||||
return Object.entries(this.releaseInfo)
|
||||
.filter(([key]) => key !== 'Versión instalada')
|
||||
.map(([key, value]) => ({ key, value }));
|
||||
}
|
||||
|
||||
get releaseChannel(): string {
|
||||
return this.releaseInfo?.['Versión instalada'] ?? '';
|
||||
}
|
||||
|
||||
loadReleaseInfo(): void {
|
||||
this.releaseLoading = true;
|
||||
this.http.get<Record<string, string>>(`${this.baseUrl}/opengnsys/server/release`).subscribe({
|
||||
next: data => {
|
||||
this.releaseInfo = data;
|
||||
this.releaseLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.releaseLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Las sincronizaciones con la infraestructura (subredes DHCP, plantillas PXE y
|
||||
* ogLives) están restringidas a ROLE_SUPER_ADMIN en ogCore; para el resto de
|
||||
* perfiles devuelven "Access Denied", así que ni siquiera se lanzan.
|
||||
*/
|
||||
private get canSyncInfrastructure(): boolean {
|
||||
return this.auth.isSuperAdmin;
|
||||
this.syncTemplates();
|
||||
this.syncOgLives();
|
||||
}
|
||||
|
||||
syncSubnets() {
|
||||
if (!this.canSyncInfrastructure) return;
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.toastService.error('Error al sincronizar las subredes: tiempo de espera agotado');
|
||||
}, 3500);
|
||||
|
||||
this.http.get<any>(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe(data => {
|
||||
const dhcps = data['hydra:member'] || [];
|
||||
if (dhcps.length === 0) return;
|
||||
|
||||
let pending = dhcps.length;
|
||||
let hasError = false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!hasError) {
|
||||
this.toastService.error(this.translate.instant('errorSyncingSubnets'));
|
||||
hasError = true;
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
dhcps.forEach((dhcp: any) => {
|
||||
this.http.post(`${this.baseUrl}/dhcp-servers/${dhcp.uuid}/sync`, {}).subscribe({
|
||||
next: () => {
|
||||
pending--;
|
||||
if (pending === 0 && !hasError) {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.success(this.translate.instant('serverSubnetSynchronization'));
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
pending--;
|
||||
if (!hasError) {
|
||||
hasError = true;
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.error(error.error['hydra:description'] || `Error al sincronizar subredes del DHCP ${dhcp.name || dhcp.uuid}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, error => {
|
||||
console.error(this.translate.instant('errorFetchingDhcpServersFor'), error);
|
||||
this.http.post(`${this.baseUrl}/subnets/sync`, {}).subscribe({
|
||||
next: (response) => {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.success('Sincronización con componente DHCP exitosa');
|
||||
},
|
||||
error: (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las subredes');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
syncTemplates() {
|
||||
if (!this.canSyncInfrastructure) return;
|
||||
if (this.bootServers.length === 0) return;
|
||||
|
||||
let pending = this.bootServers.length;
|
||||
let hasError = false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!hasError) {
|
||||
this.toastService.error(this.translate.instant('errorSyncingTemplates'));
|
||||
hasError = true;
|
||||
}
|
||||
}, 5000);
|
||||
this.toastService.error('Error al sincronizar las plantillas Pxe: tiempo de espera agotado');
|
||||
}, 3500);
|
||||
|
||||
this.bootServers.forEach(server => {
|
||||
this.http.post(`${this.baseUrl}/boot-servers/${server.uuid}/pxe-templates/sync`, {}).subscribe({
|
||||
next: () => {
|
||||
pending--;
|
||||
if (pending === 0 && !hasError) {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.success(this.translate.instant('successfulPxeTemplateSynchronization'));
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
pending--;
|
||||
if (!hasError) {
|
||||
hasError = true;
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.error(error.error?.['hydra:description'] || `Error al sincronizar plantillas Pxe del servidor ${server.name || server.uuid}`);
|
||||
}
|
||||
}
|
||||
this.http.post(`${this.baseUrl}/pxe-templates/sync`, {})
|
||||
.subscribe(response => {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.success('Sincronización de las plantillas Pxe completada');
|
||||
}, error => {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las plantillas Pxe');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
syncOgLives(): void {
|
||||
if (!this.canSyncInfrastructure) return;
|
||||
if (this.bootServers.length === 0) return;
|
||||
|
||||
let pending = this.bootServers.length;
|
||||
let hasError = false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!hasError) {
|
||||
this.toastService.error(this.translate.instant('errorWhenSynchronizingOgliveImages'));
|
||||
hasError = true;
|
||||
}
|
||||
}, 5000);
|
||||
this.toastService.error('Error al sincronizar las imagenes ogLive : tiempo de espera agotado');
|
||||
}, 3500);
|
||||
|
||||
this.bootServers.forEach(server => {
|
||||
this.http.post(`${this.baseUrl}/boot-servers/${server.uuid}/oglives/sync`, {}).subscribe({
|
||||
next: () => {
|
||||
pending--;
|
||||
if (pending === 0 && !hasError) {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.success(this.translate.instant('synchronizationWithOglivesCompleted'));
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
pending--;
|
||||
if (!hasError) {
|
||||
hasError = true;
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.error(error.error?.['hydra:description'] || `Error al sincronizar ogLives del servidor ${server.name || server.uuid}`);
|
||||
}
|
||||
}
|
||||
this.http.post(`${this.baseUrl}/og-lives/sync`, {})
|
||||
.subscribe(response => {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.success('Sincronización con los ogLives completada');
|
||||
}, error => {
|
||||
clearTimeout(timeoutId);
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las imagenes ogLive');
|
||||
});
|
||||
}
|
||||
|
||||
[key: string]: any;
|
||||
|
||||
loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string, showLoading: boolean = true): void {
|
||||
if (isDhcp) {
|
||||
this.loadingDhcp = true;
|
||||
} else {
|
||||
this.loadingOgBoot = true;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
this.loading = true;
|
||||
}
|
||||
this[errorState] = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
if (isDhcp) {
|
||||
this.loadingDhcp = false;
|
||||
} else {
|
||||
this.loadingOgBoot = false;
|
||||
}
|
||||
this[errorState] = true;
|
||||
}, 3500);
|
||||
this.http.get<any>(apiUrl).subscribe({
|
||||
next: data => {
|
||||
diskUsage.used = data.message.disk_usage.used;
|
||||
diskUsage.available = data.message.disk_usage.available;
|
||||
diskUsage.total = data.message.disk_usage.total;
|
||||
diskUsage.percentage = data.message.disk_usage.percentage;
|
||||
|
||||
Object.assign(servicesStatus, data.message.services_status);
|
||||
|
||||
if (isDhcp) {
|
||||
this.subnets.length = 0;
|
||||
if (data.message.subnets) {
|
||||
this.subnets.push(...data.message.subnets);
|
||||
}
|
||||
} else {
|
||||
installedOgLives.length = 0;
|
||||
if (data.message.installed_oglives) {
|
||||
installedOgLives.push(...data.message.installed_oglives);
|
||||
}
|
||||
this.loadingOgBootOgLives = false;
|
||||
}
|
||||
|
||||
diskUsageChartData.length = 0;
|
||||
diskUsageChartData.push(
|
||||
{ name: 'Usado', value: parseFloat(diskUsage.used) },
|
||||
{ name: 'Disponible', value: parseFloat(diskUsage.available) }
|
||||
);
|
||||
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
if (isDhcp) {
|
||||
this.loadingDhcp = false;
|
||||
} else {
|
||||
this.loadingOgBoot = false;
|
||||
}
|
||||
clearTimeout(timeoutId);
|
||||
},
|
||||
error: error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado de ogBoot');
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
if (isDhcp) {
|
||||
this.loadingDhcp = false;
|
||||
} else {
|
||||
this.loadingOgBoot = false;
|
||||
}
|
||||
this[errorState] = true;
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadRepositories(showLoading: boolean = true): void {
|
||||
if (showLoading) this.loadingRepositories = true;
|
||||
this.http.get<any>(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe({
|
||||
next: data => {
|
||||
if (showLoading) {
|
||||
this.loading = true;
|
||||
}
|
||||
this.errorRepositories = {};
|
||||
|
||||
this.http.get<any>(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe(
|
||||
data => {
|
||||
this.repositories = data['hydra:member'] || [];
|
||||
if (showLoading) this.loadingRepositories = false;
|
||||
this.checkOverallLoading();
|
||||
},
|
||||
error: () => {
|
||||
this.repositories = [];
|
||||
if (showLoading) this.loadingRepositories = false;
|
||||
this.checkOverallLoading();
|
||||
this.toastService.error(this.translate.instant('errorLoadingList2'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadBootServers(showLoading: boolean = true): void {
|
||||
if (showLoading) this.loadingBootServers = true;
|
||||
this.http.get<any>(`${this.bootServersApiUrl}?page=1&itemsPerPage=10`).subscribe({
|
||||
next: data => {
|
||||
this.bootServers = data['hydra:member'] || [];
|
||||
if (showLoading) this.loadingBootServers = false;
|
||||
|
||||
if (this.bootServers.length > 0) {
|
||||
this.syncTemplates();
|
||||
this.syncOgLives();
|
||||
if (this.repositories.length === 0) {
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let remainingRepositories = this.repositories.length;
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
this.repositories.forEach(repository => {
|
||||
if (!(repository.uuid in this.errorRepositories)) {
|
||||
this.errorRepositories[repository.uuid] = true;
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
this.repositories.forEach(repository => {
|
||||
this.loadRepositoryStatus(repository.uuid, (errorOccurred: boolean) => {
|
||||
remainingRepositories--;
|
||||
|
||||
this.errorRepositories[repository.uuid] = errorOccurred;
|
||||
|
||||
if (remainingRepositories === 0) {
|
||||
if (showLoading) {
|
||||
this.loading = false;
|
||||
}
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
error => {
|
||||
this.loading = false;
|
||||
this.repositories = [];
|
||||
this.toastService.error('Error al cargar la lista de repositorios');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
|
||||
console.log(`Cargando estado del repositorio: ${repositoryUuid}`);
|
||||
const timeoutId = setTimeout(() => {
|
||||
callback(true);
|
||||
}, 5000);
|
||||
|
||||
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${repositoryUuid}/status`).subscribe(
|
||||
data => {
|
||||
|
||||
this.checkOverallLoading();
|
||||
if (!data.success) {
|
||||
clearTimeout(timeoutId);
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const details = data.details;
|
||||
const { disk, services, ram, cpu, processes } = details;
|
||||
|
||||
this.repositoryStatuses[repositoryUuid] = {
|
||||
disk: {
|
||||
...disk,
|
||||
used: parseFloat(disk.used.replace('GB', '')),
|
||||
available: parseFloat(disk.available.replace('GB', '')),
|
||||
total: parseFloat(disk.total.replace('GB', '')),
|
||||
percentage: disk.used_percentage
|
||||
},
|
||||
ram: {
|
||||
...ram,
|
||||
used: parseFloat(ram.used.replace('GB', '')),
|
||||
available: parseFloat(ram.available.replace('GB', '')),
|
||||
total: parseFloat(ram.total.replace('GB', '')),
|
||||
percentage: ram.used_percentage
|
||||
},
|
||||
cpu: {
|
||||
used_percentage: cpu.used_percentage
|
||||
},
|
||||
services: services,
|
||||
processes: processes
|
||||
};
|
||||
clearTimeout(timeoutId);
|
||||
callback(false);
|
||||
},
|
||||
error: () => {
|
||||
this.bootServers = [];
|
||||
if (showLoading) this.loadingBootServers = false;
|
||||
this.checkOverallLoading();
|
||||
this.toastService.error(this.translate.instant('errorLoadingList1'));
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
|
||||
clearTimeout(timeoutId);
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
loadDhcps(showLoading: boolean = true): void {
|
||||
if (showLoading) this.loadingDhcps = true;
|
||||
this.http.get<any>(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe({
|
||||
next: data => {
|
||||
this.dhcps = data['hydra:member'] || [];
|
||||
if (showLoading) this.loadingDhcps = false;
|
||||
this.checkOverallLoading();
|
||||
},
|
||||
error: () => {
|
||||
this.dhcps = [];
|
||||
if (showLoading) this.loadingDhcps = false;
|
||||
this.checkOverallLoading();
|
||||
this.toastService.error(this.translate.instant('errorLoadingList'));
|
||||
}
|
||||
});
|
||||
loadOgBootStatus(): void {
|
||||
this.isDhcp = false;
|
||||
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot', false);
|
||||
}
|
||||
|
||||
checkOverallLoading(): void {
|
||||
if (!this.loadingRepositories && !this.loadingBootServers && !this.loadingDhcps) {
|
||||
this.loading = false;
|
||||
loadDhcpStatus(): void {
|
||||
this.isDhcp = true;
|
||||
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp', false);
|
||||
}
|
||||
|
||||
|
||||
onRepositoryChange(repositoryUuid: string): void {
|
||||
this.selectedRepositoryUuid = repositoryUuid;
|
||||
if (repositoryUuid && !this.repositoryStatuses[repositoryUuid]) {
|
||||
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
|
||||
if (errorOccurred) {
|
||||
this.errorRepositories[repositoryUuid] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedRepositoryName(): string {
|
||||
const selectedRepo = this.repositories.find(repo => repo.uuid === this.selectedRepositoryUuid);
|
||||
return selectedRepo ? selectedRepo.name : '';
|
||||
}
|
||||
|
||||
getRepositoryIframeUrl(): SafeResourceUrl {
|
||||
const repositoryName = this.getSelectedRepositoryName();
|
||||
if (repositoryName) {
|
||||
const encodedName = encodeURIComponent(repositoryName);
|
||||
const url = `${this.configService.logUrl}/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
}
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl('');
|
||||
}
|
||||
|
||||
getOgBootIframeUrl(): SafeResourceUrl {
|
||||
const url = `${this.configService.logUrl}/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
}
|
||||
|
||||
getDhcpIframeUrl(): SafeResourceUrl {
|
||||
const url = `${this.configService.logUrl}/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
}
|
||||
|
||||
getOgCoreIframeUrl(): SafeResourceUrl {
|
||||
const url = `${this.configService.logUrl}/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser`;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
}
|
||||
|
||||
refreshAll(): void {
|
||||
this.loading = true;
|
||||
this.updateLastUpdateTime();
|
||||
this.loadBootServers(true);
|
||||
this.loadDhcps(true);
|
||||
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, false, 'errorOgBoot', true);
|
||||
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, true, 'errorDhcp', true);
|
||||
this.loadRepositories(true);
|
||||
this.syncSubnets();
|
||||
this.toastService.success(this.translate.instant('dataUpdatedCorrectly'));
|
||||
this.syncTemplates();
|
||||
this.syncOgLives();
|
||||
this.toastService.success('Datos actualizados correctamente');
|
||||
}
|
||||
|
||||
updateLastUpdateTime(): void {
|
||||
|
|
@ -330,11 +391,80 @@ export class GlobalStatusComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
retryRepositoryStatus(repositoryUuid: string): void {
|
||||
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
|
||||
this.errorRepositories[repositoryUuid] = errorOccurred;
|
||||
if (!errorOccurred) {
|
||||
this.toastService.success('Estado del repositorio actualizado correctamente');
|
||||
} else {
|
||||
this.toastService.error('Error al cargar el estado del repositorio');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
selectSection(section: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore'): void {
|
||||
this.selectedSection = section;
|
||||
|
||||
switch (section) {
|
||||
case 'repositories':
|
||||
break;
|
||||
case 'ogboot':
|
||||
if (!this.ogBootDiskUsage) {
|
||||
this.loadOgBootStatus();
|
||||
}
|
||||
break;
|
||||
case 'dhcp':
|
||||
if (!this.dhcpDiskUsage) {
|
||||
this.loadDhcpStatus();
|
||||
}
|
||||
break;
|
||||
case 'ogcore':
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
scrollToLogs(): void {
|
||||
let logsSectionId: string;
|
||||
switch (this.selectedSection) {
|
||||
case 'repositories':
|
||||
logsSectionId = 'repository-logs-section';
|
||||
break;
|
||||
case 'ogboot':
|
||||
logsSectionId = 'ogboot-logs-section';
|
||||
break;
|
||||
case 'dhcp':
|
||||
logsSectionId = 'dhcp-logs-section';
|
||||
break;
|
||||
case 'ogcore':
|
||||
logsSectionId = 'ogcore-logs-section';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const checkAndScroll = () => {
|
||||
const logsSection = document.getElementById(logsSectionId);
|
||||
if (logsSection) {
|
||||
logsSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (checkAndScroll()) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,233 +0,0 @@
|
|||
.section-content {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.repositories-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.repositories-selector-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.repository-selector {
|
||||
display: flex;
|
||||
padding: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.repository-select-field {
|
||||
min-width: 260px;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.repository-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selected-repository-content {
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.6rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.section-header h2::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logs-button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.logs-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 14px rgba(102, 126, 234, 0.35);
|
||||
}
|
||||
|
||||
.logs-button mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.logs-section {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.logs-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.logs-header h3::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logs-header p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.ogboot-logs-iframe {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.no-repositories {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.no-repositories .no-data-icon {
|
||||
font-size: 2.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.no-repositories h3 {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.no-repositories p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.no-repository-selected {
|
||||
text-align: center;
|
||||
padding: 1.5rem 1rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.no-repository-selected .no-data-icon {
|
||||
font-size: 2.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.no-repository-selected h3 {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.no-repository-selected p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 160px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e9ecef;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 42px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
color: #dc3545;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.error-card h3 {
|
||||
margin: 0 0 0.4rem;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.error-card p {
|
||||
margin: 0 0 1rem;
|
||||
color: #6c757d;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
<div class="section-content">
|
||||
<div class="repositories-container">
|
||||
@if(bootServers.length === 0) {
|
||||
<div class="no-repositories">
|
||||
<mat-icon class="no-data-icon">cloud_off</mat-icon>
|
||||
<h3>{{ 'noBootServersAvailable' | translate }}</h3>
|
||||
<p>{{ 'noConfiguredBootServers' | translate }}</p>
|
||||
</div>
|
||||
} @else {<div class="repositories-selector-container">
|
||||
<div class="repository-selector">
|
||||
<mat-form-field appearance="outline" class="repository-select-field">
|
||||
<mat-label>{{ 'selectOgBootServer' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedBootServerUuid" (selectionChange)="onBootServerChange($event.value)">
|
||||
@for(server of bootServers; track server.uuid) {
|
||||
<mat-option [value]="server.uuid">
|
||||
{{ server.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-icon matSuffix>storage</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@if(selectedBootServerUuid && !errorBootServers[selectedBootServerUuid] && bootServerStatuses[selectedBootServerUuid]) {
|
||||
<div class="selected-repository-content">
|
||||
<div class="repository-item">
|
||||
<div class="section-header">
|
||||
<h2>{{ getSelectedBootServerName() }}</h2>
|
||||
<button class="logs-button" (click)="scrollToLogs()">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>{{ 'buttonViewLogs' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="repository-content">
|
||||
<app-status-tab [loading]="loadingBootServers" [diskUsage]="bootServerStatuses[selectedBootServerUuid].disk"
|
||||
[servicesStatus]="bootServerStatuses[selectedBootServerUuid].services"
|
||||
[installedOgLives]="bootServerStatuses[selectedBootServerUuid].installedOgLives"
|
||||
[diskUsageChartData]="bootServerStatuses[selectedBootServerUuid].diskUsageChartData" [view]="view" [colorScheme]="colorScheme"
|
||||
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="false" [isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ogboot-logs-section">
|
||||
<app-status-logs
|
||||
[title]="'ogbootLogs'"
|
||||
[identifiers]="['ogboot', 'tftpd-hpa']"
|
||||
[host]="getSelectedBootServerIp()"
|
||||
[oglogUrl]="getBootServerIframeUrl()">
|
||||
</app-status-logs>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if(!selectedBootServerUuid) {
|
||||
<div class="no-repository-selected">
|
||||
<mat-icon class="no-data-icon">storage</mat-icon>
|
||||
<h3>{{ 'selectABootServer' | translate }}</h3>
|
||||
<p>{{ 'selectBootServerToView' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if(selectedBootServerUuid && errorBootServers[selectedBootServerUuid]) {
|
||||
<div class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>{{ 'connectionError' | translate }}</h3>
|
||||
<p>{{ 'cannotConnectBootServer' | translate }}</p>
|
||||
<button mat-raised-button color="primary" (click)="retryBootServerStatus(selectedBootServerUuid)">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
{{ 'buttonRetry' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GlobalOgbootStatusComponent as OgbootStatusComponent } from './ogboot-status.component';
|
||||
|
||||
describe('OgbootStatusComponent', () => {
|
||||
let component: OgbootStatusComponent;
|
||||
let fixture: ComponentFixture<OgbootStatusComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OgbootStatusComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(OgbootStatusComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-global-ogboot-status',
|
||||
templateUrl: './ogboot-status.component.html',
|
||||
styleUrl: './ogboot-status.component.css'
|
||||
})
|
||||
export class GlobalOgbootStatusComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
loadingBootServers: boolean = false;
|
||||
|
||||
@Input() bootServers: any[] = [];
|
||||
|
||||
bootServerStatuses: { [key: string]: any } = {};
|
||||
selectedBootServerUuid: string = '';
|
||||
errorBootServers: { [key: string]: boolean } = {};
|
||||
|
||||
showLabels: boolean = true;
|
||||
isDoughnut: boolean = true;
|
||||
colorScheme: any = {
|
||||
domain: ['#df200d', '#26a700']
|
||||
};
|
||||
view: [number, number] = [400, 220];
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
private sanitizer: DomSanitizer
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
|
||||
|
||||
loadBootServerStatus(bootServerUuid: string, callback: (errorOccurred: boolean) => void): void {
|
||||
const timeoutId = setTimeout(() => {
|
||||
callback(true);
|
||||
}, 5000);
|
||||
|
||||
this.http.get<any>(`${this.baseUrl}/boot-servers/server/${bootServerUuid}/status`).subscribe(
|
||||
data => {
|
||||
if (data.code && data.code !== 200) {
|
||||
clearTimeout(timeoutId);
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = data.message || data;
|
||||
const disk = message.disk_usage;
|
||||
const services = message.services_status;
|
||||
const installedOgLives = message.installed_oglives || [];
|
||||
|
||||
let diskUsageChartData: any[] = [];
|
||||
diskUsageChartData.push(
|
||||
{ name: 'Usado', value: parseFloat(disk.used) },
|
||||
{ name: 'Disponible', value: parseFloat(disk.available) }
|
||||
);
|
||||
|
||||
this.bootServerStatuses[bootServerUuid] = {
|
||||
disk: {
|
||||
...disk,
|
||||
used: parseFloat(disk.used.replace('GB', '')),
|
||||
available: parseFloat(disk.available.replace('GB', '')),
|
||||
total: parseFloat(disk.total.replace('GB', '')),
|
||||
percentage: disk.used_percentage || disk.percentage
|
||||
},
|
||||
services: services,
|
||||
installedOgLives: installedOgLives,
|
||||
diskUsageChartData: diskUsageChartData
|
||||
};
|
||||
clearTimeout(timeoutId);
|
||||
callback(false);
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error?.['hydra:description'] || 'Error al cargar el estado del servidor de boot');
|
||||
clearTimeout(timeoutId);
|
||||
callback(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onBootServerChange(bootServerUuid: string): void {
|
||||
this.selectedBootServerUuid = bootServerUuid;
|
||||
if (bootServerUuid && !this.bootServerStatuses[bootServerUuid]) {
|
||||
this.loadBootServerStatus(bootServerUuid, (errorOccurred: boolean) => {
|
||||
if (errorOccurred) {
|
||||
this.errorBootServers[bootServerUuid] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedBootServerIp(): string {
|
||||
const s = this.bootServers.find(s => s.uuid === this.selectedBootServerUuid);
|
||||
return s ? s.ip : '';
|
||||
}
|
||||
|
||||
getSelectedBootServerName(): string {
|
||||
const selectedBootServer = this.bootServers.find(server => server.uuid === this.selectedBootServerUuid);
|
||||
return selectedBootServer ? selectedBootServer.name : '';
|
||||
}
|
||||
|
||||
getBootServerIframeUrl(): SafeResourceUrl {
|
||||
const bootServerName = this.getSelectedBootServerName();
|
||||
if (bootServerName) {
|
||||
const encodedName = encodeURIComponent(bootServerName);
|
||||
const url = `${this.configService.logUrl}/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
}
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl('');
|
||||
}
|
||||
|
||||
retryBootServerStatus(bootServerUuid: string): void {
|
||||
this.loadBootServerStatus(bootServerUuid, (errorOccurred: boolean) => {
|
||||
this.errorBootServers[bootServerUuid] = errorOccurred;
|
||||
if (!errorOccurred) {
|
||||
this.toastService.success(this.translate.instant('bootServerStatus'));
|
||||
} else {
|
||||
this.toastService.error(this.translate.instant('errorLoadingState2'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scrollToLogs(): void {
|
||||
const checkAndScroll = () => {
|
||||
const logsSection = document.getElementById('ogboot-logs-section');
|
||||
if (logsSection) {
|
||||
logsSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (checkAndScroll()) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
.section-content {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.logs-section {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.logs-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.logs-header h3::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logs-header p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.ogcore-logs-iframe {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<div class="section-content">
|
||||
<div class="tab-content">
|
||||
<div id="ogcore-logs-section">
|
||||
<app-status-logs
|
||||
[title]="'ogcoreLogs'"
|
||||
[identifiers]="['ogcore']"
|
||||
[host]="''"
|
||||
[oglogUrl]="getOgCoreIframeUrl()">
|
||||
</app-status-logs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OgcoreStatusComponent } from './ogcore-status.component';
|
||||
|
||||
describe('OgcoreStatusComponent', () => {
|
||||
let component: OgcoreStatusComponent;
|
||||
let fixture: ComponentFixture<OgcoreStatusComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OgcoreStatusComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(OgcoreStatusComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ogcore-status',
|
||||
templateUrl: './ogcore-status.component.html',
|
||||
styleUrl: './ogcore-status.component.css'
|
||||
})
|
||||
export class OgcoreStatusComponent {
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private sanitizer: DomSanitizer
|
||||
) {}
|
||||
|
||||
getOgCoreIframeUrl(): SafeResourceUrl {
|
||||
const url = `${this.configService.logUrl}/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser`;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
.section-content {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.repositories-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.repositories-selector-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.repository-selector {
|
||||
display: flex;
|
||||
padding: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.repository-select-field {
|
||||
min-width: 260px;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.repository-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selected-repository-content {
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.6rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.section-header h2::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logs-button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.logs-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 14px rgba(102, 126, 234, 0.35);
|
||||
}
|
||||
|
||||
.logs-button mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.logs-section {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.logs-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.logs-header h3::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logs-header p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.repository-logs-iframe {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.no-repositories {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.no-repositories .no-data-icon {
|
||||
font-size: 2.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.no-repositories h3 {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.no-repositories p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.no-repository-selected {
|
||||
text-align: center;
|
||||
padding: 1.5rem 1rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.no-repository-selected .no-data-icon {
|
||||
font-size: 2.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.no-repository-selected h3 {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.no-repository-selected p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 160px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e9ecef;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 42px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
color: #dc3545;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.error-card h3 {
|
||||
margin: 0 0 0.4rem;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.error-card p {
|
||||
margin: 0 0 1rem;
|
||||
color: #6c757d;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
<div class="section-content">
|
||||
<div class="repositories-container">
|
||||
@if(dhcps.length === 0) {
|
||||
<div class="no-repositories">
|
||||
<mat-icon class="no-data-icon">cloud_off</mat-icon>
|
||||
<h3>{{ 'noDhcpsAvailable' | translate }}</h3>
|
||||
<p>{{ 'noConfiguredDhcps' | translate }}</p>
|
||||
</div>
|
||||
} @else {<div class="repositories-selector-container">
|
||||
<div class="repository-selector">
|
||||
<mat-form-field appearance="outline" class="repository-select-field">
|
||||
<mat-label>{{ 'buttonSelectDhcp' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedDhcpUuid" (selectionChange)="onDhcpChange($event.value)">
|
||||
@for(dhcp of dhcps; track dhcp.uuid) {
|
||||
<mat-option [value]="dhcp.uuid">
|
||||
{{ dhcp.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-icon matSuffix>storage</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@if(selectedDhcpUuid && !errorDhcps[selectedDhcpUuid] && dhcpStatuses[selectedDhcpUuid]) {
|
||||
<div class="selected-repository-content">
|
||||
<div class="repository-item">
|
||||
<div class="section-header">
|
||||
<h2>{{ getSelectedDhcpName() }}</h2>
|
||||
<button class="logs-button" (click)="scrollToLogs()">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>{{ 'buttonViewLogs' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="repository-content">
|
||||
<app-status-tab [loading]="loadingDhcps" [diskUsage]="dhcpStatuses[selectedDhcpUuid].disk"
|
||||
[servicesStatus]="dhcpStatuses[selectedDhcpUuid].services" [subnets]="subnets"
|
||||
[diskUsageChartData]="dhcpStatuses[selectedDhcpUuid].diskUsageChartData" [view]="view"
|
||||
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true"
|
||||
[isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dhcp-logs-section">
|
||||
<app-status-logs
|
||||
[title]="'dhcpLogs'"
|
||||
[identifiers]="['ogdhcp', 'kea-dhcp4', 'kea-ctrl-agent', 'DhcpLFC']"
|
||||
[host]="getSelectedDhcpIp()"
|
||||
[oglogUrl]="getDhcpIframeUrl()">
|
||||
</app-status-logs>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if(!selectedDhcpUuid) {
|
||||
<div class="no-repository-selected">
|
||||
<mat-icon class="no-data-icon">storage</mat-icon>
|
||||
<h3>{{ 'selectADhcp' | translate }}</h3>
|
||||
<p>{{ 'selectDhcpToView' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if(selectedDhcpUuid && errorDhcps[selectedDhcpUuid]) {
|
||||
<div class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>{{ 'connectionError' | translate }}</h3>
|
||||
<p>{{ 'cannotConnectDhcp' | translate }}</p>
|
||||
<button mat-raised-button color="primary" (click)="retryDhcpStatus(selectedDhcpUuid)">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
{{ 'buttonRetry' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue