Compare commits

...

256 Commits

Author SHA1 Message Date
Manuel Aranda Rosales c290c94675 Merge pull request 'develop' (#27) from develop into main
oggui-debian-package/pipeline/tag This commit looks good Details
oggui-debian-package/pipeline/head This commit looks good Details
Reviewed-on: #27
2025-06-09 12:25:12 +02:00
Manuel Aranda Rosales 9216137dee Merge branch 'main' into develop 2025-06-09 12:24:14 +02:00
Manuel Aranda Rosales 15b37d9158 Fixed test 2025-06-09 12:23:07 +02:00
Manuel Aranda Rosales b6c00e12d8 Fixed test 2025-06-09 12:21:20 +02:00
Manuel Aranda Rosales b4ba0b1244 refs #2182. Changed Output messages 2025-06-09 12:05:35 +02:00
Lucas Lara García 06d0a83aab refs #2169 Refactor legend component to enhance client status display and add translations for client states 2025-06-04 13:13:30 +02:00
Lucas Lara García 3bc07b56cd refs #2171 Add LogoutGuard and update AuthService logout method to support redirect options 2025-06-04 10:07:18 +02:00
Lucas Lara García e964c6b47a refs #2148 Refactor dialog components to use <mat-dialog-content> and <mat-dialog-actions> for better responsiveness 2025-06-03 10:58:41 +02:00
Lucas Lara García 36f709f7c1 Hide edit user button for super-admin 2025-06-02 10:59:00 +02:00
Manuel Aranda Rosales f840272e0d Merge pull request 'develop' (#25) from develop into main
oggui-debian-package/pipeline/tag There was a failure building this commit Details
oggui-debian-package/pipeline/head There was a failure building this commit Details
Reviewed-on: #25
2025-06-02 08:38:22 +02:00
Manuel Aranda Rosales a8817bf49a Solve conflics 2025-06-02 08:37:50 +02:00
Manuel Aranda Rosales 6c40d78f15 solve conflicts 2025-06-02 08:34:39 +02:00
Manuel Aranda Rosales c2f3ea3caa Added changelog 2025-06-02 08:32:26 +02:00
Manuel Aranda Rosales 62001e4a44 Fixed test 2025-06-02 08:29:21 +02:00
Manuel Aranda Rosales 6c7951be31 refs #2118. Fixed bug in manage-repo form 2025-06-02 08:09:59 +02:00
Manuel Aranda Rosales f94d522420 refs #2158. Added script to existint command-task 2025-06-02 08:09:20 +02:00
Lucas Lara García 8882fd40a5 test: enhance ChangeParentComponent tests with necessary imports and providers 2025-05-30 09:31:56 +02:00
Manuel Aranda Rosales 4e23723717 refs #2098. Move clients to different OU
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-29 16:00:53 +02:00
Manuel Aranda Rosales 1ce50857ac refs #2098. Move clients to different OU 2025-05-29 15:31:27 +02:00
Lucas Lara García 3cd46e1166 feat: implement page not found component with styled layout
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-29 11:58:47 +02:00
Lucas Lara García 7137768939 refactor: remove Admin and Dashboard components; implement role-based routing and guards
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-29 11:51:20 +02:00
Lucas Lara García 79188ffbc5 refs #2078 and #2079 Add user category checks for client management actions in groups component
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-28 12:15:58 +02:00
Lucas Lara García 84fd9d0335 refs #2078 and #2079 Implement role-based command filtering and UI adjustments for user permissions
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-27 14:14:01 +02:00
Lucas Lara García a26f2481fa fix: update BootSoPartition and RemoveCacheImage component tests with new dependencies and mock data
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-26 13:10:51 +02:00
Manuel Aranda Rosales a6ff85139b Removed temp. ogGit
oggui-debian-package/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/tag There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-23 14:14:12 +02:00
Lucas Lara García c7ed41a1a5 refs #2089 and #2091 Implement AuthService for user authentication and role management
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-23 13:56:07 +02:00
Manuel Aranda Rosales 4e6dbde59c Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-23 13:28:33 +02:00
Manuel Aranda Rosales fcfcd58d93 refs #2085. RemoveCacheImage 2025-05-23 13:28:22 +02:00
Manuel Aranda Rosales 98b4d3c4f0 refs #2074. BootOs 2025-05-23 13:27:43 +02:00
Lucas Lara García faf3fa0613 fix: reset pagination on filter change in ClientTaskLogsComponent
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-22 09:28:05 +02:00
Lucas Lara García 11b1c26f50 Text fixed: add MatInputModule to ClientTaskLogsComponent test setup
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-21 09:58:56 +02:00
Lucas Lara García cd45009751 refs #2049 Improve date validation and update parameter naming in task logs
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-20 16:20:05 +02:00
Lucas Lara García b55f15f16b refs #2049 enhance date filtering in task logs with max date constraint and validation
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-20 14:29:11 +02:00
Lucas Lara García c824953b1e refactor: add date filters to task logs component and improve validation
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-20 13:59:10 +02:00
Lucas Lara García 9d21a6fefc refactor: streamline trace loading parameters and enhance logging
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-20 13:51:17 +02:00
Lucas Lara García a84372214b refactor: simplify progress container structure and enhance trace status display
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-20 13:26:35 +02:00
Lucas Lara García 4c8b6c7dbd refactor: update task logs table to improve trace information display and localization
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-20 13:17:52 +02:00
Manuel Aranda Rosales d83f0148fb Merge pull request 'develop' (#23) from develop into main
oggui-debian-package/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details
Reviewed-on: #23
2025-05-19 16:58:23 +02:00
Manuel Aranda Rosales 3d2f9dfa5e Merge branch 'main' into develop
testing/ogGui-multibranch/pipeline/pr-main Build queued... Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-19 16:58:05 +02:00
Manuel Aranda Rosales 6433adbecd Added changelog
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-19 16:57:07 +02:00
Manuel Aranda Rosales 35bb0e6e62 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-19 16:19:43 +02:00
Manuel Aranda Rosales 76835be2a2 Improvements 2025-05-19 16:19:37 +02:00
Lucas Lara García a6ce1ee4cf fix: update command filter to use ID or UUID for selection
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-19 13:32:00 +02:00
Lucas Lara García a57675631f refs #2040 update joyride steps and translations for task logs component
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-19 13:26:31 +02:00
Manuel Aranda Rosales 9fcfeb7f4e Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-19 08:13:00 +02:00
Manuel Aranda Rosales f70dcd109a Assistants improvements 2025-05-19 08:12:53 +02:00
Manuel Aranda Rosales 2f202e5037 refs #2031. New config tls var 2025-05-19 08:12:07 +02:00
Lucas Lara García b0d255a7d1 refactor: prevent default setting of organizational unit when not in edit mode and streamline data loading logic
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-16 13:46:15 +02:00
Lucas Lara García 4ff21afb57 refactor: conditionally update network settings based on excludeParentChanges flag
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-16 12:59:03 +02:00
Lucas Lara García 639f55edb3 refactor: update command filter to use name instead of id and remove date range handling 2025-05-14 10:07:19 +02:00
Nicolas Arenas f41cbf2cef Adding oggui postinst debug
oggui-debian-package/pipeline/head This commit looks good Details
2025-05-14 06:12:46 +02:00
Nicolas Arenas b82f139c9d Adding oggui postinst debug
oggui-debian-package/pipeline/head This commit looks good Details
2025-05-14 06:04:51 +02:00
Manuel Aranda Rosales bad345c2fd Merge pull request 'develop' (#22) from develop into main
oggui-debian-package/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details
Reviewed-on: #22
2025-05-13 08:37:13 +02:00
Manuel Aranda Rosales 56a8c563ff Merge branch 'main' into develop 2025-05-13 08:36:28 +02:00
Manuel Aranda Rosales 14986ce524 Added changelog 2025-05-13 08:35:43 +02:00
Manuel Aranda Rosales 15be37a4d0 refs #1984. Added partitionInfo to list 2025-05-13 08:26:58 +02:00
Manuel Aranda Rosales 1c1f75811c refs #1984. Git integration UX changes 2025-05-12 22:44:55 +02:00
Manuel Aranda Rosales e3adf08bd9 refs #1984. Git integration UX changes 2025-05-12 16:28:48 +02:00
Manuel Aranda Rosales 114483410b refs #1827. UX changes. New calendar styles 2025-05-12 16:26:35 +02:00
Lucas Lara García b65d715443 Refactor command filter to use command ID instead of name in ClientTaskLogs component
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-09 14:27:42 +02:00
Lucas Lara García 53d1191ddf Erefs #1963 nhance ClientTaskLogs component to include date range filtering in trace loading and reset functionality
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-09 13:52:28 +02:00
Lucas Lara García 600d5f9b37 refs #1963 Enhance loadTraces method to include command and status filters in HTTP parameters
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-09 13:41:00 +02:00
Lucas Lara García d10a209a25 Enhance unit tests for ClientTaskLogs and OutputDialog components by adding necessary imports, providers, and schemas
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-09 13:07:19 +02:00
Manuel Aranda Rosales 757de78dc4 refs #1969. Partition changes
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-08 17:09:56 +02:00
Manuel Aranda Rosales 7ea5013cf4 refs #1972. General styles changed. Show image details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-08 16:33:22 +02:00
Lucas Lara García 5777be9417 Enhance resetFilters method to include client input parameter and update template button accordingly
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-08 14:13:11 +02:00
Lucas Lara García 28336603ad Refactor resetFilters method to accept input parameters for command and status filters
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-08 14:11:45 +02:00
Lucas Lara García 60684d2c50 Implement ClientTaskLogs component with enhanced UI and functionality
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-08 13:59:24 +02:00
Lucas Lara García 1ba62b9283 Add ClientTaskLogs component and integrate with Groups component
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
- Introduced ClientTaskLogsComponent to display client task logs.
- Updated GroupsComponent to include a button for opening client task logs.
- Added translations for 'procedimientosCliente' in both English and Spanish locales.
- Created associated HTML, CSS, and spec files for ClientTaskLogsComponent.
2025-05-08 13:02:38 +02:00
Manuel Aranda Rosales 242f7a374c refs #1972. General styles changed. New 2025
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-08 12:56:14 +02:00
Manuel Aranda Rosales a5730a1de4 refs #1971. Trace UX changes. Some improvements and fixed filters 2025-05-08 12:54:58 +02:00
Manuel Aranda Rosales 81766471ee Fixed wrong dir
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-08 09:06:20 +02:00
Manuel Aranda Rosales 9a84e45cb8 Pushed app.module 2025-05-08 09:05:56 +02:00
Manuel Aranda Rosales ef3158a045 refs #1922. Show new fields in clients table.
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-08 08:06:57 +02:00
Manuel Aranda Rosales 9ba8c1771d refs #1906. Updated UX to CommandTask
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-08 08:03:45 +02:00
Manuel Aranda Rosales 59e8c3842e Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-05-08 08:01:38 +02:00
Manuel Aranda Rosales fc51481977 refs #1968. Pxe some UX improvements 2025-05-08 08:01:14 +02:00
Lucas Lara García f35ba106ba refs #1953 Refactor manage organizational unit component: replace hardcoded strings with translation keys and update localization files
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-07 13:45:40 +02:00
Lucas Lara García 2dce55ce1d Refactor legend component: update remote access titles to use translation keys and enhance localization files
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-07 12:19:00 +02:00
Lucas Lara García bdd6206e18 Refactor execute command component: replace command names with translation keys and update localization files for execute commands
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-07 11:58:51 +02:00
Lucas Lara García a3f99958a3 Refactor groups component: update tour steps, improve button layout, and enhance translation keys
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-06 14:09:12 +02:00
Lucas Lara García 8cc1854d09 Refactor partition type display: simplify client name presentation and improve table structure
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-02 13:34:02 +02:00
Lucas Lara García f6dbd6dad9 refs #1941 Refactor partition type organizer: enhance data structure and improve client grouping logic
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-05-02 13:24:06 +02:00
Manuel Aranda Rosales 35512ce65f General improvements
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-30 13:25:37 +02:00
Manuel Aranda Rosales 1e05176758 refs #1827. UX changes. New calendar styles
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-30 13:17:16 +02:00
Manuel Aranda Rosales 83ab784c48 refs #1922. Show new fields in clients table. Fixed wrong details 2025-04-30 13:16:18 +02:00
Manuel Aranda Rosales bf100943a0 refs #1906. Updated UX to CommandTask
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-30 13:15:06 +02:00
Nicolas Arenas 1fbec200ab Updates control
oggui-debian-package/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-29 15:57:49 +02:00
Nicolas Arenas 253af06ad5 Updtes control description
testing/ogGui-multibranch/pipeline/head Something is wrong with the build of this commit Details
oggui-debian-package/pipeline/head Something is wrong with the build of this commit Details
2025-04-29 15:53:05 +02:00
Manuel Aranda Rosales 1df6578efd Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-04-29 14:15:31 +02:00
Lucas Lara García ed073deb5d Refactor runScriptContext handling in ExecuteCommandComponent and RunScriptAssistantComponent for improved data management; update related HTML to reflect changes.
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/head Something is wrong with the build of this commit Details
2025-04-29 14:05:43 +02:00
Manuel Aranda Rosales d47eaf49a7 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-04-28 08:20:16 +02:00
Lucas Lara García 34ea12fc50 Translate table headers in PartitionTypeOrganizatorComponent to Spanish for better localization
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-25 14:23:39 +02:00
Lucas Lara García 6fd04fb46e refs #1889 Add PartitionTypeOrganizatorComponent: implement partition management modal and integrate into GroupsComponent
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-25 14:02:15 +02:00
Lucas Lara García 2bc17e8b56 Refactor updateCommandStates method in ExecuteCommandComponent: restore and optimize command state management logic for improved functionality
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-24 14:09:54 +02:00
Lucas Lara García 9582ce338c refs #1928 Add runScriptContext input to ExecuteCommandComponent and update related components for improved script execution context handling
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-24 14:08:42 +02:00
Lucas Lara García e8b713ea09 refs #1931 Refactor table styles in ClientDetailsComponent: remove border-radius and box-shadow for a cleaner look
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-23 15:03:59 +02:00
Lucas Lara García 256b3ba788 refs #1931 Enhance ClientDetailsComponent layout and styling: increase dialog width, adjust chart dimensions, and improve disk layout for better data presentation
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-23 14:56:17 +02:00
Lucas Lara García f016c66d55 refs #1931 Refactor ClientMainViewComponent and related files: remove component and styles, update routing, and adjust dialog dimensions for improved client detail display
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-23 14:22:12 +02:00
Lucas Lara García 70e21c6ca2 refs #1931 Refactor ClientDetailsComponent to enhance layout and improve data handling in client details view
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-23 13:51:45 +02:00
Lucas Lara García a40be684b5 refs #1931 Load partitions data and log relevant information for debugging
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-23 13:05:33 +02:00
Lucas Lara García ca0140e275 refs #1931 Add ClientDetailsComponent for enhanced client information display and management
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-23 12:45:32 +02:00
Lucas Lara García 88aaf39b65 refs #1884 Update command enabling logic to include 'create-image' for active clients
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-22 13:30:01 +02:00
Lucas Lara García a418d26615 refs #1884 Refactor command state handling to improve client status management and enable/disable commands based on client states
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-22 13:19:22 +02:00
Lucas Lara García 6621f7a8fe refs #1884 Enhance command execution logic to handle 'disconnected' state and update UI for multiple clients
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-22 12:59:49 +02:00
Nicolas Arenas 5dc1677851 Fix typo
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details
oggui-debian-package/pipeline/head Something is wrong with the build of this commit Details
2025-04-22 12:13:42 +02:00
Nicolas Arenas 73a69f79c9 Preserve config files
oggui-debian-package/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-22 11:54:28 +02:00
Lucas Lara García 558a3205ab refs #1884 Add clientState input to execute-command component for dynamic state handling
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-21 14:56:09 +02:00
Manuel Aranda Rosales 858a204036 Merge pull request 'develop' (#21) from develop into main
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/tag This commit looks good Details
oggui-debian-package/pipeline/head This commit looks good Details
Reviewed-on: #21
2025-04-16 14:36:15 +02:00
Manuel Aranda Rosales 844d3dc0f0 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/pr-main Build started... Details
2025-04-16 14:33:12 +02:00
Manuel Aranda Rosales 23bf2b51ea refs #1924. Refresh status card view 2025-04-16 14:33:03 +02:00
Manuel Aranda Rosales e4e6a8907e Merge branch 'main' into develop 2025-04-16 14:32:39 +02:00
Manuel Aranda Rosales 0096daca42 refs #1924. Refresh status card view 2025-04-16 14:32:17 +02:00
Lucas Lara García de56a23a2b refs #1920 use all clients by default in execute command
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-16 11:34:47 +02:00
Manuel Aranda Rosales 3bae27d88e Merge pull request 'develop' (#20) from develop into main
testing/ogGui-multibranch/pipeline/head This commit looks good Details
Reviewed-on: #20
2025-04-16 11:29:37 +02:00
Manuel Aranda Rosales 265b4888c3 Merge branch 'main' into develop
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/pr-main There was a failure building this commit Details
2025-04-16 11:27:54 +02:00
Manuel Aranda Rosales ce1a06d51b Added changelog
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-16 11:23:11 +02:00
Manuel Aranda Rosales a0d833726d refs #1922. Show new fields in clients table
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-16 11:17:11 +02:00
Manuel Aranda Rosales c7919cf412 refs #1921. Crreate image version bug in selector
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-16 10:52:02 +02:00
Manuel Aranda Rosales cbe15bba4d refs #1907. Updated global status sync endpoints
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-16 10:50:27 +02:00
Manuel Aranda Rosales b6e8134810 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-04-15 18:37:50 +02:00
Manuel Aranda Rosales 6205f3ad2f refs #1919. Fixed bug when removed partition 2025-04-15 18:37:36 +02:00
Lucas Lara García 436267cfb9 refs #1883 Move 'Execute commands' to tree node actions
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-15 15:17:03 +02:00
Nicolas Arenas 2b3f7b6e34 Updated pre and post files
testing/ogGui-multibranch/pipeline/head Something is wrong with the build of this commit Details
oggui-debian-package/pipeline/tag This commit looks good Details
oggui-debian-package/pipeline/head This commit looks good Details
2025-04-11 13:44:29 +02:00
Lucas Lara García e9a00119aa Fix incorrect path for loading config.json in ConfigService
testing/ogGui-multibranch/pipeline/head Something is wrong with the build of this commit Details
2025-04-11 13:20:53 +02:00
Manuel Aranda Rosales b2d34a6880 Merge pull request 'develop' (#19) from develop into main
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/head This commit looks good Details
Reviewed-on: #19
2025-04-11 12:03:38 +02:00
Manuel Aranda Rosales cf5f2754c6 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/pr-main Build started... Details
2025-04-11 11:59:01 +02:00
Manuel Aranda Rosales 80d9f5cb0f Solve conflics 2025-04-11 11:58:57 +02:00
Lucas Lara García 321a31eecb refs #1899 Refactor dialog close handling in ManageClient and ManageOrganizationalUnit components to return success status; update GroupsComponent to close menus accordingly.
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-11 11:56:48 +02:00
Manuel Aranda Rosales 636a66b93b Updated changelog
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-11 11:53:44 +02:00
Manuel Aranda Rosales 6a06e0a477 UX general improvements
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-11 11:45:32 +02:00
Manuel Aranda Rosales 411491091b refs #1864. Added ssh_port and user in imageRepository in UX. 2025-04-11 11:45:15 +02:00
Manuel Aranda Rosales 9e9d7b9873 refs #1902. Multi select checkbox logic
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-11 11:44:23 +02:00
Manuel Aranda Rosales dad3635f4f refs #1857. Rename image.
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-11 11:15:43 +02:00
Manuel Aranda Rosales 2958e05c98 refs #1901. Fixed manage-ou component. Added spinner
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-11 11:14:04 +02:00
Manuel Aranda Rosales c365ef2a14 refs #1794. Improvements, and new UX
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-11 10:46:49 +02:00
Manuel Aranda Rosales 1f7101c7a0 refs #1857. Rename image.
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-11 10:40:27 +02:00
Lucas Lara García 824e55102e refs #1900 Set selected node and fetch clients on menu click in GroupsComponent
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-11 09:44:39 +02:00
Nicolas Arenas 4758190b6d Publish main in nightly repo
oggui-debian-package/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-10 16:51:29 +02:00
Nicolas Arenas 64fa13f36f Publish main in nightly repo
oggui-debian-package/pipeline/head Something is wrong with the build of this commit Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-10 16:50:26 +02:00
Lucas Lara García c7d6e41874 refs #1869 Add ShowGitImages component and integrate it into RepositoriesComponent
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-10 14:16:35 +02:00
Lucas Lara García 02fbf57384 refs #1867 Add MatDialog to LoginComponent for displaying GlobalStatusComponent on successful login
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-09 13:31:10 +02:00
Lucas Lara García bd0135b796 Fix component references in ShowMonoliticImagesComponent tests
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-09 12:31:12 +02:00
Lucas Lara García 390bc54213 Add ShowGitImages component and refactor show-images for show-monolitic-images
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-09 12:27:55 +02:00
Lucas Lara García 7659c09cd6 Fixed tests
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-09 10:08:59 +02:00
Manuel Aranda Rosales 380cf50080 refs #1858. Added description into deployImage selector
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-08 15:57:42 +02:00
Manuel Aranda Rosales eac4b0a948 refs #1858. Added description and change in repository component
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-08 15:57:03 +02:00
Manuel Aranda Rosales ebd448ce71 refs #1852. Updated mercure event and added new client status DISCONNECTED
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-08 15:55:35 +02:00
Lucas Lara García 672f0eade4 refs #1805 Enhance clients table layout and styling for improved readability and responsiveness
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-08 10:05:09 +02:00
Lucas Lara García 2b0d70dd58 refs #1799 Enhance responsiveness and layout for small screens in groups component
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-07 09:56:46 +02:00
Manuel Aranda Rosales 4f2bf0ec05 refs #1740. Run assistant updates. Save command
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-03 08:40:26 +02:00
Manuel Aranda Rosales 1c417e5f35 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-03 06:55:55 +02:00
Manuel Aranda Rosales 4fed92505e refs #1797. Rename image. Crete image 2025-04-03 06:55:48 +02:00
Lucas Lara García 74f5f79206 refs #1798 Enhance header component for better small screen support and responsiveness
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-02 16:35:57 +02:00
Lucas Lara García b5a6bb0559 refs #1791 Refactor layout components for improved sidebar behavior and responsiveness
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-02 14:06:03 +02:00
Lucas Lara García 5baf4d8e3d Improve subnet synchronization with loading state and error handling
testing/ogGui-multibranch/pipeline/head Build queued... Details
2025-04-02 14:05:53 +02:00
Lucas Lara García 4d32540784 Fixed tests
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-04-02 09:51:24 +02:00
Manuel Aranda Rosales 9ef61500cb refs #1797. Rename image. Crete image
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-01 19:45:47 +02:00
Manuel Aranda Rosales 41f9521d4a refs #1793. Updted assistants UX
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-04-01 10:50:45 +02:00
Manuel Aranda Rosales 673fe5e7fd refs #1739. Rin script assistant 2025-04-01 10:49:16 +02:00
Manuel Aranda Rosales 945ae8ca0b Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-31 14:37:52 +02:00
Lucas Lara García 11a4773570 Refactor error handling in Global Status component to prevent duplicate entries in errorRepositories
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-31 14:32:00 +02:00
Nicolas Arenas 49671ed686 Using shared libraries
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details
2025-03-27 08:35:57 +01:00
Manuel Aranda Rosales 34bf065de9 Merge branch 'main' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui
oggui-debian-package/pipeline/tag This commit looks good Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-27 07:54:13 +01:00
Manuel Aranda Rosales 23d2b591f8 Updated changelog 2025-03-27 07:54:02 +01:00
Nicolas Arenas 6f16d07537 Updated Jenkinsfile to avoid colissions
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/tag This commit looks good Details
2025-03-27 07:36:18 +01:00
Manuel Aranda Rosales e33726bf6a Merge branch 'main' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/tag There was a failure building this commit Details
2025-03-26 21:49:53 +01:00
Manuel Aranda Rosales edfab0be94 Added constraint create client form (Mac). Updated groups tree UX 2025-03-26 21:49:42 +01:00
Nicolas Arenas da9fbc1fdb Update Jenkinsfile
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-26 13:24:26 +01:00
Nicolas Arenas c568a555a2 Improves oggui package
oggui-debian-package/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details
2025-03-26 13:19:32 +01:00
Manuel Aranda Rosales 7bff91aa42 Merge pull request 'develop' (#18) from develop into main
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details
Reviewed-on: #18
2025-03-25 16:25:10 +01:00
Manuel Aranda Rosales 081da1efc6 Merge main, and updated improvements
testing/ogGui-multibranch/pipeline/pr-main There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-25 16:23:30 +01:00
Manuel Aranda Rosales 7dc1f662e6 Merge main, and updated improvements 2025-03-25 15:53:41 +01:00
Manuel Aranda Rosales fd0778d096 Merge branch 'main' into develop 2025-03-25 15:40:43 +01:00
Manuel Aranda Rosales d430091d54 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-25 15:31:13 +01:00
Lucas Lara García 1d28e443a3 Refactor Global Status component to support individual error handling for repositories
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-25 15:30:26 +01:00
Manuel Aranda Rosales 8fdee4fc9b Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-25 15:03:52 +01:00
Lucas Lara García 22d775e793 Refactor Global Status component to handle multiple error states for OgBoot, Dhcp, and Repositories
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-25 15:02:23 +01:00
Manuel Aranda Rosales 3cd61cfc8f Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-25 14:21:29 +01:00
Lucas Lara García 1fbf494061 refs #1726 Add error handling and display for data loading in Global Status component 2025-03-25 13:49:52 +01:00
Lucas Lara García 4998463ba1 refs #1726 Add translation support for disk and RAM usage labels in Global Status component 2025-03-25 12:42:37 +01:00
Manuel Aranda Rosales 1f2c953509 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-25 12:21:56 +01:00
Lucas Lara García 2f47b2ec66 Refactor Global Status component to handle repository loading asynchronously and enhance CSS for RAM and process status display 2025-03-25 12:13:46 +01:00
Manuel Aranda Rosales 97b1ce15ab Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-25 10:57:31 +01:00
Lucas Lara García 04ed52754c refs #1726 Add translation support for RAM and CPU usage labels in Global Status component 2025-03-25 10:56:36 +01:00
Manuel Aranda Rosales 3a5c4efecd Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-24 13:46:05 +01:00
Lucas Lara García fdf33addc1 Fix repository status handling by using UUID instead of ID in Global Status component 2025-03-24 13:27:11 +01:00
Manuel Aranda Rosales b5510ffa13 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-24 13:07:47 +01:00
Lucas Lara García a0b3f0a4f7 Add repository status display with RAM, CPU usage, and process details in Global Status component 2025-03-24 13:05:21 +01:00
Manuel Aranda Rosales 5d54cf78ec Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-24 12:38:13 +01:00
Lucas Lara García 3599a40ede Enhance Global Status component to display detailed repository status including RAM and CPU usage 2025-03-24 12:37:44 +01:00
Manuel Aranda Rosales da8451d405 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-24 12:28:46 +01:00
Lucas Lara García dcf9390870 Add repository loading functionality to Global Status component 2025-03-24 12:27:09 +01:00
Nicolas Arenas 84863cb0ac Updated oggui debian install
oggui-debian-package/pipeline/tag This commit looks good Details
2025-03-21 14:51:15 +01:00
Manuel Aranda Rosales 4e7c823094 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-21 12:30:03 +01:00
Lucas Lara García ebe14e0125 Add loading spinner to Global Status component during data fetch 2025-03-21 10:42:11 +01:00
Manuel Aranda Rosales 063ed4c310 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-20 14:18:24 +01:00
Lucas Lara García 44c4c60297 Enhance Global Status component layout with loading state handling and improved dialog dimensions
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-20 14:17:49 +01:00
Manuel Aranda Rosales c6b3deea41 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-20 14:17:37 +01:00
Lucas Lara García b1af49c641 Refactor Installed OgLives section in Global Status component for improved clarity
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-20 11:20:46 +01:00
Manuel Aranda Rosales b4bf4909fa Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop 2025-03-20 11:12:00 +01:00
Manuel Aranda Rosales 1bf77166d6 Updated client view styles 2025-03-20 11:11:51 +01:00
Lucas Lara García 3d62161aaa refs #1725 Add support for DHCP and subnets in Global Status component
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-20 11:07:38 +01:00
Nicolas Arenas 2f968499f6 jenkins_upload_packages (#17)
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/tag This commit looks good Details
Reviewed-on: #17
2025-03-20 10:46:35 +01:00
Lucas Lara García 081f9a9846 Enhance unit tests for ConvertImageToVirtualComponent with improved mock services and dependencies
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-20 09:46:52 +01:00
Manuel Aranda Rosales 09d3420387 refs #1580. Fixed add multiple clients regexp
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-20 09:42:56 +01:00
Manuel Aranda Rosales 523b4bfc60 refs #1731. New UX integration. Convert image to virtual
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-19 15:40:52 +01:00
Lucas Lara García 7e133f2b2b Fix installedOgLives in global status components
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-19 14:02:04 +01:00
Lucas Lara García 07acbc5f87 Refactor Global Status component to improve disk usage data handling and ensure proper chart data structure
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-19 13:19:13 +01:00
Lucas Lara García 6c8ad465ea refs #1725 Use tab status component for dchp status in global component
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-19 13:02:25 +01:00
Lucas Lara García 50755bd1d5 refs #1727 Add StatusTab component to Global Status for enhanced service and disk usage display
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-19 13:01:23 +01:00
Nicolas Arenas d4eec6e5ff jenkins_upload_packages (#16)
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
Uploads packages

Reviewed-on: #16
refers #1313
2025-03-19 12:28:22 +01:00
Lucas Lara García 9f9d73644b Refactor tests for ManageRepository and ShowImages components to improve structure and add necessary imports
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-19 08:55:14 +01:00
Manuel Aranda Rosales 335f4683fc Refactor Images/Repositories modules
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-18 17:09:25 +01:00
Lucas Lara García a9dd983f53 Refactor Global Status component to remove unnecessary console logs and clean up disk usage chart data handling
testing/ogGui-multibranch/pipeline/head Something is wrong with the build of this commit Details
2025-03-18 14:31:38 +01:00
Lucas Lara García bb31acb4cc Fix service status assignment and update disk usage chart data in Global Status component
testing/ogGui-multibranch/pipeline/head Something is wrong with the build of this commit Details
2025-03-18 14:27:41 +01:00
Lucas Lara García f7dcafbd52 refs #1724 OgBoot status added to global status component
testing/ogGui-multibranch/pipeline/head Something is wrong with the build of this commit Details
2025-03-18 13:55:40 +01:00
Nicolas Arenas 06f969f43f jenkins-deb (#15)
testing/ogGui-multibranch/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/head This commit looks good Details
Include Jenkinsfile in main branch

Reviewed-on: #15

refs #1715
2025-03-18 10:56:19 +01:00
Lucas Lara García f004de1ebd Add check for ogBootServicesStatus in getServices method to prevent errors
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-18 10:22:50 +01:00
Lucas Lara García a125252be9 Fix loading state handling in Global Status component template
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-18 10:20:00 +01:00
Lucas Lara García e8e68649cd refs #1705 Enhance Global Status component to reload OgBoot status on tab change
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-18 10:13:30 +01:00
Lucas Lara García 984e4fe4db refs #1705 Add Global Status component with styling and integration in header
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-18 09:56:36 +01:00
Manuel Aranda Rosales d1af610e93 Refactor transferImages
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-18 09:26:43 +01:00
Manuel Aranda Rosales fd612b1a66 Test refactor transferImage
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-17 09:05:40 +01:00
Nicolas Arenas 251708e21e reates debian folder to create debian packages , refs #1708
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
Reviewed-on: #14
2025-03-13 17:04:07 +01:00
Manuel Aranda Rosales 82eea78c30 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-13 14:47:29 +01:00
Manuel Aranda Rosales 083b46a94d refs #1702. Updated ogLive sync. Deleted wrong or uninstalled oglives 2025-03-13 14:44:11 +01:00
Lucas Lara García 8312132e1f refs #1684 Remove localization support and update translations for software management
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-13 14:25:59 +01:00
Lucas Lara García e230b3b41d refs #1700 Add mock ConfigService to component tests for improved isolation
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-13 12:00:08 +01:00
Manuel Aranda Rosales 44199881cc refs #1671. Udpated deploy method type. Added udpcast-direct'
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-13 11:14:33 +01:00
Manuel Aranda Rosales a5617ad012 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-12 17:35:48 +01:00
Manuel Aranda Rosales bd14cbcfd0 refs #1693. Convert Image. Webhook updated. 2025-03-12 17:35:40 +01:00
Lucas Lara García dc99c2d2a7 refs #1690 Add mercureUrl to config and refactor services to use ConfigService for API URLs
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-12 14:23:47 +01:00
Lucas Lara García 40385bc73c refs #1690 Add ConfigService integration to EnvVars and Roles components and fixed tests.
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-12 10:29:54 +01:00
Manuel Aranda Rosales fda7d9b154 Updated changelog
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-12 09:54:44 +01:00
Manuel Aranda Rosales 294e85508b Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-11 17:10:11 +01:00
Manuel Aranda Rosales 16c367e770 refs #1693. Convert Image 2025-03-11 17:10:04 +01:00
Lucas Lara García 68d5f7f006 refs #1688 Add ConfigService for loading application configuration at startup
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-11 13:46:56 +01:00
Manuel Aranda Rosales d2b3c8f772 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-11 11:12:31 +01:00
Manuel Aranda Rosales 769d55a624 Merge branch 'main' into develop 2025-03-11 11:12:12 +01:00
Manuel Aranda Rosales 7a7c3e8e0d Added Mercure_URL var env
testing/ogGui-multibranch/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/tag This commit looks good Details
2025-03-11 11:10:37 +01:00
Lucas Lara García a6806c5fa7 Use ngx-translate exclusively instead of i18n
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-10 17:56:11 +01:00
Manuel Aranda Rosales 70319d718f Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-10 16:21:43 +01:00
Manuel Aranda Rosales adc11df008 Improvements repository UX 2025-03-10 16:21:03 +01:00
Lucas Lara García 8e10d135e1 refs #1638. Refactor client details layout and improve sync status display
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-07 14:41:11 +01:00
Lucas Lara García b0d24b4799 Update client name styling for improved readability and layout
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-07 10:35:06 +01:00
Lucas Lara García 9ab68cc6e2 refs #1654. Refactor create image component for improved UX and loading state management
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-07 09:27:37 +01:00
Manuel Aranda Rosales 67ebc5b926 Merge pull request 'develop' (#13) from develop into main
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/tag This commit looks good Details
Reviewed-on: #13
2025-03-06 08:12:20 +01:00
Manuel Aranda Rosales 83c3b3caed Added changeLol 0.9.0
testing/ogGui-multibranch/pipeline/pr-main There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-06 08:09:09 +01:00
Manuel Aranda Rosales 754dc8ed15 UX and CSS improvements
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-05 17:33:11 +01:00
Manuel Aranda Rosales 2b69ef3bd6 Added getImage api call
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-05 17:32:45 +01:00
Manuel Aranda Rosales b23d1727e8 refs #1645. Cancel deploy image button
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-05 17:31:56 +01:00
Lucas Lara García c568d5a8e7 refs #1641. Implement debounced client filtering in groups component
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-05 15:45:23 +01:00
Lucas Lara García 5907404f77 refs #1639. Enhance user modal for add/edit functionality and improve password change dialog
testing/ogGui-multibranch/pipeline/head This commit looks good Details
2025-03-05 13:25:22 +01:00
Manuel Aranda Rosales 9b67d6ef43 Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-03-05 09:02:17 +01:00
Manuel Aranda Rosales 9c004441a8 Chagned filter groups and user improvements 2025-03-05 09:02:07 +01:00
359 changed files with 14896 additions and 4425 deletions

7
.gitignore vendored
View File

@ -1,2 +1,9 @@
ogWebconsole/.env
ogWebconsole/test-results/ogGui-junit-report.xml
node_modules/
### Debian packaging
debian/oggui
debian/*.substvars
debian/*.log
debian/.debhelper/
debian/files

View File

@ -1,20 +1,144 @@
# Changelog
## [0.14.1] - 2025-06-09
### Fixed
- Se han corregido los errores en produccion que hacia que no salieran mensajes desde la API correctamente.
---
## [0.14.0] - 2025-06-02
### Added
- Se ha añadido funcionalidad de usuarios/roles para separar las vistas segun los permisos.
- Nuevo boton de "mover" en la pantalla de grupos, para mover equipos entre aulas y grupos.
### Improved
- Se ha completado la opcion de inicion de sesion y eliminar imagen cache.
---
## [0.13.1] - 2025-05-23
### Changed
- Desactivado temporalmente la funcionalidad de ogGit.
---
## [0.13.0] - 2025-05-20
### Added
- Se ha añadido nuevo campo "SSL_ENABLED" en el apartado configuracion.
### Improved
- Mejoras en el asistente de particionado.
- Se han añadido mejoras en los filtros de las trazas.
---
## [0.12.0] - 2025-5-13
### Added
- Se ha añadido un nuevo modal del detalle de las acciones ejecutadas por cada cliente.
- Se ha añadido un modulo para la gestion de las tareas y acciones programadas.
- Se han añadido nuevos campos en el listado general de clientes.
### Improved
- Se ha cambiado la pagina de detalles de un cliente, por un modal.
- Se han actualizado gran parte de las ayudas contextuales de las distintas parrillas de datos.
- Se ha mejorado y corregido los errores del particionador.
- Mejoras en la pantalla de trazas.
- Cambios en la estetica general de la aplicacion
- Añadida la primera version de la la integracion con ogGit
- Se ha mejorado la responsividad de la aplicacion, para pantallas pequeñas.
### Fixed
- Se ha corregido un error que hacia que no apareciesen los calendarios en la pantalla de editar OU.
- En la pantalla de hacer deploy, al seleccionar imagen ahora deja desmarcarla.
## [0.11.2] - 2025-4-16
### Fixed
- Se ha corregido un error en la actualizacion del estado de los pcs en la vista tarjetas.
---
## [0.11.1] - 2025-4-16
### Improved
- Nuevos campos en la tabla de clientes. Tipo de firmware y mac.
## Fixed
- Se ha corregido error al crear OUs, que no refrescaba la web.
- Se ha corregido error en el formulario de creacion de imagenes. Si se seleccionaba una imagen para un versionado, no dejaba deseleccionar.
- Se ha corregido un bug en el particionador que impedia ejecutar, cuando eliminabamos una particion.
---
## [0.11.0] - 2025-4-11
### Added
- Se ha diseñado el nuevo formulario para poder ejecutar script. Sistema mejorado con variables etiquetadas.
- Se puede añadir descripcion a una imagen.
- Se han añadido al formulario de crear/editar repositorio, la posibilidad de añadir usuario y puerto ssh.
- Nuevo estado en pc => desconectado.
- Se ha añadido nueva accion para renombrar imagen monolitica.
### Improved
- Se ha mejorado la interfaz de usuario tanto para el despliegue de imagenes, como el particionado.
- Se ha mejorado la responsividad de la vista de grupos.
- Cambios en el comportamiento general de muchos componentes modales. Se han añadido spinners de carga mas intuitivos.
---
## [0.10.1] - 2025-3-27
### Improved
- Mejoras en el comportamiento del arbol de grupos.
- Nueva regexp para controlar las "macs" en la creacion de clientes.
---
## [0.10.0] - 2025-3-25
### Added
- Nuevo componenten de estado global.
- Servicio para que el ogGui obtenga de forma dinamica las variables de entorno.
- Nueva funcionalidad para convertir imagen en imagen virtual.
- Nueva funcionalidad para importar imágenes externas al sistema.
- Despliegue de imangenes sin cache. Cambios en el formulario de "despliegue".
### Improved
- Mejoras en la internacionalización.
- Nueva UX ogRepository. Ahora se gestionan las imagenes de forma mas sencilla.
- Cambios en ogLive. Mejora en la sincronizacion y obtención de datos en la API
### Fixed
- Cambios en la expresion regular para la validacion de documentos DHCP en la carga masiva de pc.
---
## [0.9.2] - 2025-03-19
### Changed
- Jenkinsfile to pubilsh packages in repo in case og release
---
## [0.9.1] - 2025-03-12
### Changed
- Se ha modificado el acceso a Mercure añadiendo nueva variable de entorno.
---
## [0.9.0] - 2025-3-4
### Added
- Integracion con Mercure. Subscriber tanto en "Trazas" con en "Clientes".
- Nueva funcionalidad para checkear la integridad de una imagen. Boton en apartado "imagenes" dentro del repositorio.
- Centralizacion de estilos.
- Nueva funcionalidad para realizar backup de imágenes.
- Botón para cancelar despliegues de imagenes. Aparece en "trazas" tan solo para los comendos "deploy" y para el estado "en progreso".
### Changed
- Nueva interfaz en "Grupos". Se ha aprovechado mejor el espacio y acortado el tamaño de las filas, para poder tener mas elementos por pantalla.
- Cambios en filtros de "Grupos". Ahora se pueden filtrar por "Centro" y "Unidad Organizativa" y estado. Ahora se busca en base de datos, y no en una lista de clientes dados.
- Refactorizados compontentes de crear/editar clientes en uno solo.
- Cambios en DHCP. Nueva UX en "ver clientes". Ahora tenemos un buscador detallado.
- Para gestionar/añadir clientes a subredes ahora tenemos un botón para "añadir todos" y tan solo nos aparecn los equipos que no estén previamente asignados en una subred.
---
## [0.7.0] - 2024-12-10
### Refactored
- Refactored the group screen, removing the separate tabs for clients, advanced search, and organizational units.
- Added support for partitioning functionality in the client detail view.
---
## [0.6.1] - 2024-11-19
### Improved
- Introduced a new automatic sync mode for the ogdhcp and ogBoot components.
- Improve test coverage.
- New view for clients inside the classroom on the main page.
---
## [0.6.0] - 2024-11-19
### Added
- Added functionality to execute actions from the menu in the general groups screen.
- Displayed the selected center on the general screen for better context.

View File

@ -1,5 +0,0 @@
oggui (1.0) unstable; urgency=low
* Initial release.
-- Your Name <nicolas.arenas@qindel.com> Thu, 01 Jan 1970 00:00:00 +0000

View File

@ -1,9 +0,0 @@
Package: oggui
Version: %%VERSION%%
Section: base
Priority: optional
Architecture: all
Depends: nginx , npm , nodejs
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
Description: Description of the ogcore package
This is a longer description of the ogcore package.

View File

@ -1,21 +0,0 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ogcore
Source: <source URL>
Files: *
Copyright: 2023 Your Name <your.email@example.com>
License: GPL-3+
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this package; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
MA 02110-1301 USA.

View File

@ -1,40 +0,0 @@
#!/bin/bash
set -e
# Asegurarse de que el usuario exista
USER="opengnsys"
HASH_FILE="/opt/opengnsys/oggui/var/lib/oggui/oggui.config.hash"
CONFIG_FILE="/opt/opengnsys/oggui/src/.env"
# Provisionar base de datos si es necesario en caso de instalación.
# Detectar si es una instalación nueva o una actualización
if [ "$1" = "configure" ] && [ -z "$2" ]; then
cd /opt/opengnsys/oggui/src/
npm install -g @angular/cli
npm install
/usr/local/bin/ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production --localize=false
cp -pr /opt/opengnsys/oggui/src/dist/oggui/browser/* /opt/opengnsys/oggui/browser/
md5sum "$CONFIG_FILE" > "$HASH_FILE"
ln -s /opt/opengnsys/oggui/etc/systemd/system/oggui.service /etc/systemd/system/oggui.service
systemctl daemon-reload
systemctl enable oggui
elif [ "$1" = "configure" ] && [ -n "$2" ]; then
cd /opt/opengnsys/oggui
echo "Actualización desde la versión $2"
fi
# Cambiar la propiedad de los archivos al usuario especificado
chown opengnsys:www-data /opt/opengnsys/
chown -R opengnsys:www-data /opt/opengnsys/oggui
# Install http server stuff
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf
# Reiniciar servicios si es necesario
# systemctl restart nombre_del_servicio
systemctl daemon-reload
systemctl restart nginx
exit 0

View File

@ -1,15 +0,0 @@
#!/bin/bash
set -e
# Asegurarse de que el usuario exista
USER="opengnsys"
HOME_DIR="/opt/opengnsys"
if id "$USER" &>/dev/null; then
echo "El usuario $USER ya existe."
else
echo "Creando el usuario $USER con home en $HOME_DIR."
useradd -m -d "$HOME_DIR" -s /bin/bash "$USER"
fi
exit 0

View File

@ -0,0 +1,107 @@
@Library('jenkins-shared-library') _
pipeline {
agent {
label 'jenkins-slave'
}
environment {
DEBIAN_FRONTEND = 'noninteractive'
DEFAULT_DEV_NAME = 'Opengnsys Team'
DEFAULT_DEV_EMAIL = 'opengnsys@qindel.com'
}
options {
skipDefaultCheckout()
}
parameters {
string(name: 'DEV_NAME', defaultValue: '', description: 'Nombre del desarrollador')
string(name: 'DEV_EMAIL', defaultValue: '', description: 'Email del desarrollador')
}
stages {
stage('Prepare Workspace') {
steps {
script {
env.BUILD_DIR = "${WORKSPACE}/oggui"
sh "mkdir -p ${env.BUILD_DIR}"
}
}
}
stage('Checkout') {
steps {
dir("${env.BUILD_DIR}") {
checkout scm
}
}
}
stage('Generate Changelog') {
when {
expression {
return env.TAG_NAME != null
}
}
steps {
script {
def devName = params.DEV_NAME ? params.DEV_NAME : env.DEFAULT_DEV_NAME
def devEmail = params.DEV_EMAIL ? params.DEV_EMAIL : env.DEFAULT_DEV_EMAIL
generateDebianChangelog(env.BUILD_DIR, devName, devEmail)
}
}
}
stage('Generate Changelog (Nightly)'){
when {
branch 'main'
}
steps {
script {
def devName = params.DEV_NAME ? params.DEV_NAME : env.DEFAULT_DEV_NAME
def devEmail = params.DEV_EMAIL ? params.DEV_EMAIL : env.DEFAULT_DEV_EMAIL
generateDebianChangelog(env.BUILD_DIR, devName, devEmail,"nightly")
}
}
}
stage('Build') {
steps {
script {
construirPaquete(env.BUILD_DIR, "../artifacts", "172.17.8.68", "/var/tmp/opengnsys/debian-repo/oggui")
}
}
}
stage ('Publish to Debian Repository') {
when {
expression {
return env.TAG_NAME != null
}
}
agent { label 'debian-repo' }
steps {
script {
// Construir el patrón de versión esperado en el nombre del paquete
def versionPattern = "${env.TAG_NAME}-${env.BUILD_NUMBER}"
publicarEnAptly('/var/tmp/opengnsys/debian-repo/oggui', 'opengnsys-devel', versionPattern)
}
}
}
stage ('Publish to Debian Repository (Nightly)') {
when {
branch 'main'
}
agent { label 'debian-repo' }
steps {
script {
// Construir el patrón de versión esperado en el nombre del paquete
def versionPattern = "-${env.BUILD_NUMBER}~nightly"
publicarEnAptly('/var/tmp/opengnsys/debian-repo/oggui', 'nightly', versionPattern)
}
}
}
}
post {
always {
notifyBuildStatus('narenas@qindel.com')
}
}
}

14
debian/changelog vendored 100644
View File

@ -0,0 +1,14 @@
oggui (0.0.1-1) unstable; urgency=medium
* Add debian files
* Update .gitignore
* refs #1637 refactor: remove unused client edit and create components; add manage client component
* refs #1619. Style: enhance cards view layout and paginator integration in groups component
* refactor: update paginator settings and improve page change handling in groups component
* Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
* style: clean up and optimize CSS for groups component; enhance HTML structure and improve responsiveness
* Updated groups paginator
* Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
* refs #1567. New subnet field: 'dns'
-- Tu Nombre <tuemail@example.com> Mon, 10 Mar 2025 14:48:36 +0000

1
debian/compat vendored 100644
View File

@ -0,0 +1 @@
12

13
debian/control vendored 100644
View File

@ -0,0 +1,13 @@
Source: oggui
Section: web
Priority: optional
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
Build-Depends: debhelper (>= 12), nodejs, npm
Standards-Version: 4.5.0
Package: oggui
Architecture: any
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
Depends: ${shlibs:Depends}, ${misc:Depends}, nginx
Description: OpenGnsys GUI created for the Opengnsys Team
Opengnsys Graphical Intercface

1
debian/debhelper-build-stamp vendored 100644
View File

@ -0,0 +1 @@
oggui

2
debian/files vendored 100644
View File

@ -0,0 +1,2 @@
oggui_1.0.1+deb-pkg20250310-1_amd64.buildinfo web optional
oggui_1.0.1+deb-pkg20250310-1_amd64.deb web optional

10
debian/oggui.config vendored 100644
View File

@ -0,0 +1,10 @@
#!/bin/sh
set -e
. /usr/share/debconf/confmodule
db_input high opengnsys/oggui_ogcoreUrl || true
db_input high opengnsys/oggui_ogmercureUrl || true
db_go

4
debian/oggui.install vendored 100644
View File

@ -0,0 +1,4 @@
ogWebconsole/dist/oggui/browser /opt/opengnsys/oggui/
etc /opt/opengnsys/oggui/
ogWebconsole/ssl/* /opt/opengnsys/oggui/etc/nginx/certs/

58
debian/oggui.postinst vendored 100644
View File

@ -0,0 +1,58 @@
#!/bin/bash
set -e
set -x
. /usr/share/debconf/confmodule
db_get opengnsys/oggui_ogcoreUrl
OGCORE_URL="$RET"
db_get opengnsys/oggui_ogmercureUrl
OGMERCURE_URL="$RET"
# Asegurarse de que el usuario exista
USER="opengnsys"
CONFIG_FILE="/opt/opengnsys/oggui/browser/assets/config.json"
restore_config_if_modified() {
local new="$1"
local backup="$1.bak"
if [ -f "$backup" ]; then
if ! cmp -s "$new" "$backup"; then
echo ">>> Archivo modificado por el usuario detectado en $new"
echo " - Guardando archivo nuevo como ${new}.new"
mv -f "$new" "${new}.new"
echo " - Restaurando archivo anterior desde backup"
mv -f "$backup" "$new"
else
echo ">>> El archivo $new no ha cambiado desde la última versión, eliminando backup"
rm -f "$backup"
fi
fi
}
# Detectar si es una instalación nueva o una actualización
if [ "$1" = "configure" ] && [ -z "$2" ]; then
jq --arg apiUrl "$OGCORE_URL" --arg mercureUrl "$OGMERCURE_URL" '.apiUrl = $apiUrl | .mercureUrl = $mercureUrl' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf
ln -s $CONFIG_FILE /opt/opengnsys/oggui/etc/config.json
mkdir -p /etc/nginx/certs/
cp -p /opt/opengnsys/oggui/etc/nginx/certs/* /etc/nginx/certs/
chown -R www-data:www-data /etc/nginx/certs
systemctl daemon-reload
systemctl restart nginx
elif [ "$1" = "configure" ] && [ -n "$2" ]; then
cd /opt/opengnsys/oggui
echo "Actualización desde la versión $2"
# Si upgrade recupero los archivos de configuracion
echo ">>> Backup de archivos de configuración reales en /opt/opengnsys"
restore_config_if_modified "/opt/opengnsys/oggui/etc/nginx/oggui.conf"
restore_config_if_modified "$CONFIG_FILE"
fi
# Cambiar la propiedad de los archivos al usuario especificado
chown opengnsys:www-data /opt/opengnsys/
chown -R opengnsys:www-data /opt/opengnsys/oggui
exit 0

6
debian/oggui.postrm.debhelper vendored 100644
View File

@ -0,0 +1,6 @@
# Automatically added by dh_installdebconf/13.14.1ubuntu5
if [ "$1" = purge ] && [ -e /usr/share/debconf/confmodule ]; then
. /usr/share/debconf/confmodule
db_purge
fi
# End automatically added section

32
debian/oggui.preinst vendored 100644
View File

@ -0,0 +1,32 @@
#!/bin/bash
set -e
backup_file_if_exists() {
local original="$1"
local backup="$1.bak"
if [ -e "$original" ]; then
echo " - Guardando backup de $original en $backup"
cp -a "$original" "$backup"
fi
}
CONFIG_FILE="/opt/opengnsys/oggui/browser/assets/config.json"
# Asegurarse de que el usuario exista
USER="opengnsys"
HOME_DIR="/opt/opengnsys"
if id "$USER" &>/dev/null; then
echo "El usuario $USER ya existe."
else
echo "Creando el usuario $USER con home en $HOME_DIR."
useradd -m -d "$HOME_DIR" -s /bin/bash "$USER"
fi
# Si upgrade hago backup del archivo de configuración
if [ "$1" = "upgrade" ]; then
echo ">>> Backup de archivos de configuración reales en /opt/opengnsys"
backup_file_if_exists "/opt/opengnsys/oggui/etc/nginx/sites-available/oggui.conf"
backup_file_if_exists "$CONFIG_FILE"
fi
exit 0

13
debian/oggui.prerm vendored 100644
View File

@ -0,0 +1,13 @@
#!/bin/bash
set -e
set -x
# Solo eliminar archivos de configuración si se está eliminando el paquete
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
rm -f /etc/nginx/sites-enabled/oggui.conf
systemctl daemon-reload
systemctl restart nginx
fi
exit 0

9
debian/oggui.templates vendored 100644
View File

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

11
debian/rules vendored 100755
View File

@ -0,0 +1,11 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_build:
cd ogWebconsole && npm install
cd ogWebconsole && npx ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production
override_dh_auto_install:
dh_auto_install

View File

@ -1,5 +1,5 @@
server {
listen 4200;
listen 4200 ssl;
server_name localhost;
root /opt/opengnsys/oggui/browser;
@ -13,7 +13,8 @@ server {
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
try_files $uri =404;
}
ssl_certificate /opt/opengnsys/oggui/etc/nginx/certs/oggui.uds-test.net.crt.pem;
ssl_certificate_key /opt/opengnsys/oggui/etc/nginx/certs/oggui.uds-test.net.key.pem;
# Configuración para evitar problemas con rutas de Angular
error_page 404 /index.html;
}

View File

@ -1,14 +0,0 @@
[Unit]
Description=Aplicación Angular con Nginx
After=network.target
[Service]
Type=simple
ExecStart=/opt/opengnsys/oggui/bin/start-oggui.sh
Restart=always
User=www-data
WorkingDirectory=/var/www/mi-aplicacion
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target

View File

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

View File

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

View File

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

View File

@ -4,12 +4,6 @@
"newProjectRoot": "projects",
"projects": {
"ogWebconsole": {
"i18n": {
"sourceLocale": "es",
"locales": {
"en": "src/locale/en.json"
}
},
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
@ -30,7 +24,7 @@
"builder": "@ngx-env/builder:application",
"options": {
"baseHref": "/oggui/",
"localize": true,
"localize": false,
"aot": true,
"outputPath": "dist/og-webconsole",
"index": "src/index.html",
@ -41,20 +35,23 @@
],
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "src/locale",
"output": "/locale"
}
],
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "src/locale",
"output": "/locale"
}
],
"styles": [
"src/custom-theme.scss",
"src/styles.css",
"node_modules/ngx-toastr/toastr.css"
],
"scripts": []
"scripts": [],
"allowedCommonJsDependencies": [
"rfdc"
]
},
"configurations": {
"production": {
@ -66,7 +63,7 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kb",
"maximumWarning": "7kb",
"maximumError": "10kb"
}
],
@ -76,16 +73,6 @@
"optimization": false,
"extractLicenses": false,
"sourceMap": false
},
"es": {
"localize": [
"es-ES"
]
},
"en": {
"localize": [
"en-US"
]
}
},
"defaultConfiguration": "production"
@ -104,29 +91,16 @@
},
"development": {
"buildTarget": "ogWebconsole:build:development"
},
"es": {
"buildTarget": "ogWebconsole:build:es"
},
"en": {
"buildTarget": "ogWebconsole:build:en"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@ngx-env/builder:extract-i18n",
"options": {
"buildTarget": "ogWebconsole:build"
}
},
"test": {
"builder": "@ngx-env/builder:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing",
"@angular/localize/init"
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
@ -146,4 +120,4 @@
"cli": {
"analytics": "95fac95c-8936-41a8-8c9c-1fae82fe6912"
}
}
}

View File

@ -3,30 +3,26 @@ import { RouterModule, Routes } from '@angular/router';
import { MainLayoutComponent } from './layout/main-layout/main-layout.component';
import { AuthLayoutComponent } from './layout/auth-layout/auth-layout.component';
import { LoginComponent } from './components/login/login.component';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component';
import { AdminComponent } from './components/admin/admin.component';
import { UsersComponent } from './components/admin/users/users/users.component';
import { RolesComponent } from './components/admin/roles/roles/roles.component';
import { GroupsComponent } from './components/groups/groups.component';
import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.component';
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
import {OgbootStatusComponent} from "./components/ogboot/ogboot-status/ogboot-status.component";
import { OgbootStatusComponent } from "./components/ogboot/ogboot-status/ogboot-status.component";
import { CalendarComponent } from "./components/calendar/calendar.component";
import { CommandsComponent } from './components/commands/main-commands/commands.component';
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
import { ImagesComponent } from './components/images/images.component';
import {SoftwareComponent} from "./components/software/software.component";
import {SoftwareProfileComponent} from "./components/software-profile/software-profile.component";
import {OperativeSystemComponent} from "./components/operative-system/operative-system.component";
import { TaskLogsComponent } from './components/task-logs/task-logs.component';
import { SoftwareComponent } from "./components/software/software.component";
import { SoftwareProfileComponent } from "./components/software-profile/software-profile.component";
import { OperativeSystemComponent } from "./components/operative-system/operative-system.component";
import {
PartitionAssistantComponent
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
import {RepositoriesComponent} from "./components/repositories/repositories.component";
import { RepositoriesComponent } from "./components/repositories/repositories.component";
import {
CreateClientImageComponent
} from "./components/groups/components/client-main-view/create-image/create-image.component";
@ -36,49 +32,52 @@ import {
import {
MainRepositoryViewComponent
} from "./components/repositories/main-repository-view/main-repository-view.component";
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
import {MenusComponent} from "./components/menus/menus.component";
import {OgDhcpSubnetsComponent} from "./components/ogdhcp/og-dhcp-subnets.component";
import {StatusComponent} from "./components/ogdhcp/status/status.component";
import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component";
import { MenusComponent } from "./components/menus/menus.component";
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
import { StatusComponent } from "./components/ogdhcp/status/status.component";
import {
RunScriptAssistantComponent
} from "./components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component";
import { roleGuard } from './guards/role.guard';
import { LogoutGuard } from './guards/logout.guard';
const routes: Routes = [
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
{ path: '', component: MainLayoutComponent,
{
path: '', component: MainLayoutComponent,
children: [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'admin', component: AdminComponent },
{ path: 'users', component: UsersComponent },
{ path: 'env-vars', component: EnvVarsComponent },
{ path: 'user-groups', component: RolesComponent },
{ 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: 'groups', component: GroupsComponent },
{ path: 'pxe-images', component: PXEimagesComponent },
{ path: 'pxe', component: PxeComponent },
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
{ path: 'ogboot-status', component: OgbootStatusComponent },
{ path: 'subnets', component: OgDhcpSubnetsComponent },
{ path: 'ogdhcp-status', component: StatusComponent },
{ path: 'commands', component: CommandsComponent },
{ path: 'commands-groups', component: CommandsGroupsComponent },
{ path: 'commands-task', component: CommandsTaskComponent },
{ path: 'commands-logs', component: TaskLogsComponent },
{ path: 'calendars', component: CalendarComponent },
{ path: 'clients/deploy-image', component: DeployImageComponent },
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
{ path: 'clients/:id', component: ClientMainViewComponent },
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
{ path: 'images', component: ImagesComponent },
{ path: 'repositories', component: RepositoriesComponent },
{ path: 'repository/:id', component: MainRepositoryViewComponent },
{ path: 'software', component: SoftwareComponent },
{ path: 'software-profiles', component: SoftwareProfileComponent },
{ path: 'operative-systems', component: OperativeSystemComponent },
{ path: 'menus', component: MenusComponent },
{ path: 'pxe-images', component: PXEimagesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'pxe', component: PxeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'pxe-boot-file', component: PxeBootFilesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'ogdhcp-status', component: StatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'clients/run-script', component: RunScriptAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'clients/:id/create-image', component: CreateClientImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'repositories', component: RepositoriesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'repository/:id', component: MainRepositoryViewComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'software', component: SoftwareComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'software-profiles', component: SoftwareProfileComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'operative-systems', component: OperativeSystemComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'menus', component: MenusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
],
},
{
path: 'auth',
component: AuthLayoutComponent,
children: [
{ path: 'login', component: LoginComponent },
{ path: 'login', component: LoginComponent, canActivate: [LogoutGuard] },
],
},
{ path: '**', component: PageNotFoundComponent },

View File

@ -1,4 +1,5 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, LOCALE_ID, APP_INITIALIZER } from '@angular/core';
import { ConfigService } from './services/config.service';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@ -16,7 +17,6 @@ import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AdminComponent } from './components/admin/admin.component';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
@ -66,7 +66,7 @@ import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.co
import { CreatePXEImageComponent } from './components/ogboot/pxe-images/create-image/create-image/create-image.component';
import { InfoImageComponent } from './components/ogboot/pxe-images/info-image/info-image/info-image.component';
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
import { CreatePxeTemplateComponent } from './components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component';
import { CreatePxeTemplateComponent } from './components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component';
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
import { MatExpansionPanel, MatExpansionPanelDescription, MatExpansionPanelTitle } from "@angular/material/expansion";
import { OgbootStatusComponent } from './components/ogboot/ogboot-status/ogboot-status.component';
@ -86,9 +86,8 @@ import { CreateCommandGroupComponent } from './components/commands/commands-grou
import { DetailCommandGroupComponent } from './components/commands/commands-groups/detail-command-group/detail-command-group.component';
import { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component';
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-task.component';
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
import { TaskLogsComponent } from './components/task-logs/task-logs.component';
import { MatSliderModule } from '@angular/material/slider';
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
import { ImagesComponent } from './components/images/images.component';
import { CreateImageComponent } from './components/images/create-image/create-image.component';
import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component';
@ -101,7 +100,7 @@ import { OperativeSystemComponent } from './components/operative-system/operativ
import { CreateOperativeSystemComponent } from './components/operative-system/create-operative-system/create-operative-system.component';
import { ShowTemplateContentComponent } from './components/ogboot/pxe/show-template-content/show-template-content.component';
import { RepositoriesComponent } from './components/repositories/repositories.component';
import { CreateRepositoryComponent } from './components/repositories/create-repository/create-repository.component';
import { ManageRepositoryComponent } from './components/repositories/manage-repository/manage-repository.component';
import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component';
import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component';
import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component';
@ -117,8 +116,7 @@ import { CreateMultipleClientComponent } from './components/groups/shared/client
import { ExportImageComponent } from './components/images/export-image/export-image.component';
import { ImportImageComponent } from "./components/repositories/import-image/import-image.component";
import { LoadingComponent } from './shared/loading/loading.component';
import { RepositoryImagesComponent } from './components/repositories/repository-images/repository-images.component';
import { InputDialogComponent } from './components/commands/commands-task/task-logs/input-dialog/input-dialog.component';
import { InputDialogComponent } from './components/task-logs/input-dialog/input-dialog.component';
import { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component";
@ -129,10 +127,44 @@ import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-
import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component';
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component';
import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component';
import { ConvertImageComponent } from './components/repositories/convert-image/convert-image.component';
import {NgOptimizedImage, registerLocaleData} from '@angular/common';
import localeEs from '@angular/common/locales/es';
import { GlobalStatusComponent } from './components/global-status/global-status.component';
import { ShowMonoliticImagesComponent } from './components/repositories/show-monolitic-images/show-monolitic-images.component';
import { StatusTabComponent } from './components/global-status/status-tab/status-tab.component';
import { ConvertImageToVirtualComponent } from './components/repositories/convert-image-to-virtual/convert-image-to-virtual.component';
import { RunScriptAssistantComponent } from './components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component';
import {
SaveScriptComponent
} from "./components/groups/components/client-main-view/run-script-assistant/save-script/save-script.component";
import { EditImageComponent } from './components/repositories/edit-image/edit-image.component';
import { ShowGitImagesComponent } from './components/repositories/show-git-images/show-git-images.component';
import { RenameImageComponent } from './components/repositories/rename-image/rename-image.component';
import { ClientDetailsComponent } from './components/groups/shared/client-details/client-details.component';
import { PartitionTypeOrganizatorComponent } from './components/groups/shared/partition-type-organizator/partition-type-organizator.component';
import { CreateTaskScheduleComponent } from './components/commands/commands-task/create-task-schedule/create-task-schedule.component';
import { ShowTaskScheduleComponent } from './components/commands/commands-task/show-task-schedule/show-task-schedule.component';
import { ShowTaskScriptComponent } from './components/commands/commands-task/show-task-script/show-task-script.component';
import { CreateTaskScriptComponent } from './components/commands/commands-task/create-task-script/create-task-script.component';
import { ViewParametersModalComponent } from './components/commands/commands-task/show-task-script/view-parameters-modal/view-parameters-modal.component';
import { OutputDialogComponent } from './components/task-logs/output-dialog/output-dialog.component';
import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs/client-task-logs.component';
import { BootSoPartitionComponent } from './components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component';
import { RemoveCacheImageComponent } from './components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component';
import { ChangeParentComponent } from './components/groups/shared/change-parent/change-parent.component';
import { SoftwareProfilePartitionComponent } from './components/commands/main-commands/execute-command/software-profile-partition/software-profile-partition.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
export function initializeApp(configService: ConfigService) {
return () => configService.loadConfig();
}
registerLocaleData(localeEs, 'es-ES');
@NgModule({
declarations: [
AppComponent,
@ -141,7 +173,6 @@ export function HttpLoaderFactory(http: HttpClient) {
HeaderComponent,
SidebarComponent,
LoginComponent,
AdminComponent,
MainLayoutComponent,
UsersComponent,
RolesComponent,
@ -183,7 +214,6 @@ export function HttpLoaderFactory(http: HttpClient) {
TaskLogsComponent,
ServerInfoDialogComponent,
StatusComponent,
ClientMainViewComponent,
ImagesComponent,
CreateImageComponent,
PartitionAssistantComponent,
@ -195,7 +225,7 @@ export function HttpLoaderFactory(http: HttpClient) {
CreateOperativeSystemComponent,
ShowTemplateContentComponent,
RepositoriesComponent,
CreateRepositoryComponent,
ManageRepositoryComponent,
ExecuteCommandComponent,
ExecuteCommandOuComponent,
DeployImageComponent,
@ -208,12 +238,34 @@ export function HttpLoaderFactory(http: HttpClient) {
ExportImageComponent,
ImportImageComponent,
LoadingComponent,
RepositoryImagesComponent,
InputDialogComponent,
ManageOrganizationalUnitComponent,
BackupImageComponent,
ShowClientsComponent,
OperationResultDialogComponent
OperationResultDialogComponent,
ConvertImageComponent,
GlobalStatusComponent,
ShowMonoliticImagesComponent,
StatusTabComponent,
ConvertImageToVirtualComponent,
RunScriptAssistantComponent,
SaveScriptComponent,
EditImageComponent,
ShowGitImagesComponent,
RenameImageComponent,
ClientDetailsComponent,
PartitionTypeOrganizatorComponent,
CreateTaskScheduleComponent,
ShowTaskScheduleComponent,
ShowTaskScriptComponent,
CreateTaskScriptComponent,
ViewParametersModalComponent,
OutputDialogComponent,
ClientTaskLogsComponent,
BootSoPartitionComponent,
RemoveCacheImageComponent,
ChangeParentComponent,
SoftwareProfilePartitionComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,
@ -262,7 +314,7 @@ export function HttpLoaderFactory(http: HttpClient) {
progressAnimation: 'increasing',
closeButton: true
}
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger, NgOptimizedImage
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA,
@ -273,8 +325,16 @@ export function HttpLoaderFactory(http: HttpClient) {
useClass: CustomInterceptor,
multi: true
},
{ provide: LOCALE_ID, useValue: 'es-ES' },
provideAnimationsAsync(),
provideHttpClient(withInterceptorsFromDi())
provideHttpClient(withInterceptorsFromDi()),
ConfigService,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [ConfigService],
multi: true
}
],
})
export class AppModule { }

View File

@ -1,48 +0,0 @@
/* Estilos del contenedor para centrar los botones */
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
/* Estilos del contenedor de cada botón y texto */
.button-container {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 10px;
}
/* Estilos del texto debajo de los botones */
span{
margin: 0;
font-size: 20px;
text-align: center;
}
/* Media query para hacer los botones responsive */
@media (max-width: 900px) {
button {
height: 120px;
width: 120px;
}
}
@media (max-width: 600px) {
button {
height: 90px;
width: 90px;
}
}
@media (max-width: 400px) {
button {
height: 70px;
width: 70px;
}
span{
font-size: 14px;
}
}

View File

@ -1,10 +0,0 @@
<div class="container">
<button class="action-button" routerLink="/users">
<mat-icon>group</mat-icon>
<span>{{ 'labelUsers' | translate }}</span>
</button>
<button class="action-button" routerLink="/user-groups">
<mat-icon>admin_panel_settings</mat-icon>
<span>{{ 'labelRoles' | translate }}</span>
</button>
</div>

View File

@ -1,48 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AdminComponent } from './admin.component';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
import { Router } from '@angular/router';
describe('AdminComponent', () => {
let component: AdminComponent;
let fixture: ComponentFixture<AdminComponent>;
let router: Router;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AdminComponent],
imports: [
RouterTestingModule,
MatButtonModule,
MatIconModule,
TranslateModule.forRoot()
]
}).compileComponents();
router = TestBed.inject(Router);
});
beforeEach(() => {
fixture = TestBed.createComponent(AdminComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('debería crear el componente', () => {
expect(component).toBeTruthy();
});
it('debería renderizar dos botones', () => {
const buttons = fixture.nativeElement.querySelectorAll('button');
expect(buttons.length).toBe(2);
});
it('debería tener un botón con routerLink a "/users"', () => {
const button = fixture.nativeElement.querySelector('button[routerLink="/users"]');
expect(button).toBeTruthy();
expect(button.querySelector('mat-icon').textContent.trim()).toBe('group');
});
});

View File

@ -1,13 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-admin',
templateUrl: './admin.component.html',
styleUrl: './admin.component.css'
})
export class AdminComponent {
}

View File

@ -16,12 +16,20 @@
<ng-container matColumnDef="value">
<mat-header-cell *matHeaderCellDef> Valor </mat-header-cell>
<mat-cell *matCellDef="let variable">
<mat-form-field class="value-input">
<!-- Si es booleano, usamos checkbox -->
<mat-checkbox *ngIf="isBoolean(variable.value)"
[checked]="variable.value === 'true'"
(change)="variable.value = $event.checked ? 'true' : 'false'">
</mat-checkbox>
<!-- Si no es booleano, usamos input -->
<mat-form-field *ngIf="!isBoolean(variable.value)" class="value-input">
<input matInput [(ngModel)]="variable.value" />
</mat-form-field>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
@ -30,4 +38,4 @@
<button class="action-button" (click)="loadEnvVars()">Recargar</button>
<button class="submit-button" (click)="saveEnvVars()">Guardar Cambios</button>
</div>
</div>
</div>

View File

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

View File

@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import { HttpClient } from "@angular/common/http";
import { ToastrService } from "ngx-toastr";
import { ConfigService } from '@services/config.service';
@Component({
selector: 'app-env-vars',
@ -8,16 +9,17 @@ import {ToastrService} from "ngx-toastr";
styleUrl: './env-vars.component.css'
})
export class EnvVarsComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
envVars: { name: string; value: string }[] = [];
displayedColumns: string[] = ['name', 'value'];
private apiUrl = `${this.baseUrl}/env-vars`;
private apiUrl: string;
constructor(
private http: HttpClient,
private toastService: ToastrService,
) {}
private configService: ConfigService
) {
this.apiUrl = `${this.configService.apiUrl}/env-vars`;
}
ngOnInit(): void {
this.loadEnvVars();
@ -29,12 +31,15 @@ export class EnvVarsComponent {
this.envVars = Object.entries(response.vars).map(([name, value]) => ({ name, value }));
},
error: (err) => {
console.error('Error al cargar las variables de entorno:', err);
this.toastService.error('No se pudieron cargar las variables de entorno.');
}
});
}
isBoolean(value: string): boolean {
return value === 'true' || value === 'false';
}
saveEnvVars(): void {
const vars = this.envVars.reduce((acc, variable) => {
acc[variable.name] = variable.value;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<app-loading [isLoading]="loading"></app-loading>
<h1 mat-dialog-title>{{ 'dialogTitleAddUser' | translate }}</h1>
<h1 mat-dialog-title>{{ isEditMode ? ('dialogTitleEditUser' | translate) : ('dialogTitleAddUser' | translate) }}</h1>
<mat-dialog-content class="form-container">
<form [formGroup]="userForm" class="user-form">
<mat-form-field appearance="fill" class="full-width">
@ -29,10 +29,11 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Vista tarjetas</mat-label>
<mat-select formControlName="groupsView" required>
<mat-option *ngFor="let option of views" [value]="option.value" >
<mat-option *ngFor="let option of views" [value]="option.value">
{{ option.name }}
</mat-option>
</mat-select>
@ -41,5 +42,5 @@
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()">{{ 'buttonAdd' | translate }}</button>
</mat-dialog-actions>
<button class="submit-button" (click)="onSubmit()" [disabled]="userForm.invalid">{{ isEditMode ? ('buttonEdit' | translate) : ('buttonAdd' | translate) }}</button>
</mat-dialog-actions>

View File

@ -1,9 +1,10 @@
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {ToastrService} from "ngx-toastr";
import {HttpClient} from "@angular/common/http";
import {DataService} from "../data.service";
import { ToastrService } from "ngx-toastr";
import { HttpClient } from "@angular/common/http";
import { DataService } from "../data.service";
import { ConfigService } from '@services/config.service';
interface UserGroup {
'@id': string;
@ -17,17 +18,19 @@ interface UserGroup {
styleUrls: ['./add-user-modal.component.css']
})
export class AddUserModalComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
baseUrl: string;
@Output() userAdded = new EventEmitter<void>();
@Output() userEdited = new EventEmitter<void>();
userForm: FormGroup<any>;
userGroups: UserGroup[] = [];
organizationalUnits: any[] = [];
userId: string | null = null;
loading: boolean = false;
isEditMode: boolean = false;
protected views = [
{value: 'card', name: 'Tarjetas'},
{value: 'list', name: 'Listado'},
{ value: 'card', name: 'Tarjetas' },
{ value: 'list', name: 'Listado' },
];
constructor(
@ -36,8 +39,10 @@ export class AddUserModalComponent implements OnInit {
private http: HttpClient,
private fb: FormBuilder,
private dataService: DataService,
private toastService: ToastrService
private toastService: ToastrService,
private configService: ConfigService
) {
this.baseUrl = this.configService.apiUrl;
this.userForm = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required],
@ -45,6 +50,10 @@ export class AddUserModalComponent implements OnInit {
groupsView: ['card', Validators.required],
organizationalUnits: [[]]
});
if (data) {
this.isEditMode = true;
}
}
ngOnInit(): void {
@ -94,42 +103,49 @@ export class AddUserModalComponent implements OnInit {
onSubmit(): void {
if (this.userForm.valid) {
const payload = {
const payload: any = {
username: this.userForm.value.username,
allowedOrganizationalUnits: this.userForm.value.organizationalUnit,
password: this.userForm.value.password,
allowedOrganizationalUnits: this.userForm.value.organizationalUnits,
enabled: true,
userGroups: [this.userForm.value.role ],
userGroups: [this.userForm.value.role],
groupsView: this.userForm.value.groupsView
};
if (!this.userId && this.userForm.value.password) {
payload.password = this.userForm.value.password;
} else if (this.userId && this.userForm.value.password.trim() !== '') {
payload.password = this.userForm.value.password;
}
this.loading = true;
if (this.userId) {
this.http.put(`${this.baseUrl}${this.userId}`, payload).subscribe(
(response) => {
this.toastService.success('Usuario editado correctamente');
this.userEdited.emit();
this.dialogRef.close();
this.loading = false
this.loading = false;
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
this.loading = false
this.loading = false;
}
);
} else {
this.http.post(`${this.baseUrl}/users`, payload).subscribe(
(response) => {
this.toastService.success('Usuario añadido correctamente');
this.userAdded.emit();
this.dialogRef.close();
this.loading = false
this.loading = false;
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
this.loading = false
this.loading = false;
}
);
}
}
}
}
}

View File

@ -1,6 +1,5 @@
.user-form .form-field {
display: block;
margin-bottom: 10px;
}
.checkbox-group label {
@ -23,4 +22,8 @@ mat-spinner {
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
.form-container {
margin-top: 2em;
}

View File

@ -1,4 +1,4 @@
<h1 mat-dialog-title>{{ 'dialogTitleEditUser' | translate }}</h1>
<h1 mat-dialog-title>{{ 'dialogTitleChangePassword' | translate }}</h1>
<mat-dialog-content class="form-container">
<form [formGroup]="userForm" class="user-form">
<mat-form-field class="form-field">

View File

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

View File

@ -9,7 +9,6 @@
</div>
</div>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
@ -32,10 +31,10 @@
{{ user[column.columnDef] === 'card' ? 'Vista tarjetas' : 'Listado' }}
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'groupsView'" >
<ng-container *ngIf="column.columnDef !== 'groupsView'">
{{ column.cell(user) }}
</ng-container>
</td>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
@ -58,4 +57,4 @@
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,12 @@
.time-fields {
display: flex;
gap: 15px; /* Espacio entre los campos */
gap: 15px;
}
.hour-fields {
display: flex;
gap: 15px;
}
.time-field {
@ -34,4 +39,74 @@
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
}
.custom-text {
font-style: italic;
font-size: 0.875rem;
color: #666;
margin: 4px 0 12px;
}
.weekday-toggle-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 40px 0 40px 0;
width: 100%;
justify-content: space-between;
}
.weekday-toggle {
flex: 1 1 calc(14.28% - 10px);
padding: 10px 0;
border-radius: 999px;
border: 1px solid #ccc;
background-color: #f5f5f5;
cursor: pointer;
font-size: 14px;
text-align: center;
transition: all 0.2s ease;
min-width: 40px;
}
.weekday-toggle.selected {
background-color: #1976d2;
color: white;
border-color: #1976d2;
}
.availability-summary {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 12px 16px;
margin-top: 16px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.summary-text {
color: #0d47a1;
line-height: 1.4;
}
.unavailability-summary {
background-color: #ffebee;
border-left: 4px solid #d32f2f;
padding: 12px 16px;
margin-top: 16px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.summary-text {
color: #b71c1c;
line-height: 1.4;
}

View File

@ -1,24 +1,26 @@
<h2 mat-dialog-title>{{ isEditMode ? ('editCalendar' | translate) : ('addCalendar' | translate) }}</h2>
<mat-dialog-content class="form-container">
<mat-slide-toggle [(ngModel)]="isRemoteAvailable" class="example-margin">
<mat-checkbox [(ngModel)]="isRemoteAvailable">
{{ 'remoteAvailability' | translate }}
</mat-slide-toggle>
</mat-checkbox>
<mat-divider style="margin: 10px 0;"></mat-divider>
<div *ngIf="!isRemoteAvailable" class="form-group">
<mat-label>{{ 'selectWeekDays' | translate }}</mat-label>
<div class="row">
<div class="col-md-6 checkbox-group">
<mat-checkbox *ngFor="let day of weekDays.slice(0, (weekDays.length / 2) + 1)" [(ngModel)]="busyWeekDays[day]">
{{ day }}
</mat-checkbox>
</div>
<div class="col-md-6 checkbox-group">
<mat-checkbox *ngFor="let day of weekDays.slice(weekDays.length / 2 + 1)" [(ngModel)]="busyWeekDays[day]">
{{ day }}
</mat-checkbox>
</div>
<p class="custom-text"> (Los dias y horas seleccionados se marcarán como aula no disponible para remote pc.) </p>
<div class="weekday-toggle-group full-width">
<button
*ngFor="let day of weekDays"
type="button"
class="weekday-toggle"
[class.selected]="busyWeekDays[day]"
(click)="busyWeekDays[day] = !busyWeekDays[day]">
{{ day.slice(0, 3) }}
</button>
</div>
<div class="time-fields">
<mat-form-field appearance="fill" class="time-field">
<mat-label>{{ 'startTime' | translate }}</mat-label>
@ -30,12 +32,24 @@
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
</mat-form-field>
</div>
<mat-divider></mat-divider>
<div class="unavailability-summary" *ngIf="busyFromHour && busyToHour && busyWeekDays && getSelectedDays().length">
<mat-icon style="width: 50px;" color="warn">block</mat-icon>
<span class="summary-text">
El aula estará <strong>no disponible</strong> para Remote PC los días:
<strong>{{ getSelectedDays().join(', ') }}</strong> de
<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
</span>
</div>
</div>
<div *ngIf="isRemoteAvailable" class="form-group">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'reasonLabel' | translate }}</mat-label>
<input matInput [(ngModel)]="availableReason" placeholder="{{ 'reasonPlaceholder' | translate }}" [required]="isRemoteAvailable">
<mat-hint>Razón por la cual el aula SI está disponible para su uso en Remote PC</mat-hint>
</mat-form-field>
<div class="time-fields">
<mat-form-field appearance="fill" class="full-width">
@ -53,6 +67,32 @@
<mat-datepicker #picker2></mat-datepicker>
</mat-form-field>
</div>
<div class="hour-fields">
<mat-form-field appearance="fill" class="time-field">
<mat-label>{{ 'startTime' | translate }}</mat-label>
<input matInput [(ngModel)]="busyFromHour" type="time" placeholder="{{ 'startTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
</mat-form-field>
<mat-form-field appearance="fill" class="time-field">
<mat-label>{{ 'endTime' | translate }}</mat-label>
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
</mat-form-field>
</div>
<mat-divider></mat-divider>
<div class="availability-summary" *ngIf="availableFromDate && availableToDate">
<mat-icon color="primary" style="width: 50px;">info</mat-icon>
<span class="summary-text">
El aula estará <strong>disponible</strong> para reserva desde el
<strong>{{ availableFromDate | date:'fullDate' }}</strong> hasta el
<strong>{{ availableToDate | date:'fullDate' }}</strong>
<span *ngIf="busyFromHour && busyToHour">
en el horario de <strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
</span>
</span>
</div>
</div>
</mat-dialog-content>

View File

@ -1,7 +1,8 @@
import {Component, Inject} from '@angular/core';
import {ToastrService} from "ngx-toastr";
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import { Component, Inject } from '@angular/core';
import { ToastrService } from "ngx-toastr";
import { HttpClient } from "@angular/common/http";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { ConfigService } from '@services/config.service';
@Component({
selector: 'app-create-calendar-rule',
@ -9,7 +10,7 @@ import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
styleUrl: './create-calendar-rule.component.css'
})
export class CreateCalendarRuleComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
baseUrl: string;
name: string = '';
remoteCalendarRules: any[] = [];
weekDays: string[] = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
@ -29,20 +30,23 @@ export class CreateCalendarRuleComponent {
constructor(
private toastService: ToastrService,
private http: HttpClient,
private configService: ConfigService,
public dialogRef: MatDialogRef<CreateCalendarRuleComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) { }
) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
this.calendarId = this.data.calendar
if (this.data) {
this.isEditMode = true;
this.availableFromDate = this.data.rule? this.data.rule.availableFromDate : null;
this.availableToDate = this.data.rule? this.data.rule.availableToDate : null;
this.isRemoteAvailable = this.data.rule? this.data.rule.isRemoteAvailable : false;
this.availableReason = this.data.rule? this.data.rule.availableReason : null;
this.busyFromHour = this.data.rule? this.data.rule.busyFromHour : null;
this.busyToHour = this.data.rule? this.data.rule.busyToHour : null;
this.availableFromDate = this.data.rule ? this.data.rule.availableFromDate : null;
this.availableToDate = this.data.rule ? this.data.rule.availableToDate : null;
this.isRemoteAvailable = this.data.rule ? this.data.rule.isRemoteAvailable : false;
this.availableReason = this.data.rule ? this.data.rule.availableReason : null;
this.busyFromHour = this.data.rule ? this.data.rule.busyFromHour : null;
this.busyToHour = this.data.rule ? this.data.rule.busyToHour : null;
if (this.data.rule && this.data.rule.busyWeekDays) {
this.busyWeekDays = this.data.rule.busyWeekDays.reduce((acc: {
[x: string]: boolean;
@ -60,8 +64,8 @@ export class CreateCalendarRuleComponent {
this.dialogRef.close();
}
toggleAdditionalForm(): void {
this.showAdditionalForm = !this.showAdditionalForm;
getSelectedDays(): string[] {
return Object.keys(this.busyWeekDays || {}).filter(day => this.busyWeekDays[day]);
}
getSelectedDaysIndices() {
@ -70,6 +74,11 @@ export class CreateCalendarRuleComponent {
.filter(index => index !== -1);
}
convertDateToLocalISO(date: Date): string {
const adjustedDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
return adjustedDate.toISOString();
}
submitRule(): void {
this.getSelectedDaysIndices()
const selectedDaysArray = Object.keys(this.busyWeekDays).map((day, index) => this.busyWeekDays[index]);
@ -79,8 +88,8 @@ export class CreateCalendarRuleComponent {
busyWeekDays: this.selectedDaysIndices,
busyFromHour: this.busyFromHour,
busyToHour: this.busyToHour,
availableFromDate: this.availableFromDate,
availableToDate: this.availableToDate,
availableFromDate: this.availableFromDate ? this.convertDateToLocalISO(this.availableFromDate) : null,
availableToDate: this.availableToDate ? this.convertDateToLocalISO(this.availableToDate) : null,
isRemoteAvailable: this.isRemoteAvailable,
availableReason: this.availableReason
};
@ -89,7 +98,7 @@ export class CreateCalendarRuleComponent {
this.http.put(`${this.baseUrl}${this.ruleId}`, formData)
.subscribe({
next: (response) => {
this.toastService.success('Calendar updated successfully');
this.toastService.success('Calendar rule updated successfully');
this.dialogRef.close(true);
},
error: (error) => {
@ -101,7 +110,7 @@ export class CreateCalendarRuleComponent {
this.http.post(`${this.baseUrl}/remote-calendar-rules`, formData)
.subscribe({
next: (response) => {
this.toastService.success('Calendar created successfully');
this.toastService.success('Calendar rule created successfully');
this.dialogRef.close(true);
},
error: (error) => {

View File

@ -25,7 +25,7 @@
.time-fields {
display: flex;
gap: 15px; /* Espacio entre los campos */
gap: 15px;
}
.time-field {
@ -34,24 +34,25 @@
.list-item-content {
display: flex;
align-items: flex-start; /* Alinea el contenido al inicio */
justify-content: space-between; /* Espacio entre los textos y los íconos */
width: 100%; /* Asegúrate de que el contenido ocupe todo el ancho */
align-items: flex-start;
justify-content: space-between;
width: 100%;
}
.text-content {
flex-grow: 1; /* Permite que este contenedor ocupe el espacio disponible */
margin-right: 16px; /* Espaciado a la derecha para separar de los íconos */
flex-grow: 1;
margin-right: 16px;
margin-left: 10px;
margin-bottom: 16px;
}
.icon-container {
display: flex;
align-items: center; /* Alinea los íconos verticalmente */
align-items: center;
}
.right-icon {
margin-left: 8px; /* Espaciado entre los íconos */
margin-left: 8px;
cursor: pointer;
}
@ -60,4 +61,15 @@
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
}
.rule-available {
background-color: #e8f5e9;
border-left: 4px solid #4caf50;
}
.rule-unavailable {
background-color: #ffebee;
border-left: 4px solid #f44336;
}

View File

@ -6,7 +6,7 @@
<input matInput [(ngModel)]="name" required>
<mat-icon *ngIf="isEditMode" matSuffix (click)="submitForm()">mode_edit</mat-icon>
</mat-form-field>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div *ngIf="isEditMode" mat-subheader>{{ 'rulesHeader' | translate }}</div>
<button class="action-button" *ngIf="isEditMode" (click)="createRule()" style="padding: 10px;">
@ -18,14 +18,20 @@
<mat-list *ngIf="isEditMode">
<ng-container *ngFor="let rule of remoteCalendarRules;">
<mat-list-item>
<mat-list-item
[ngClass]="{
'rule-available': rule.isRemoteAvailable,
'rule-unavailable': !rule.isRemoteAvailable
}"
>
<div class="list-item-content">
<mat-icon matListItemIcon>event_available</mat-icon>
<div class="text-content">
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('statusAvailable' | translate) : ('statusUnavailable' | translate) }}</div>
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyWeekDaysMap }}</div>
<div matListItemLine *ngIf="rule.isRemoteAvailable">{{ rule.availableReason }} | {{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</div>
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('remotePcStatusAvailable' | translate) : ('remotePcStatusUnavailable' | translate) }}</div>
<div matListItemLine *ngIf="!rule.isRemoteAvailable">Días: <strong>{{ rule.busyWeekDaysMap }}</strong></div>
<div matListItemLine *ngIf="rule.isRemoteAvailable">Razón: {{ rule.availableReason }}</div>
<div matListItemLine *ngIf="rule.isRemoteAvailable">Días: <strong>{{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</strong></div>
<div matListItemLine>Horario: {{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
</div>
<div class="icon-container">
<button mat-icon-button color="primary" class="right-icon" (click)="createRule(rule)">

View File

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

View File

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

View File

@ -33,15 +33,6 @@
<ng-container *ngIf="column.columnDef !== 'commands'">
{{ column.cell(commandGroup) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'commands'" joyrideStep="viewCommandsStep" text="{{ 'viewCommandsStepText' | translate }}">
<button class="action-button" [matMenuTriggerFor]="menu">{{ 'viewCommands' | translate }}</button>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let command of commandGroup.commands">
{{ command.name }}
</button>
</mat-menu>
</ng-container>
</td>
</ng-container>

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
<app-loading [isLoading]="loading"></app-loading>
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
@ -23,36 +25,28 @@
<div *ngIf="!loading">
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
<ng-container matColumnDef="taskid">
<th mat-header-cell *matHeaderCellDef> {{ 'idColumn' | translate }} </th>
<td mat-cell *matCellDef="let task"> {{ task.id }} </td>
</ng-container>
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let task">
<ng-container *ngIf="column.columnDef !== 'management'">
{{ column.cell(task) }}
</ng-container>
<ng-container matColumnDef="notes">
<th mat-header-cell *matHeaderCellDef> {{ 'infoColumn' | translate }} </th>
<td mat-cell *matCellDef="let task"> {{ task.notes }} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'createdByColumn' | translate }} </th>
<td mat-cell *matCellDef="let task"> {{ task.createdBy }} </td>
</ng-container>
<ng-container matColumnDef="scheduledDate">
<th mat-header-cell *matHeaderCellDef> {{ 'executionDateColumn' | translate }} </th>
<td mat-cell *matCellDef="let task"> {{ task.dateTime | date:'short' }} </td>
</ng-container>
<ng-container matColumnDef="enabled">
<th mat-header-cell *matHeaderCellDef> {{ 'statusColumn' | translate }} </th>
<td mat-cell *matCellDef="let task"> {{ task.enabled ? ('enabled' | translate) : ('disabled' | translate) }} </td>
<ng-container *ngIf="column.columnDef === 'management'">
<button class="action-button" (click)="openShowScheduleDialog(task)"> Programaciones</button>
<button class="action-button" style="margin-left: 0.5vw;" (click)="openShowScriptDialog(task)">Acciones</button>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<td mat-cell *matCellDef="let task" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
<button mat-icon-button color="info" (click)="viewTaskDetails(task)">
<mat-icon>visibility</mat-icon>
<button mat-icon-button color="primary" (click)="manageScheduleAction(task)">
<mat-icon>watch</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="manageScriptAction(task)">
<mat-icon>code-blocks</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="editTask(task)">
<mat-icon>edit</mat-icon>

View File

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

View File

@ -6,6 +6,14 @@ import { CreateTaskComponent } from './create-task/create-task.component';
import { DetailTaskComponent } from './detail-task/detail-task.component';
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { JoyrideService } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
import {CreateTaskScheduleComponent} from "./create-task-schedule/create-task-schedule.component";
import {ShowClientsComponent} from "../../ogdhcp/show-clients/show-clients.component";
import {Subnet} from "../../ogdhcp/og-dhcp-subnets.component";
import {ShowTaskScheduleComponent} from "./show-task-schedule/show-task-schedule.component";
import {ShowTaskScriptComponent} from "./show-task-script/show-task-script.component";
import {CreateTaskScriptComponent} from "./create-task-script/create-task-script.component";
import {DatePipe} from "@angular/common";
@Component({
selector: 'app-commands-task',
@ -13,19 +21,34 @@ import { JoyrideService } from 'ngx-joyride';
styleUrls: ['./commands-task.component.css']
})
export class CommandsTaskComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
baseUrl: string;
tasks: any[] = [];
filters: { [key: string]: string | boolean } = {};
length: number = 0;
itemsPerPage: number = 10;
page: number = 1;
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
displayedColumns: string[] = ['taskid', 'notes', 'name', 'scheduledDate', 'enabled', 'actions'];
loading: boolean = false;
private apiUrl = `${this.baseUrl}/command-tasks`;
datePipe: DatePipe = new DatePipe('es-ES');
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
private joyrideService: JoyrideService) {}
columns = [
{ columnDef: 'id', header: 'ID', cell: (task: any) => task.id },
{ columnDef: 'name', header: 'Nombre de tarea', cell: (task: any) => task.name },
{ columnDef: 'organizationalUnit', header: 'Ámbito', cell: (task: any) => task.organizationalUnit.name },
{ columnDef: 'management', header: 'Gestiones', cell: (task: any) => task.schedules },
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss', 'UTC') },
{ columnDef: 'createdBy', header: 'Creado por', cell: (task: any) => task.createdBy },
];
displayedColumns: string[] = ['id', 'name', 'organizationalUnit', 'management', 'nextExecution', 'createdBy', 'actions'];
loading: boolean = false;
private apiUrl: string;
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
private configService: ConfigService,
private joyrideService: JoyrideService) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/command-tasks`;
}
ngOnInit(): void {
this.loadTasks();
@ -51,24 +74,25 @@ export class CommandsTaskComponent implements OnInit {
);
}
viewTaskDetails(task: any): void {
this.dialog.open(DetailTaskComponent, {
width: '800px',
data: { task },
}).afterClosed().subscribe(() => this.loadTasks());
}
openCreateTaskModal(): void {
this.dialog.open(CreateTaskComponent, {
width: '800px',
}).afterClosed().subscribe(() => this.loadTasks());
}).afterClosed().subscribe(result => {
if (result) {
this.loadTasks();
}
})
}
editTask(task: any): void {
this.dialog.open(CreateTaskComponent, {
width: '800px',
data: { task },
}).afterClosed().subscribe(() => this.loadTasks());
}).afterClosed().subscribe(result => {
if (result) {
this.loadTasks();
}
})
}
deleteTask(task: any): void {
@ -90,12 +114,66 @@ export class CommandsTaskComponent implements OnInit {
});
}
manageScheduleAction(task: any): void {
this.dialog.open(CreateTaskScheduleComponent, {
width: '800px',
data: { task },
}).afterClosed().subscribe( result => {
if (result) {
this.loadTasks();
}
})
}
manageScriptAction(task: any): void {
this.dialog.open(CreateTaskScriptComponent, {
width: '900px',
data: { task },
}).afterClosed().subscribe( result => {
if (result) {
this.loadTasks();
}
})
}
onPageChange(event: any): void {
this.page = event.pageIndex + 1;
this.itemsPerPage = event.pageSize;
this.loadTasks();
}
openShowScheduleDialog(commandTask: any) {
const dialogRef = this.dialog.open(ShowTaskScheduleComponent, {
width: '85vw',
height: '85vh',
maxWidth: '85vw',
maxHeight: '85vh',
data: { commandTask: commandTask }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadTasks();
}
});
}
openShowScriptDialog(commandTask: any) {
const dialogRef = this.dialog.open(ShowTaskScriptComponent, {
width: '85vw',
height: '85vh',
maxWidth: '85vw',
maxHeight: '85vh',
data: { commandTask: commandTask }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadTasks();
}
});
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
@ -110,5 +188,5 @@ export class CommandsTaskComponent implements OnInit {
themeColor: '#3f51b5'
});
}
}

View File

@ -0,0 +1,128 @@
.dialog-title {
font-weight: bold;
}
.task-form {
padding: 20px;
}
.full-width {
width: 100%;
}
.custom-time {
display: flex;
gap: 15px;
}
.w-half {
width: 50%;
}
mat-form-field {
margin-bottom: 16px;
}
form {
display: flex;
flex-direction: column;
gap: 16px;
margin: auto;
}
mat-form-field {
width: 100%;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
.weekday-toggle-group {
display: flex;
justify-content: space-between;
gap: 8px;
margin-bottom: 16px;
}
.weekday-toggle {
flex: 1;
padding: 8px 0;
border: 1px solid #ccc;
border-radius: 4px;
background: #f5f5f5;
cursor: pointer;
font-weight: 500;
transition: background 0.2s ease;
}
.weekday-toggle.selected {
background: #1976d2;
color: white;
border-color: #1976d2;
}
.month-toggle-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 16px;
}
.month-toggle-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
justify-content: space-between;
}
.month-toggle {
flex: 1;
padding: 8px;
min-width: 48px;
border: 1px solid #ccc;
border-radius: 6px;
background-color: #f0f0f0;
text-align: center;
cursor: pointer;
transition: background-color 0.2s ease;
}
.month-toggle.selected {
background-color: #4caf50;
color: white;
border-color: #4caf50;
}
.summary-card {
background: #f9fafb;
border-left: 5px solid #3f51b5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 16px;
transition: box-shadow 0.3s ease;
}
.summary-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.summary-card {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 12px 16px;
margin-top: 16px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.summary-text {
color: #0d47a1;
line-height: 1.4;
}

View File

@ -0,0 +1,87 @@
<h2 mat-dialog-title class="dialog-title">Programar accion</h2>
<mat-dialog-content class="dialog-content">
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form">
<mat-form-field appearance="fill" class="w-full">
<mat-label>Repetición</mat-label>
<mat-select formControlName="recurrenceType">
<mat-option *ngFor="let type of recurrenceTypes" [value]="type">{{ type | titlecase }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="w-full" *ngIf="form.get('recurrenceType')?.value === 'none'">
<mat-label>Fecha de ejecución</mat-label>
<input matInput [matDatepicker]="oneTimePicker" formControlName="executionDate">
<mat-datepicker-toggle matSuffix [for]="oneTimePicker"></mat-datepicker-toggle>
<mat-datepicker #oneTimePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Hora</mat-label>
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
</mat-form-field>
<!-- Mostrar solo si no es 'none' -->
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
<label>Días de la semana:</label>
<div class="weekday-toggle-group">
<button
*ngFor="let day of weekDays"
type="button"
class="weekday-toggle"
[class.selected]="selectedDays[day]"
(click)="toggleDay(day)">
{{ day.slice(0, 3) }}
</button>
</div>
</div>
<!-- Selección de meses -->
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
<label>Meses:</label>
<div class="month-toggle-row" *ngFor="let row of monthRows">
<button
*ngFor="let month of row"
type="button"
class="month-toggle"
[class.selected]="selectedMonths[month]"
(click)="toggleMonth(month)">
{{ month.slice(0, 3) }}
</button>
</div>
</div>
<!-- Rango de fechas -->
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
<mat-form-field appearance="fill" class="w-half">
<mat-label>Desde</mat-label>
<input matInput [matDatepicker]="fromPicker" formControlName="initDate">
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="fill" class="w-half">
<mat-label>Hasta</mat-label>
<input matInput [matDatepicker]="toPicker" formControlName="endDate">
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
</mat-form-field>
</div>
<mat-checkbox formControlName="enabled">Activar tarea</mat-checkbox>
<mat-card *ngIf="summaryText" class="summary-card">
<mat-icon color="primary" style="width: 50px;">info</mat-icon>
<span class="summary-text">
{{ summaryText }}
</span>
</mat-card>
</form>
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" >{{ 'buttonSave' | translate }}</button>
</mat-dialog-actions>

View File

@ -0,0 +1,100 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateTaskScheduleComponent } from './create-task-schedule.component';
import {LoadingComponent} from "../../../../shared/loading/loading.component";
import {HttpClientTestingModule} from "@angular/common/http/testing";
import {ToastrModule} from "ngx-toastr";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {MatDividerModule} from "@angular/material/divider";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatIconModule} from "@angular/material/icon";
import {MatButtonModule} from "@angular/material/button";
import {MatTableModule} from "@angular/material/table";
import {MatPaginatorModule} from "@angular/material/paginator";
import {MatTooltipModule} from "@angular/material/tooltip";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatSelectModule} from "@angular/material/select";
import {MatTabsModule} from "@angular/material/tabs";
import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {MatListModule} from "@angular/material/list";
import {MatCardModule} from "@angular/material/card";
import {MatMenuModule} from "@angular/material/menu";
import {MatTreeModule} from "@angular/material/tree";
import {TranslateModule, TranslateService} from "@ngx-translate/core";
import {JoyrideModule} from "ngx-joyride";
import {ConfigService} from "@services/config.service";
import {ActivatedRoute} from "@angular/router";
import {MatDatepickerModule} from "@angular/material/datepicker";
import {MatButtonToggleModule} from "@angular/material/button-toggle";
import {
DateAdapter,
MAT_DATE_FORMATS,
MAT_NATIVE_DATE_FORMATS,
MatNativeDateModule,
provideNativeDateAdapter
} from "@angular/material/core";
import {MatCheckboxModule} from "@angular/material/checkbox";
describe('CreateTaskScheduleComponent', () => {
let component: CreateTaskScheduleComponent;
let fixture: ComponentFixture<CreateTaskScheduleComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [CreateTaskScheduleComponent, LoadingComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
BrowserAnimationsModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatPaginatorModule,
MatTooltipModule,
FormsModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
MatDialogModule,
MatSelectModule,
MatTabsModule,
MatAutocompleteModule,
MatListModule,
MatCardModule,
MatMenuModule,
MatTreeModule,
MatDatepickerModule,
MatButtonToggleModule,
MatNativeDateModule,
MatCheckboxModule,
TranslateModule.forRoot(),
JoyrideModule.forRoot(),
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
{ provide: MAT_DATE_FORMATS, useValue: MAT_NATIVE_DATE_FORMATS },
]
}).compileComponents();
fixture = TestBed.createComponent(CreateTaskScheduleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,203 @@
import {Component, Inject, OnInit} from '@angular/core';
import {FormBuilder, FormGroup} from "@angular/forms";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {ConfigService} from "@services/config.service";
@Component({
selector: 'app-create-task-schedule',
templateUrl: './create-task-schedule.component.html',
styleUrl: './create-task-schedule.component.css'
})
export class CreateTaskScheduleComponent implements OnInit{
form: FormGroup;
baseUrl: string;
apiUrl: string;
recurrenceTypes = ['none', 'custom'];
weekDays: string[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
isSingleDateSelected: boolean = true;
monthsList: string[] = [
'january', 'february', 'march', 'april', 'may', 'june',
'july', 'august', 'september', 'october', 'november', 'december'
];
monthRows: string[][] = [];
editing: boolean = false;
selectedMonths: { [key: string]: boolean } = {};
selectedDays: { [key: string]: boolean } = {};
constructor(
private fb: FormBuilder,
public dialogRef: MatDialogRef<CreateTaskScheduleComponent>,
private http: HttpClient,
private toastr: ToastrService,
private configService: ConfigService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/command-task-schedules`;
this.form = this.fb.group({
executionDate: [new Date()],
executionTime: ['08:00'],
recurrenceType: ['none'],
recurrenceDetails: this.fb.group({
daysOfWeek: [[]],
months: this.fb.control([]),
initDate: [null],
endDate: [null]
}),
enabled: [true]
});
if (this.data.schedule) {
this.editing = true;
this.loadData();
}
this.form.get('recurrenceType')?.valueChanges.subscribe((value) => {
if (value === 'none') {
this.form.get('recurrenceDetails')?.disable();
} else {
this.form.get('recurrenceDetails')?.enable();
}
});
}
ngOnInit(): void {
this.monthRows = [
this.monthsList.slice(0, 6),
this.monthsList.slice(6, 12)
];
}
loadData(): void {
this.http.get<any>(`${this.baseUrl}${this.data.schedule['@id']}`).subscribe(
(data) => {
const formattedExecutionTime = this.formatExecutionTime(data.executionTime);
this.form.patchValue({
executionDate: data.executionDate,
executionTime: formattedExecutionTime,
recurrenceType: data.recurrenceType,
recurrenceDetails: {
...data.recurrenceDetails,
initDate: data.recurrenceDetails.initDate || null,
endDate: data.recurrenceDetails.endDate || null,
daysOfWeek: data.recurrenceDetails.daysOfWeek || [],
months: data.recurrenceDetails.months || []
},
enabled: data.enabled
});
this.selectedDays = data.recurrenceDetails.daysOfWeek.reduce((acc: any, day: string) => {
acc[day] = true;
return acc;
}, {});
this.selectedMonths = data.recurrenceDetails.months.reduce((acc: any, month: string) => {
acc[month] = true;
return acc;
}, {});
},
(error) => {
console.error('Error loading schedule data', error);
}
);
}
formatExecutionTime(time: string | Date): string {
const date = (time instanceof Date) ? time : new Date(time);
if (isNaN(date.getTime())) {
console.error('Invalid execution time:', time);
return '';
}
return date.toISOString().substring(11, 16);
}
convertDateToLocalISO(date: Date): string {
date = new Date(date);
const adjustedDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
return adjustedDate.toISOString();
}
onSubmit() {
const formData = this.form.value;
const payload: any = {
commandTask: this.data.task['@id'],
executionDate: formData.recurrenceType === 'none' ? this.convertDateToLocalISO(formData.executionDate) : null,
executionTime: formData.executionTime,
recurrenceType: formData.recurrenceType,
recurrenceDetails: {
...formData.recurrenceDetails,
initDate: formData.recurrenceDetails?.initDate || null,
endDate: formData.recurrenceDetails?.endDate || null,
daysOfWeek: formData.recurrenceDetails?.daysOfWeek || [],
months: formData.recurrenceDetails?.months || []
},
enabled: formData.enabled
}
if (this.editing) {
const taskId = this.data.task.uuid;
this.http.patch<any>(`${this.baseUrl}${this.data.schedule['@id']}`, payload).subscribe({
next: () => {
this.toastr.success('Programacion de tarea actualizada con éxito');
this.dialogRef.close(true);
},
error: () => {
this.toastr.error('Error al actualizar la tarea');
}
});
} else {
this.http.post<any>(this.apiUrl, payload).subscribe({
next: () => {
this.toastr.success('Programacion de tarea creada con éxito');
this.dialogRef.close(true);
},
error: () => {
this.toastr.error('Error al crear la tarea');
}
});
}
}
onCancel(): void {
this.dialogRef.close(false);
}
get summaryText(): string {
const recurrence = this.form.get('recurrenceType')?.value;
const start = this.form.get('recurrenceType')?.value === 'none' ? this.form.get('executionDate')?.value : this.form.get('recurrenceDetails.initDate')?.value;
const end = this.form.get('recurrenceType')?.value === 'none' ? this.form.get('executionDate')?.value : this.form.get('recurrenceDetails.endDate')?.value;
const time = this.form.get('executionTime')?.value;
const days = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
const months = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
if (recurrence === 'none') {
return `Esta acción se ejecutará una sola vez el ${ this.formatDate(start)} a las ${time}.`;
}
return `Esta acción se ejecutará todos los ${days.join(', ')} de ${months.join(', ')}, desde el ${this.formatDate(start)} hasta el ${this.formatDate(end)} a las ${time}.`;
}
formatDate(date: string | Date): string {
const realDate = (date instanceof Date) ? date : new Date(date);
return new Intl.DateTimeFormat('es-ES', { dateStyle: 'long' }).format(realDate);
}
toggleDay(day: string) {
this.selectedDays[day] = !this.selectedDays[day];
const days = Object.keys(this.selectedDays).filter(d => this.selectedDays[d]);
this.form.get('recurrenceDetails.daysOfWeek')?.setValue(days);
}
toggleMonth(month: string) {
this.selectedMonths[month] = !this.selectedMonths[month];
const months = Object.keys(this.selectedMonths).filter(m => this.selectedMonths[m]);
this.form.get('recurrenceDetails.months')?.setValue(months);
}
}

View File

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

View File

@ -0,0 +1,65 @@
<h2 mat-dialog-title class="dialog-title">Añadir acción a: {{ data.task?.name }}</h2>
<mat-dialog-content class="dialog-content">
<div class="task-form">
<div class="toggle-options">
<mat-button-toggle-group [(ngModel)]="commandType" exclusive>
<mat-button-toggle value="new">
<mat-icon>edit</mat-icon> Nuevo Script
</mat-button-toggle>
<mat-button-toggle value="existing">
<mat-icon>storage</mat-icon> Script Guardado
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div *ngIf="commandType === 'new'" class="new-command-container">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecucion </mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Ingrese el script</mat-label>
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
</mat-form-field>
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
</div>
<div *ngIf="commandType === 'existing'">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Seleccione script a ejecutar</mat-label>
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecucion </mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field>
<div class="script-content">
<h3>Script:</h3>
<div class="script-preview" [innerHTML]="scriptContent"></div>
</div>
<div class="script-params" *ngIf="parameterNames.length > 0">
<h3>Ingrese los parámetros:</h3>
<div *ngFor="let paramName of parameterNames">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ paramName }}</mat-label>
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
</mat-form-field>
</div>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" >{{ 'buttonSave' | translate }}</button>
</mat-dialog-actions>

View File

@ -0,0 +1,89 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateTaskScriptComponent } from './create-task-script.component';
import {GroupsComponent} from "../../../groups/groups.component";
import {ExecuteCommandComponent} from "../../main-commands/execute-command/execute-command.component";
import {LoadingComponent} from "../../../../shared/loading/loading.component";
import {HttpClientTestingModule} from "@angular/common/http/testing";
import {ToastrModule} from "ngx-toastr";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {MatDividerModule} from "@angular/material/divider";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatIconModule} from "@angular/material/icon";
import {MatButtonModule} from "@angular/material/button";
import {MatTableModule} from "@angular/material/table";
import {MatPaginatorModule} from "@angular/material/paginator";
import {MatTooltipModule} from "@angular/material/tooltip";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatSelectModule} from "@angular/material/select";
import {MatTabsModule} from "@angular/material/tabs";
import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {MatListModule} from "@angular/material/list";
import {MatCardModule} from "@angular/material/card";
import {MatMenuModule} from "@angular/material/menu";
import {MatTreeModule} from "@angular/material/tree";
import {TranslateModule, TranslateService} from "@ngx-translate/core";
import {JoyrideModule} from "ngx-joyride";
import {ConfigService} from "@services/config.service";
import {ActivatedRoute} from "@angular/router";
import {MatButtonToggleModule} from "@angular/material/button-toggle";
describe('CreateTaskScriptComponent', () => {
let component: CreateTaskScriptComponent;
let fixture: ComponentFixture<CreateTaskScriptComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [CreateTaskScriptComponent, LoadingComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
BrowserAnimationsModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatPaginatorModule,
MatTooltipModule,
FormsModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
MatDialogModule,
MatSelectModule,
MatTabsModule,
MatAutocompleteModule,
MatListModule,
MatCardModule,
MatMenuModule,
MatButtonToggleModule,
MatTreeModule,
TranslateModule.forRoot(),
JoyrideModule.forRoot(),
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
]
}).compileComponents();
fixture = TestBed.createComponent(CreateTaskScriptComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,138 @@
import {Component, EventEmitter, Inject, OnInit, Output} from '@angular/core';
import {SelectionModel} from "@angular/cdk/collections";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {ConfigService} from "@services/config.service";
import {ActivatedRoute, Router} from "@angular/router";
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
import {
SaveScriptComponent
} from "../../../groups/components/client-main-view/run-script-assistant/save-script/save-script.component";
import {FormBuilder, FormGroup} from "@angular/forms";
@Component({
selector: 'app-create-task-script',
templateUrl: './create-task-script.component.html',
styleUrl: './create-task-script.component.css'
})
export class CreateTaskScriptComponent implements OnInit {
form: FormGroup;
baseUrl: string;
@Output() dataChange = new EventEmitter<any>();
errorMessage = '';
loading: boolean = false;
scripts: any[] = [];
scriptContent: string = "";
parameters: any = {};
commandType: string = 'existing';
selectedScript: any = null;
newScript: string = '';
executionOrder: Number = 0;
selection = new SelectionModel(true, []);
parameterNames: string[] = Object.keys(this.parameters);
constructor(
private fb: FormBuilder,
private http: HttpClient,
public dialogRef: MatDialogRef<CreateTaskScriptComponent>,
private toastService: ToastrService,
private configService: ConfigService,
private router: Router,
private dialog: MatDialog,
private route: ActivatedRoute,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.baseUrl = this.configService.apiUrl;
this.loadScripts()
this.form = this.fb.group({
content: [''],
order: [''],
})
}
ngOnInit(): void {
}
loadScripts(): void {
this.loading = true;
this.http.get(`${this.baseUrl}/commands?readOnly=false&enabled=true`).subscribe((data: any) => {
this.scripts = data['hydra:member'];
this.loading = false;
}, (error) => {
this.toastService.error(error.error['hydra:description']);
this.loading = false;
});
}
saveNewScript() {
if (!this.newScript.trim()) {
this.toastService.error('Debe ingresar un script antes de guardar.');
return;
}
const dialogRef = this.dialog.open(SaveScriptComponent, {
width: '400px',
data: this.newScript
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Script guardado correctamente');
}
});
}
onScriptChange() {
if (this.selectedScript) {
this.scriptContent = this.selectedScript.script;
const matches = this.scriptContent.match(/@(\w+)/g) || [];
const uniqueParams = Array.from(new Set(matches.map(m => m.slice(1))));
this.parameters = {};
uniqueParams.forEach(param => this.parameters[param] = '');
this.parameterNames = uniqueParams;
this.updateScript();
}
}
onParamChange(name: string, value: string): void {
this.parameters[name] = value;
this.updateScript();
}
updateScript(): void {
let updatedScript = this.selectedScript.script;
for (const [key, value] of Object.entries(this.parameters)) {
const regex = new RegExp(`@${key}\\b`, 'g');
updatedScript = updatedScript.replace(regex, value || `@${key}`);
}
this.scriptContent = updatedScript;
}
onSubmit() {
this.http.post(`${this.baseUrl}/command-task-scripts`, {
commandTask: this.data.task['@id'],
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
order: this.executionOrder,
type: 'run-script',
}).subscribe({
next: () => {
this.toastService.success('Tarea creada con éxito');
this.dialogRef.close(true);
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
}
})
}
onCancel(): void {
this.dialogRef.close(false);
}
}

View File

@ -6,23 +6,60 @@
padding: 20px;
}
.select-task {
padding: 20px;
margin-bottom: 16px;
}
.full-width {
width: 100%;
}
.button-container {
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding: 1.5rem;
}
mat-form-field {
margin-bottom: 16px;
}
.loading-spinner {
display: block;
margin: 0 auto;
align-items: center;
justify-content: center;
}
.section-title {
margin-top: 24px;
margin-bottom: 8px;
font-weight: 500;
}
.summary-section {
background-color: #f9f9f9;
border-bottom: 1px solid #ddd;
margin-bottom: 10px;
}
.summary-block {
margin-top: 10px;
}
.date-time-row {
display: flex;
gap: 16px;
margin-top: 12px;
}
.half-width {
flex: 1;
min-width: 0;
}
.full-width {
width: 100%;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}

View File

@ -1,106 +1,83 @@
<h2 mat-dialog-title class="dialog-title">{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}</h2>
<h2 mat-dialog-title class="dialog-title">
{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}
</h2>
<form [formGroup]="taskForm" class="task-form">
<mat-dialog-content>
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<h3 class="section-title">Información</h3>
<mat-divider></mat-divider>
<!-- Toggle entre crear o añadir -->
<mat-radio-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode">
<mat-radio-button value="create">Crear tarea</mat-radio-button>
<mat-radio-button value="add">Introducir en tarea existente</mat-radio-button>
</mat-radio-group>
<!-- Selección de tarea existente -->
<div *ngIf="taskMode === 'add'" class="select-task">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Información</mat-label>
<textarea matInput formControlName="notes" placeholder="Ingresa tus notas aquí"></textarea>
<mat-label>Seleccione una tarea</mat-label>
<mat-select [(ngModel)]="selectedExistingTask" name="existingTask">
<mat-option *ngFor="let task of existingTasks" [value]="task">{{ task.name }}</mat-option>
</mat-select>
</mat-form-field>
<h3 class="section-title">{{ 'informationSectionTitle' | translate }}</h3>
<mat-divider></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'informationLabel' | translate }}</mat-label>
<mat-label>Orden de ejecución</mat-label>
<input
matInput
type="number"
[(ngModel)]="executionOrder"
name="executionOrder"
min="1"
placeholder="Introduce el orden"
>
</mat-form-field>
</div>
<!-- Formulario de nueva tarea -->
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
<input matInput formControlName="name" placeholder="{{ 'nameLabel' | translate }}">
<mat-error *ngIf="taskForm.get('name')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'notesLabel' | translate }}</mat-label>
<textarea matInput formControlName="notes" placeholder="{{ 'notesPlaceholder' | translate }}"></textarea>
</mat-form-field>
<h3 class="section-title">{{ 'commandSelectionSectionTitle' | translate }}</h3>
<mat-divider></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'selectCommandsLabel' | translate }}</mat-label>
<mat-select formControlName="commandGroup" (selectionChange)="onCommandGroupChange()">
<mat-option *ngFor="let group of availableCommandGroups" [value]="group.uuid">
{{ group.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="taskForm.get('commandGroup')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'selectIndividualCommandsLabel' | translate }}</mat-label>
<mat-select formControlName="extraCommands" multiple>
<mat-option *ngFor="let command of availableIndividualCommands" [value]="command.uuid">
{{ command.name }}
</mat-option>
<mat-label>Ámbito</mat-label>
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)">
<mat-option value="organizational-unit">Unidad Organizativa</mat-option>
<mat-option value="classrooms-group">Grupo de aulas</mat-option>
<mat-option value="classroom">Aulas</mat-option>
<mat-option value="clients-group">Grupos de clientes</mat-option>
</mat-select>
</mat-form-field>
<h3 class="section-title">{{ 'executionDateTimeSectionTitle' | translate }}</h3>
<mat-divider></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'executionDateLabel' | translate }}</mat-label>
<input matInput [matDatepicker]="picker" formControlName="date" placeholder="{{ 'selectDatePlaceholder' | translate }}">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
<mat-error *ngIf="taskForm.get('date')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'executionTimeLabel' | translate }}</mat-label>
<input matInput type="time" formControlName="time" placeholder="{{ 'selectTimePlaceholder' | translate }}">
<mat-error *ngIf="taskForm.get('time')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
</mat-form-field>
<h3 class="section-title">{{ 'destinationSelectionSectionTitle' | translate }}</h3>
<mat-divider></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'selectOrganizationalUnitLabel' | translate }}</mat-label>
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
<mat-select formControlName="organizationalUnit">
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
{{ unit.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'selectClassroomLabel' | translate }}</mat-label>
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
<mat-option *ngFor="let child of selectedUnitChildren" [value]="child['@id']">
{{ child.name }}
<div class="unit-name">{{ unit.name }}</div>
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'selectClientsLabel' | translate }}</mat-label>
<mat-select formControlName="selectedClients" multiple>
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
{{ 'selectAllClients' | translate }}
</mat-option>
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
{{ client.name }} ({{ client.ip }})
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate">
¿Quieres programar la tarea al finalizar su creación?
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Selecciona Clientes</mat-label>
<mat-select formControlName="selectedClients" multiple>
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
Seleccionar todos
</mat-option>
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
{{ client.name }} ({{ client.ip }})
</mat-option>
</mat-select>
</mat-form-field>
</mat-dialog-content>
</form>
<div class="button-container">
<button class="submit-button" (click)="saveTask()">{{ 'buttonSave' | translate }}</button>
</div>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
<button
class="submit-button"
(click)="taskMode === 'create' ? saveTask() : addToExistingTask()"
>
{{ 'buttonSave' | translate }}
</button>
</mat-dialog-actions>

View File

@ -1,8 +1,12 @@
import { Component, OnInit, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ConfigService } from '@services/config.service';
import {of} from "rxjs";
import {startWith, switchMap} from "rxjs/operators";
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
@Component({
selector: 'app-create-task',
@ -10,176 +14,193 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
styleUrls: ['./create-task.component.css']
})
export class CreateTaskComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
baseUrl: string;
taskForm: FormGroup;
availableCommandGroups: any[] = [];
selectedGroupCommands: any[] = [];
availableIndividualCommands: any[] = [];
apiUrl = `${this.baseUrl}/command-tasks`;
apiUrl: string;
editing: boolean = false;
availableOrganizationalUnits: any[] = [];
selectedUnitChildren: any[] = [];
selectedClients: any[] = [];
selectedClientIds: Set<string> = new Set();
clients: any[] = [];
allOrganizationalUnits: any[] = [];
loading: boolean = false;
taskMode: 'create' | 'add' = 'create';
existingTasks: any[] = [];
selectedExistingTask: string | null = null;
executionOrder: number | null = null;
constructor(
private fb: FormBuilder,
private http: HttpClient,
private configService: ConfigService,
private toastr: ToastrService,
private dialog: MatDialog,
public dialogRef: MatDialogRef<CreateTaskComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/command-tasks`;
this.taskForm = this.fb.group({
commandGroup: ['', Validators.required],
extraCommands: [[]],
date: ['', Validators.required],
time: ['', Validators.required],
scope: [ this.data?.scope ? this.data.scope : '', Validators.required],
name: ['', Validators.required],
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null, Validators.required],
notes: [''],
organizationalUnit: ['', Validators.required],
selectedChild: [''],
selectedClients: [[]]
scheduleAfterCreate: [false]
});
}
ngOnInit(): void {
this.loadCommandGroups();
this.loadIndividualCommands();
this.loadOrganizationalUnits();
if (this.data && this.data.task) {
this.editing = true;
this.loadTaskData(this.data.task);
}
this.loading = true;
const observables = [
this.loadCommandGroups(),
this.loadIndividualCommands(),
this.loadOrganizationalUnits(),
this.startUnitsFilter(),
this.loadTasks()
];
Promise.all(observables).then(() => {
if (this.data.task) {
this.editing = true;
this.loadData().then(() => {
this.loading = false;
})
} else {
this.loading = false;
}
}).catch(() => {
this.loading = false;
})
}
loadCommandGroups(): void {
loadData(): Promise<void> {
return new Promise((resolve, reject) => {
this.http.get<any>(`${this.baseUrl}${this.data.task['@id']}`).subscribe(
(data) => {
this.taskForm.patchValue({
name: data.name,
scope: data.scope,
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
notes: data.notes,
});
resolve();
},
(error) => {
this.toastr.error('Error al cargar los datos de la tarea');
reject(error);
}
);
})
}
loadTasks(): Promise<void> {
return new Promise((resolve, reject) => {
this.http.get<any>(`${this.apiUrl}?page=1&itemsPerPage=100`).subscribe(
(data) => {
this.existingTasks = data['hydra:member'];
resolve();
},
(error) => {
this.toastr.error('Error al cargar las tareas existentes');
reject(error);
}
);
});
}
onScopeChange(scope: string): void {
this.filterUnits(scope).subscribe(filteredUnits => {
this.availableOrganizationalUnits = filteredUnits;
this.taskForm.get('organizationalUnit')?.setValue('');
});
}
startUnitsFilter(): Promise<void> {
return new Promise((resolve, reject) => {
this.taskForm.get('scope')?.valueChanges.pipe(
startWith(this.taskForm.get('scope')?.value),
switchMap((value) => this.filterUnits(value))
).subscribe(filteredUnits => {
this.availableOrganizationalUnits = filteredUnits;
resolve();
}, error => {
this.toastr.error('Error al filtrar las unidades organizacionales');
reject(error);
});
})
}
filterUnits(value: string) {
const filtered = this.allOrganizationalUnits.filter(unit => unit.type === value);
return of(filtered);
}
loadCommandGroups(): Promise<void> {
return new Promise((resolve, reject) => {
this.http.get<any>(`${this.baseUrl}/command-groups`).subscribe(
(data) => {
this.availableCommandGroups = data['hydra:member'];
resolve();
},
(error) => {
this.toastr.error('Error al cargar los grupos de comandos');
}
);
}
loadIndividualCommands(): void {
this.http.get<any>(`${this.baseUrl}/commands`).subscribe(
(data) => {
this.availableIndividualCommands = data['hydra:member'];
},
(error) => {
this.toastr.error('Error al cargar los comandos individuales');
}
);
}
loadOrganizationalUnits(): void {
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
(data) => {
this.availableOrganizationalUnits = data['hydra:member'].filter((unit: any) => unit['type'] === 'organizational-unit');
},
(error) => {
this.toastr.error('Error al cargar las unidades organizacionales');
}
);
}
loadTaskData(task: any): void {
this.taskForm.patchValue({
commandGroup: task.commandGroup ? task.commandGroup['@id'] : '',
extraCommands: task.commands ? task.commands.map((cmd: any) => cmd['@id']) : [],
date: task.dateTime ? task.dateTime.split('T')[0] : '',
time: task.dateTime ? task.dateTime.split('T')[1].slice(0, 5) : '',
notes: task.notes || '',
organizationalUnit: task.organizationalUnit ? task.organizationalUnit['@id'] : ''
reject(error);
});
});
if (task.commandGroup) {
this.selectedGroupCommands = task.commandGroup.commands;
}
}
private collectClassrooms(unit: any): any[] {
let classrooms = [];
if (unit.type === 'classroom') {
classrooms.push(unit);
}
if (unit.children && unit.children.length > 0) {
for (let child of unit.children) {
classrooms = classrooms.concat(this.collectClassrooms(child));
}
}
return classrooms;
loadIndividualCommands(): Promise<void> {
return new Promise((resolve, reject) => {
this.http.get<any>(`${this.baseUrl}/commands`).subscribe(
(data) => {
this.availableIndividualCommands = data['hydra:member'];
resolve();
},
(error) => {
this.toastr.error('Error al cargar los comandos individuales');
reject(error);
});
});
}
onOrganizationalUnitChange(): void {
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
if (selectedUnit) {
this.selectedUnitChildren = this.collectClassrooms(selectedUnit);
} else {
this.selectedUnitChildren = [];
}
this.taskForm.patchValue({ selectedChild: '', selectedClients: [] });
this.selectedClients = [];
this.selectedClientIds.clear();
loadOrganizationalUnits(): Promise<void> {
return new Promise((resolve, reject) => {
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=100`).subscribe(
(data) => {
this.allOrganizationalUnits = data['hydra:member'];
this.availableOrganizationalUnits = [...this.allOrganizationalUnits];
resolve();
},
(error) => {
this.toastr.error('Error al cargar las unidades organizacionales');
reject(error);
}
);
});
}
onChildChange(): void {
const selectedChildId = this.taskForm.get('selectedChild')?.value;
if (!selectedChildId) {
this.selectedClients = [];
addToExistingTask() {
if (!this.selectedExistingTask) {
this.toastr.error('Debes seleccionar una tarea existente.');
return;
}
const url = `${this.baseUrl}${selectedChildId}`.replace(/([^:]\/)\/+/g, '$1');
this.http.get<any>(url).subscribe(
(data) => {
if (Array.isArray(data.clients) && data.clients.length > 0) {
this.selectedClients = data.clients;
} else {
this.selectedClients = [];
this.toastr.warning('El aula seleccionada no tiene clientes.');
}
this.taskForm.patchValue({ selectedClients: [] });
this.selectedClientIds.clear();
},
(error) => {
this.toastr.error('Error al cargar los detalles del aula seleccionada');
}
);
}
toggleSelectAll() {
const allSelected = this.areAllSelected();
if (allSelected) {
this.selectedClientIds.clear();
} else {
this.selectedClients.forEach(client => this.selectedClientIds.add(client.uuid));
if (this.executionOrder == null || this.executionOrder < 1) {
this.toastr.error('Debes introducir un orden de ejecución válido (mayor que 0).');
return;
}
this.taskForm.get('selectedClients')!.setValue(Array.from(this.selectedClientIds));
const data = {
taskId: this.selectedExistingTask,
executionOrder: this.executionOrder
};
this.toastr.success('Tarea actualizada con éxito');
this.dialogRef.close(data);
}
areAllSelected(): boolean {
return this.selectedClients.length > 0 && this.selectedClients.every(client => this.selectedClientIds.has(client.uuid));
}
onCommandGroupChange(): void {
const selectedGroupId = this.taskForm.get('commandGroup')?.value;
this.http.get<any>(`${this.baseUrl}/command-groups/${selectedGroupId}`).subscribe(
(data) => {
this.selectedGroupCommands = data.commands;
},
(error) => {
this.toastr.error('Error al cargar los comandos del grupo seleccionado');
}
);
}
saveTask(): void {
if (this.taskForm.invalid) {
@ -188,22 +209,14 @@ export class CreateTaskComponent implements OnInit {
}
const formData = this.taskForm.value;
const dateTime = this.combineDateAndTime(formData.date, formData.time);
const selectedCommands = formData.extraCommands && formData.extraCommands.length > 0
? formData.extraCommands.map((id: any) => `/commands/${id}`)
: null;
const payload: any = {
commandGroups: formData.commandGroup ? [`/command-groups/${formData.commandGroup}`] : null,
dateTime: dateTime,
name: formData.name,
scope: formData.scope,
organizationalUnit: formData.organizationalUnit,
notes: formData.notes || '',
clients: Array.from(this.selectedClientIds).map((uuid: string) => `/clients/${uuid}`),
};
if (selectedCommands) {
payload.commands = selectedCommands;
}
if (this.editing) {
const taskId = this.data.task.uuid;
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
@ -217,9 +230,21 @@ export class CreateTaskComponent implements OnInit {
});
} else {
this.http.post<any>(this.apiUrl, payload).subscribe({
next: () => {
next: response => {
this.toastr.success('Tarea creada con éxito');
this.dialogRef.close(true);
this.dialogRef.close(response);
if (formData.scheduleAfterCreate) {
const dialogRef = this.dialog.open(CreateTaskScheduleComponent, {
width: '800px',
data: { task: response }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastr.success('Tarea programada correctamente');
}
});
}
},
error: () => {
this.toastr.error('Error al crear la tarea');
@ -228,14 +253,7 @@ export class CreateTaskComponent implements OnInit {
}
}
combineDateAndTime(date: string, time: string): string {
const dateObj = new Date(date);
const [hours, minutes] = time.split(':').map(Number);
dateObj.setHours(hours, minutes, 0);
return dateObj.toISOString();
}
close(): void {
this.dialogRef.close();
this.dialogRef.close(false);
}
}

View File

@ -0,0 +1,89 @@
.full-width {
width: 100%;
}
form {
padding: 20px;
}
.spacing-container {
margin-top: 20px;
margin-bottom: 16px;
}
.list-item-content {
display: flex;
align-items: flex-start;
justify-content: space-between;
width: 100%;
}
.text-content {
flex-grow: 1;
margin-right: 16px;
margin-left: 10px;
}
.icon-container {
display: flex;
align-items: center;
}
.right-icon {
margin-left: 8px;
cursor: pointer;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
.lists-container {
padding: 16px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin: 1.5rem 0rem 1.5rem 0rem;
box-sizing: border-box;
}
.search-string {
flex: 1;
padding: 5px;
}
.search-select {
flex: 1;
padding: 5px;
}
table {
width: 100%;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}
mat-spinner {
margin: 0 auto;
align-self: center;
}
.subnets-button-row {
display: flex;
gap: 15px;
}

View File

@ -0,0 +1,100 @@
<app-loading [isLoading]="loading"></app-loading>
<h2 mat-dialog-title>Gestionar programaciones de tareas en {{ data.commandTask?.name }}</h2>
<mat-dialog-content>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
text="Busca subredes por nombre para localizar una subred específica rápidamente.">
<mat-label i18n="@@searchLabel">Buscar nombre del cliente</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder"
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear tree search"
(click)="filters['name'] = ''; loadData()">
<mat-icon>close</mat-icon>
</button>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchIpStep" text="Busca programaciones por tipo.">
<mat-label i18n="@@searchLabel">Buscar por tipo</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['recurrence']" i18n-placeholder="@@searchPlaceholder"
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<button *ngIf="filters['ip']" mat-icon-button matSuffix aria-label="Clear tree search"
(click)="filters['ip'] = ''; loadData()">
<mat-icon>close</mat-icon>
</button>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<app-loading [isLoading]="loading"></app-loading>
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
text="Visualiza y administra las subredes listadas según los filtros aplicados.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let schedule">
<ng-container *ngIf="column.columnDef === 'recurrenceType'">
<mat-chip style="padding: 10px; margin: 5px;">
<ng-container *ngIf="column.cell(schedule) === 'none'; else scheduledTemplate">
No programado
<div style="font-size: 12px;">
{{ schedule.executionDate | date }}
</div>
</ng-container>
<ng-template #scheduledTemplate>
Programado
<div style="font-size: 12px;">
{{ schedule.recurrenceDetails.initDate | date }} → {{ schedule.recurrenceDetails.endDate | date}}
</div>
</ng-template>
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef === 'executionTime'">
{{ schedule.executionTime | date: 'HH:mm' }}
</ng-container>
<ng-container *ngIf="column.columnDef !== 'recurrenceType' && column.columnDef !== 'executionTime' && column.columnDef !== 'enabled'">
{{ column.cell(schedule) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'enabled'">
<mat-chip>
<ng-container *ngIf="schedule.enabled">
Activo
</ng-container>
<ng-container *ngIf="!schedule.enabled">
Inactivo
</ng-container>
</mat-chip>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editSchedule(schedule)">
<mat-icon i18n="@@deleteElementTooltip">edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteSchedule(schedule)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container" joyrideStep="paginationStep"
text="Navega entre las páginas de subredes usando el paginador.">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()">Cerrar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,86 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ShowTaskScheduleComponent } from './show-task-schedule.component';
import {LoadingComponent} from "../../../../shared/loading/loading.component";
import {HttpClientTestingModule} from "@angular/common/http/testing";
import {ToastrModule} from "ngx-toastr";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {MatDividerModule} from "@angular/material/divider";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatIconModule} from "@angular/material/icon";
import {MatButtonModule} from "@angular/material/button";
import {MatTableModule} from "@angular/material/table";
import {MatPaginatorModule} from "@angular/material/paginator";
import {MatTooltipModule} from "@angular/material/tooltip";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatSelectModule} from "@angular/material/select";
import {MatTabsModule} from "@angular/material/tabs";
import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {MatListModule} from "@angular/material/list";
import {MatCardModule} from "@angular/material/card";
import {MatMenuModule} from "@angular/material/menu";
import {MatTreeModule} from "@angular/material/tree";
import {TranslateModule} from "@ngx-translate/core";
import {JoyrideModule} from "ngx-joyride";
import {ConfigService} from "@services/config.service";
import {ActivatedRoute} from "@angular/router";
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
describe('ShowTaskScheduleComponent', () => {
let component: ShowTaskScheduleComponent;
let fixture: ComponentFixture<ShowTaskScheduleComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [ShowTaskScheduleComponent, LoadingComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
BrowserAnimationsModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatPaginatorModule,
MatTooltipModule,
FormsModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
MatDialogModule,
MatSelectModule,
MatTabsModule,
MatAutocompleteModule,
MatListModule,
MatCardModule,
MatMenuModule,
MatTreeModule,
TranslateModule.forRoot(),
JoyrideModule.forRoot(),
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
]
}).compileComponents();
fixture = TestBed.createComponent(ShowTaskScheduleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,107 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {Client} from "../../../groups/model/model";
import {ToastrService} from "ngx-toastr";
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
import {ConfigService} from "@services/config.service";
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
import {DatePipe} from "@angular/common";
@Component({
selector: 'app-show-task-schedule',
templateUrl: './show-task-schedule.component.html',
styleUrl: './show-task-schedule.component.css'
})
export class ShowTaskScheduleComponent implements OnInit{
baseUrl: string;
dataSource = new MatTableDataSource<any>([]);
length = 0;
itemsPerPage: number = 10;
pageSizeOptions: number[] = [5, 10, 20];
page = 0;
loading: boolean = false;
filters: { [key: string]: string } = {};
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
{ columnDef: 'recurrenceType', header: 'Recurrencia', cell: (schedule: any) => schedule.recurrenceType },
{ columnDef: 'time', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm', 'UTC') },
{ columnDef: 'daysOfWeek', header: 'Dias de la semana', cell: (schedule: any) => schedule.recurrenceDetails.daysOfWeek },
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails.months },
{ columnDef: 'enabled', header: 'Activo', cell: (schedule: any) => schedule.enabled }
];
displayedColumns: string[] = ['id', 'recurrenceType', 'time', 'daysOfWeek', 'months', 'enabled', 'actions'];
constructor(
private toastService: ToastrService,
private http: HttpClient,
public dialogRef: MatDialogRef<ShowTaskScheduleComponent>,
public dialog: MatDialog,
private configService: ConfigService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
if (this.data) {
this.loadData();
}
}
loadData() {
this.loading = true;
this.http.get<any>(`${this.baseUrl}/command-task-schedules?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}`, { params: this.filters }).subscribe(
(data) => {
this.dataSource.data = data['hydra:member'];
this.length = data['hydra:totalItems'];
this.loading = false;
},
(error) => {
this.loading = false;
}
);
}
editSchedule(schedule: any): void {
this.dialog.open(CreateTaskScheduleComponent, {
width: '800px',
data: { schedule: schedule, task: this.data.commandTask }
}).afterClosed().subscribe(() => this.loadData());
}
deleteSchedule(schedule: any): void {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: 'tarea programada' }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
() => {
this.toastService.success('Programación eliminada correctamente');
this.loadData();
},
(error) => {
this.toastService.error(error.error['hydra:description']);
}
);
}
})
}
onNoClick(): void {
this.dialogRef.close(false);
}
onPageChange(event: any) {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.loadData()
}
}

View File

@ -0,0 +1,89 @@
.full-width {
width: 100%;
}
form {
padding: 20px;
}
.spacing-container {
margin-top: 20px;
margin-bottom: 16px;
}
.list-item-content {
display: flex;
align-items: flex-start;
justify-content: space-between;
width: 100%;
}
.text-content {
flex-grow: 1;
margin-right: 16px;
margin-left: 10px;
}
.icon-container {
display: flex;
align-items: center;
}
.right-icon {
margin-left: 8px;
cursor: pointer;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
.lists-container {
padding: 16px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin: 1.5rem 0rem 1.5rem 0rem;
box-sizing: border-box;
}
.search-string {
flex: 1;
padding: 5px;
}
.search-select {
flex: 1;
padding: 5px;
}
table {
width: 100%;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}
mat-spinner {
margin: 0 auto;
align-self: center;
}
.subnets-button-row {
display: flex;
gap: 15px;
}

View File

@ -0,0 +1,70 @@
<app-loading [isLoading]="loading"></app-loading>
<h2 mat-dialog-title>Gestionar scripts de tareas en {{ data.commandTask?.name }}</h2>
<mat-dialog-content>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
text="Busca subredes por nombre para localizar una subred específica rápidamente.">
<mat-label i18n="@@searchLabel">Buscar contenido de script</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder"
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear tree search"
(click)="filters['name'] = ''; loadData()">
<mat-icon>close</mat-icon>
</button>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<app-loading [isLoading]="loading"></app-loading>
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
text="Visualiza y administra las subredes listadas según los filtros aplicados.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let schedule">
<ng-container *ngIf="column.columnDef === 'content'; else checkOtherColumn">
<div style="background-color: #f5f5f5; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; white-space: pre-wrap; font-size: 13px;">
{{ column.cell(schedule) }}
</div>
</ng-container>
<ng-template #checkOtherColumn>
<ng-container *ngIf="column.columnDef === 'parameters'; else normalCell">
<button mat-stroked-button color="primary" (click)="openParametersModal(schedule.parameters)">
Ver parámetros
</button>
</ng-container>
</ng-template>
<ng-template #normalCell>
{{ column.cell(schedule) }}
</ng-template>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
<button mat-icon-button color="warn" (click)="deleteTaskScript(schedule)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container" joyrideStep="paginationStep"
text="Navega entre las páginas de subredes usando el paginador.">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()">Cerrar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,86 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ShowTaskScriptComponent } from './show-task-script.component';
import {LoadingComponent} from "../../../../shared/loading/loading.component";
import {HttpClientTestingModule} from "@angular/common/http/testing";
import {ToastrModule} from "ngx-toastr";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {MatDividerModule} from "@angular/material/divider";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatIconModule} from "@angular/material/icon";
import {MatButtonModule} from "@angular/material/button";
import {MatTableModule} from "@angular/material/table";
import {MatPaginatorModule} from "@angular/material/paginator";
import {MatTooltipModule} from "@angular/material/tooltip";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatSelectModule} from "@angular/material/select";
import {MatTabsModule} from "@angular/material/tabs";
import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {MatListModule} from "@angular/material/list";
import {MatCardModule} from "@angular/material/card";
import {MatMenuModule} from "@angular/material/menu";
import {MatTreeModule} from "@angular/material/tree";
import {TranslateModule} from "@ngx-translate/core";
import {JoyrideModule} from "ngx-joyride";
import {ConfigService} from "@services/config.service";
import {ActivatedRoute} from "@angular/router";
import {ShowTaskScheduleComponent} from "../show-task-schedule/show-task-schedule.component";
describe('ShowTaskScriptComponent', () => {
let component: ShowTaskScriptComponent;
let fixture: ComponentFixture<ShowTaskScriptComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [ShowTaskScriptComponent, LoadingComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
BrowserAnimationsModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatPaginatorModule,
MatTooltipModule,
FormsModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
MatDialogModule,
MatSelectModule,
MatTabsModule,
MatAutocompleteModule,
MatListModule,
MatCardModule,
MatMenuModule,
MatTreeModule,
TranslateModule.forRoot(),
JoyrideModule.forRoot(),
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
]
}).compileComponents();
fixture = TestBed.createComponent(ShowTaskScriptComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,104 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {ToastrService} from "ngx-toastr";
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
import {ConfigService} from "@services/config.service";
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
import {ViewParametersModalComponent} from "./view-parameters-modal/view-parameters-modal.component";
@Component({
selector: 'app-show-task-script',
templateUrl: './show-task-script.component.html',
styleUrl: './show-task-script.component.css'
})
export class ShowTaskScriptComponent implements OnInit{
baseUrl: string;
dataSource = new MatTableDataSource<any>([]);
length = 0;
itemsPerPage: number = 10;
pageSizeOptions: number[] = [5, 10, 20];
page = 0;
loading: boolean = false;
filters: { [key: string]: string } = {};
columns = [
{ columnDef: 'id', header: 'ID', cell: (client: any) => client.id },
{ columnDef: 'order', header: 'Orden', cell: (client: any) => client.order },
{ columnDef: 'content', header: 'Script', cell: (client: any) => client.content },
{ columnDef: 'type', header: 'Type', cell: (client: any) => client.type },
{ columnDef: 'parameters', header: 'Parameters', cell: (client: any) => client.parameters },
];
displayedColumns: string[] = ['id', 'order', 'type', 'parameters', 'content', 'actions'];
constructor(
private toastService: ToastrService,
private http: HttpClient,
public dialogRef: MatDialogRef<ShowTaskScriptComponent>,
public dialog: MatDialog,
private configService: ConfigService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
if (this.data) {
this.loadData();
}
}
loadData() {
this.loading = true;
this.http.get<any>(`${this.baseUrl}/command-task-scripts?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}`, { params: this.filters }).subscribe(
(data) => {
this.dataSource.data = data['hydra:member'];
this.length = data['hydra:totalItems'];
this.loading = false;
},
(error) => {
this.loading = false;
}
);
}
deleteTaskScript(schedule: any): void {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: 'script de una tarea' }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
() => {
this.toastService.success('Eliminado correctamente');
this.loadData();
},
(error) => {
this.toastService.error(error.error['hydra:description']);
}
);
}
})
}
onNoClick(): void {
this.dialogRef.close(false);
}
openParametersModal(parameters: any): void {
this.dialog.open(ViewParametersModalComponent, {
width: '900px',
data: parameters
});
}
onPageChange(event: any) {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.loadData()
}
}

View File

@ -0,0 +1,7 @@
<h2 mat-dialog-title>Parámetros</h2>
<mat-dialog-content>
<pre>{{ data | json }}</pre>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">Cerrar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,69 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ViewParametersModalComponent } from './view-parameters-modal.component';
import {LoadingComponent} from "../../../../../shared/loading/loading.component";
import {HttpClientTestingModule, provideHttpClientTesting} from "@angular/common/http/testing";
import {ToastrModule, ToastrService} from "ngx-toastr";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {MatDividerModule} from "@angular/material/divider";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatIconModule} from "@angular/material/icon";
import {MatButtonModule} from "@angular/material/button";
import {MatTableModule} from "@angular/material/table";
import {MatPaginatorModule} from "@angular/material/paginator";
import {MatTooltipModule} from "@angular/material/tooltip";
import {FormBuilder, FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatSelectModule} from "@angular/material/select";
import {MatTabsModule} from "@angular/material/tabs";
import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {MatListModule} from "@angular/material/list";
import {MatCardModule} from "@angular/material/card";
import {MatMenuModule} from "@angular/material/menu";
import {MatTreeModule} from "@angular/material/tree";
import {TranslateModule} from "@ngx-translate/core";
import {JoyrideModule} from "ngx-joyride";
import {ConfigService} from "@services/config.service";
import {ActivatedRoute} from "@angular/router";
import {InputDialogComponent} from "../../../../task-logs/input-dialog/input-dialog.component";
import {provideHttpClient} from "@angular/common/http";
describe('ViewParametersModalComponent', () => {
let component: ViewParametersModalComponent;
let fixture: ComponentFixture<ViewParametersModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ViewParametersModalComponent],
imports: [
MatDialogModule,
TranslateModule.forRoot(),
],
providers: [
FormBuilder,
ToastrService,
provideHttpClient(),
provideHttpClientTesting(),
{
provide: MatDialogRef,
useValue: {}
},
{
provide: MAT_DIALOG_DATA,
useValue: {}
}
]
})
.compileComponents();
fixture = TestBed.createComponent(ViewParametersModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
@Component({
selector: 'app-view-parameters-modal',
templateUrl: './view-parameters-modal.component.html',
styleUrl: './view-parameters-modal.component.css'
})
export class ViewParametersModalComponent {
constructor(
public dialogRef: MatDialogRef<ViewParametersModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) {}
close(): void {
this.dialogRef.close();
}
}

View File

@ -1,119 +0,0 @@
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<div class="header-container-title">
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' |
translate }}</h2>
</div>
<div class="images-button-row">
<button class="action-button" (click)="resetFilters()" joyrideStep="resetFiltersStep"
text="{{ 'resetFiltersStepText' | translate }}">
{{ 'resetFilters' | translate }}
</button>
</div>
</div>
<div class="search-container">
<mat-form-field appearance="fill" class="search-select" joyrideStep="clientSelectStep"
text="{{ 'clientSelectStepText' | translate }}">
<input type="text" matInput [formControl]="clientControl" [matAutocomplete]="clientAuto"
placeholder="{{ 'filterClientPlaceholder' | translate }}">
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient"
(optionSelected)="onOptionClientSelected($event.option.value)">
<mat-option *ngFor="let client of filteredClients | async" [value]="client">
{{ client.name }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field appearance="fill" class="search-select" joyrideStep="commandSelectStep"
text="{{ 'commandSelectStepText' | translate }}">
<input type="text" matInput [formControl]="commandControl" [matAutocomplete]="commandAuto"
placeholder="{{ 'filterCommandPlaceholder' | translate }}">
<mat-autocomplete #commandAuto="matAutocomplete" [displayWith]="displayFnCommand"
(optionSelected)="onOptionCommandSelected($event.option.value)">
<mat-option *ngFor="let command of filteredCommands | async" [value]="command">
{{ command.name }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field appearance="fill" class="search-boolean">
<mat-label i18n="@@searchLabel">Estado</mat-label>
<mat-select [(ngModel)]="filters['status']" (selectionChange)="loadTraces()" placeholder="Seleccionar opción">
<mat-option [value]="undefined">Todos</mat-option>
<mat-option [value]="'failed'">Fallido</mat-option>
<mat-option [value]="'pending'">Pendiente de ejecutar</mat-option>
<mat-option [value]="'in-progress'">Ejecutando</mat-option>
<mat-option [value]="'success'">Completado con éxito</mat-option>
</mat-select>
</mat-form-field>
</div>
<app-loading [isLoading]="loading"></app-loading>
<div *ngIf="!loading">
<table mat-table [dataSource]="traces" class="mat-elevation-z8" joyrideStep="tableStep"
text="{{ 'tableStepText' | translate }}">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let trace">
<ng-container [ngSwitch]="column.columnDef">
<!-- Caso para "status" -->
<ng-container *ngSwitchCase="'status'">
<ng-container *ngIf="trace.status === 'in-progress' && trace.progress; else statusChip">
<div class="progress-container">
<mat-progress-bar class="example-margin" [mode]="mode" [value]="trace.progress" [bufferValue]="bufferValue">
</mat-progress-bar>
<span>{{trace.progress}}%</span>
</div>
</ng-container>
<ng-template #statusChip>
<mat-chip [ngClass]="{
'chip-failed': trace.status === 'failed',
'chip-success': trace.status === 'success',
'chip-pending': trace.status === 'pending',
'chip-in-progress': trace.status === 'in-progress'
}">
{{
trace.status === 'failed' ? 'Fallido' :
trace.status === 'in-progress' ? 'En ejecución' :
trace.status === 'success' ? 'Finalizado con éxito' :
trace.status === 'pending' ? 'Pendiente de ejecutar' :
trace.status
}}
</mat-chip>
</ng-template>
</ng-container>
<!-- Caso para "input" con modal -->
<ng-container *ngSwitchCase="'input'">
<button mat-icon-button (click)="openInputModal(trace.input)">
<mat-icon>info</mat-icon>
</button>
</ng-container>
<!-- Para cualquier otro caso (default) -->
<ng-container *ngSwitchDefault>
{{ column.cell(trace) }}
</ng-container>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<div class="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>

View File

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

View File

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

View File

@ -57,4 +57,15 @@
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
}
.checkbox-with-hint {
display: flex;
flex-direction: column;
}
.hint-text {
font-size: 12px;
color: gray;
margin-left: 40px;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,117 @@
.dialog-content {
display: flex;
flex-direction: column;
padding: 40px;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
.select-container {
margin-top: 20px;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.client-card {
background: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
padding: 8px;
text-align: center;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
&:hover {
background-color: #f0f0f0;
transform: scale(1.02);
}
}
.button-row {
display: flex;
padding-right: 1em;
}
.action-button {
margin-top: 10px;
margin-bottom: 10px;
}
.client-item {
position: relative;
}
.mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.selected-client {
background-color: #a0c2e5 !important;
color: white !important;
}
.loading-spinner {
display: block;
margin: 0 auto;
align-items: center;
justify-content: center;
}
.client-details {
margin-top: 4px;
}
.client-name {
font-size: 0.9em;
font-weight: 600;
color: #333;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
display: inline-block;
}
.client-ip {
display: block;
font-size: 0.9em;
color: #666;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
@media (max-width: 600px) {
.form-field {
width: 100%;
}
.dialog-actions {
flex-direction: column;
align-items: stretch;
}
button {
width: 100%;
margin-left: 0;
margin-bottom: 8px;
}
}

View File

@ -0,0 +1,100 @@
<h2 mat-dialog-title> Seleccionar particion para arrancar SO</h2>
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<div *ngIf="!loading" class="select-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-description>
Listado de clientes para arrancar un SO
<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
<div class="button-row">
<button class="action-button" (click)="toggleSelectAll()">
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
</button>
</div>
<div class="clients-grid">
<div *ngFor="let client of data.clients" class="client-item">
<div class="client-card"
(click)="client.status === 'og-live' && toggleClientSelection(client)"
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}" >
<img
[src]="'assets/images/computer_' + client.status + '.svg'"
alt="Client Icon"
class="client-image" />
<div class="client-details">
<span class="client-name">{{ client.name | slice:0:20 }}</span>
<span class="client-ip">{{ client.ip }}</span>
<span class="client-ip">{{ client.mac }}</span>
</div>
<mat-divider></mat-divider>
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
<mat-radio-button [value]="client"
color="primary"
[disabled]="!client.selected"
(click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
</mat-radio-group>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
<div *ngIf="!loading" class="partition-table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group
[(ngModel)]="selectedPartition"
[disabled]="!row.operativeSystem"
>
<mat-radio-button [value]="row">
</mat-radio-button>
</mat-radio-group>
</td>
</ng-container>
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef !== 'size'">
{{ column.cell(image) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'size'">
<div style="display: flex; flex-direction: column;">
<span> {{ image.size }} MB</span>
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
</div>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
</div>

View File

@ -0,0 +1,82 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BootSoPartitionComponent } from './boot-so-partition.component';
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatButtonModule } from "@angular/material/button";
import { MatMenuModule } from "@angular/material/menu";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { MatTableModule } from "@angular/material/table";
import { MatSelectModule } from "@angular/material/select";
import { MatIconModule } from "@angular/material/icon";
import { ToastrModule, ToastrService } from "ngx-toastr";
import { TranslateModule } from "@ngx-translate/core";
import { DataService } from "../../data.service";
import { provideHttpClient } from "@angular/common/http";
import { provideHttpClientTesting } from "@angular/common/http/testing";
import { ConfigService } from "@services/config.service";
import { MatExpansionModule } from '@angular/material/expansion';
import { MatDividerModule } from '@angular/material/divider';
describe('BootSoPartitionComponent', () => {
let component: BootSoPartitionComponent;
let fixture: ComponentFixture<BootSoPartitionComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [BootSoPartitionComponent],
imports: [
ReactiveFormsModule,
FormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
MatExpansionModule,
MatMenuModule,
BrowserAnimationsModule,
MatTableModule,
MatDividerModule,
MatSelectModule,
MatIconModule,
ToastrModule.forRoot(),
TranslateModule.forRoot()
],
providers: [
FormBuilder,
ToastrService,
DataService,
provideHttpClient(),
provideHttpClientTesting(),
{
provide: MatDialogRef,
useValue: {}
},
{
provide: MAT_DIALOG_DATA,
useValue: {
clients: []
}
},
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();
fixture = TestBed.createComponent(BootSoPartitionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

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