Compare commits
54 Commits
36539a3d9e
...
a3a6bb0c23
|
@ -1 +1,2 @@
|
|||
ogWebconsole/.env
|
||||
ogWebconsole/test-results/ogGui-junit-report.xml
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# Changelog
|
||||
|
||||
## [0.6.0] - 2024-11-21
|
||||
|
||||
### Added
|
||||
- Added functionality to execute actions from the menu in the general groups screen.
|
||||
- Displayed the selected center on the general screen for better context.
|
||||
- Implemented the option to collapse the sidebar for improved usability.
|
||||
- Introduced a dropdown menu to select boot files (BootfileNames).
|
||||
- Simplified the process of adding multiple clients in the subnets section.
|
||||
- Enabled ogBoot to manage storage units more intuitively.
|
||||
- Added preloading of template models in ogBoot for faster setup.
|
||||
- Created a contextual help tour to guide users through various components and features.
|
||||
|
||||
### Improved
|
||||
- Renamed the field "Reserved Room" to "Available RemotePC" for better clarity.
|
||||
- Added view sizes for visual cards.
|
||||
- Refactored the task stepper to improve efficiency and readability.
|
||||
- Replaced red "X" icons with more consistent and user-friendly visuals.
|
||||
- Allowed logical names for IP addresses in the subnets section.
|
||||
- Enabled reordering of commands when creating a command group.
|
||||
- Made predefined commands read-only to prevent accidental modifications.
|
||||
- Simplified the task creation modal to enhance user experience.
|
||||
- Adjusted the translation system to cover new elements and improve consistency (work in progress).
|
||||
- New element view from clients on groups main view
|
||||
|
||||
### Fixed
|
||||
- Resolved an issue that prevented editing software profiles correctly.
|
||||
- Fixed a bug where newly created commands failed to execute in the commands section.
|
||||
|
||||
---
|
||||
|
|
@ -26,10 +26,14 @@ pipeline {
|
|||
} else {
|
||||
LATEST_ID = "${DOCKER_REPO}/${DOCKER_IMAGE_NAME}:${BRANCH_NAME}-latest"
|
||||
}
|
||||
|
||||
env.IMAGE_ID_TESTING = IMAGE_ID_TESTING
|
||||
env.IMAGE_ID = IMAGE_ID
|
||||
env.LATEST_ID = LATEST_ID
|
||||
docker.build("${IMAGE_ID_TESTING}", "-f Dockerfile-testing .")
|
||||
if (env.TAG_NAME) {
|
||||
TAG_ID = "${DOCKER_REPO}/${DOCKER_IMAGE_NAME}:${env.TAG_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +55,9 @@ pipeline {
|
|||
dir('ogWebconsole') {
|
||||
docker.build("${IMAGE_ID}", "-f Dockerfile .")
|
||||
docker.build("${LATEST_ID}", "-f Dockerfile .")
|
||||
if (env.TAG_NAME) {
|
||||
docker.build("${TAG_ID}", "-f Dockerfile .")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +69,9 @@ pipeline {
|
|||
docker.withRegistry('https://index.docker.io/v1/', 'docker-hub-credentials') {
|
||||
docker.image("${IMAGE_ID}").push()
|
||||
docker.image("${LATEST_ID}").push()
|
||||
if (env.TAG_NAME) {
|
||||
docker.image("${TAG_ID}").push()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"i18n": {
|
||||
"sourceLocale": "es",
|
||||
"locales": {
|
||||
"en-US": "src/locale/messages.en.json"
|
||||
"en-US": "src/locale/en.json"
|
||||
}
|
||||
},
|
||||
"projectType": "application",
|
||||
|
@ -41,9 +41,14 @@
|
|||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "src/locale",
|
||||
"output": "/locale"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/custom-theme.scss",
|
||||
"src/styles.css",
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "og-webconsole",
|
||||
"version": "0.0.0",
|
||||
"version": "0.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "og-webconsole",
|
||||
"version": "0.0.0",
|
||||
"version": "0.5.0",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^18.0.0",
|
||||
"@angular/cdk": "~18.0.0",
|
||||
|
@ -18,8 +18,11 @@
|
|||
"@angular/platform-browser": "^18.0.0",
|
||||
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||
"@angular/router": "^18.0.0",
|
||||
"@ngx-translate/core": "^16.0.3",
|
||||
"@ngx-translate/http-loader": "^16.0.0",
|
||||
"@swimlane/ngx-charts": "^20.5.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"ngx-joyride": "^2.5.0",
|
||||
"ngx-toastr": "^19.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
@ -4975,6 +4978,30 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngx-translate/core": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-16.0.3.tgz",
|
||||
"integrity": "sha512-UPse66z9tRUmIpeorYodXBQY6O4foUmj9jy9cCuuja7lqdOwRBWPzCWqc+qYIXv5L2QoqZdxgHtqoUz+Q9weSA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16",
|
||||
"@angular/core": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngx-translate/http-loader": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-16.0.0.tgz",
|
||||
"integrity": "sha512-l3okOHGVxZ1Bm55OpakSfXvI2yYmVmhYqgwGU4aIQIRUqpkBCrSDZnmrHTcZfsGJzXKB5E2D2rko9i28gBijmA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16",
|
||||
"@angular/core": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
@ -6533,9 +6560,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
|
@ -6546,7 +6573,7 @@
|
|||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
|
@ -7221,9 +7248,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
|
@ -8081,9 +8108,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.5.5",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz",
|
||||
"integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==",
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
|
||||
"integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
|
@ -8091,7 +8118,7 @@
|
|||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.4.1",
|
||||
"cookie": "~0.7.2",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
|
@ -8397,37 +8424,37 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
|
@ -8439,9 +8466,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
|
@ -8456,14 +8483,23 @@
|
|||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
|
@ -10783,10 +10819,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
|
||||
"dev": true
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
|
@ -10813,9 +10852,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
|
@ -11228,6 +11267,18 @@
|
|||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ngx-joyride": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-joyride/-/ngx-joyride-2.5.0.tgz",
|
||||
"integrity": "sha512-C/J8C4uWZjKl9aMmRBt9egVjuIpwWFplJgBZDl1EfqNVTJkdEC51nt9DpAOuDwOgkbArhJ9sZIk3bZT4vkud/w==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=8.2.14",
|
||||
"@angular/core": ">=8.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-toastr": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz",
|
||||
|
@ -11577,9 +11628,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
|
||||
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
@ -12052,9 +12103,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
|
@ -12343,12 +12394,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
|
@ -12923,9 +12974,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
|
@ -13067,20 +13118,29 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
|
@ -13244,16 +13304,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.7.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
|
||||
"integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.5.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
|
@ -14469,9 +14529,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
|
||||
"integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
|
||||
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
|
|
|
@ -20,8 +20,11 @@
|
|||
"@angular/platform-browser": "^18.0.0",
|
||||
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||
"@angular/router": "^18.0.0",
|
||||
"@ngx-translate/core": "^16.0.3",
|
||||
"@ngx-translate/http-loader": "^16.0.0",
|
||||
"@swimlane/ngx-charts": "^20.5.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"ngx-joyride": "^2.5.0",
|
||||
"ngx-toastr": "^19.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
|
|
@ -23,10 +23,23 @@ import { TaskLogsComponent } from './components/commands/commands-task/task-logs
|
|||
import { StatusComponent } from "./components/ogdhcp/og-dhcp-subnets/status/status.component";
|
||||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import { RestoreImageComponent } from './components/groups/components/client-main-view/restore-image/restore-image.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 {
|
||||
CreateImageComponent
|
||||
} from "./components/groups/components/client-main-view/create-image/create-image.component";
|
||||
import {
|
||||
DeployImageComponent
|
||||
} from "./components/groups/components/client-main-view/deploy-image/deploy-image.component";
|
||||
import {
|
||||
MainRepositoryViewComponent
|
||||
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
||||
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||
{
|
||||
|
@ -36,6 +49,7 @@ const routes: Routes = [
|
|||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'admin', component: AdminComponent },
|
||||
{ path: 'users', component: UsersComponent },
|
||||
{ path: 'env-vars', component: EnvVarsComponent },
|
||||
{ path: 'user-groups', component: RolesComponent },
|
||||
{ path: 'groups', component: GroupsComponent },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent },
|
||||
|
@ -50,9 +64,13 @@ const routes: Routes = [
|
|||
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent },
|
||||
{ path: 'calendars', component: CalendarComponent },
|
||||
{ path: 'client/:id', component: ClientMainViewComponent },
|
||||
{ path: 'clients/:id', component: ClientMainViewComponent },
|
||||
{ path: 'clients/:id/partition-assistant', component: PartitionAssistantComponent },
|
||||
{ path: 'clients/:id/create-image', component: CreateImageComponent },
|
||||
{ path: 'clients/:id/deploy-image', component: DeployImageComponent },
|
||||
{ path: 'images', component: ImagesComponent },
|
||||
{ path: 'restore-image', component: RestoreImageComponent},
|
||||
{ path: 'repositories', component: RepositoriesComponent },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||
{ path: 'software', component: SoftwareComponent },
|
||||
{ path: 'software-profiles', component: SoftwareProfileComponent },
|
||||
{ path: 'operative-systems', component: OperativeSystemComponent },
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing'; // Asegúrate de que está importado
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
RouterTestingModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
|
@ -19,5 +21,4 @@ describe('AppComponent', () => {
|
|||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -7,4 +8,11 @@ import { Component } from '@angular/core';
|
|||
})
|
||||
export class AppComponent {
|
||||
title = 'ogWebconsole';
|
||||
constructor(private translateService: TranslateService) {
|
||||
const savedLanguage = localStorage.getItem('language') || 'es';
|
||||
this.translateService.use(savedLanguage);
|
||||
}
|
||||
ngOnInit() {
|
||||
sessionStorage.setItem('language', 'es');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { HeaderComponent } from './layout/header/header.component';
|
|||
import { SidebarComponent } from './layout/sidebar/sidebar.component';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { HTTP_INTERCEPTORS, HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { CustomInterceptor } from './core/services/custom.interceptor';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
|
@ -106,7 +106,6 @@ import { ClientMainViewComponent } from './components/groups/components/client-m
|
|||
import { ImagesComponent } from './components/images/images.component';
|
||||
import { CreateImageComponent } from './components/images/create-image/create-image.component';
|
||||
import { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component';
|
||||
import { RestoreImageComponent } from './components/groups/components/client-main-view/restore-image/restore-image.component';
|
||||
import { SoftwareComponent } from './components/software/software.component';
|
||||
import { CreateSoftwareComponent } from './components/software/create-software/create-software.component';
|
||||
import { SoftwareProfileComponent } from './components/software-profile/software-profile.component';
|
||||
|
@ -116,6 +115,21 @@ import { CreateOperativeSystemComponent } from './components/operative-system/cr
|
|||
import { ShowTemplateContentComponent } from './components/ogboot/pxe/show-template-content/show-template-content.component';
|
||||
import { AddClientsToPxeComponent } from './components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component';
|
||||
import { ClientsComponent } from './components/ogboot/pxe/clients/clients.component';
|
||||
import { RepositoriesComponent } from './components/repositories/repositories.component';
|
||||
import { CreateRepositoryComponent } from './components/repositories/create-repository/create-repository.component';
|
||||
import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component';
|
||||
import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component';
|
||||
import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component';
|
||||
import { ExecuteCommandOuComponent } from './components/groups/shared/execute-command-ou/execute-command-ou.component';
|
||||
import { JoyrideModule } from 'ngx-joyride';
|
||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
@ -179,7 +193,6 @@ import { ClientsComponent } from './components/ogboot/pxe/clients/clients.compon
|
|||
ImagesComponent,
|
||||
CreateImageComponent,
|
||||
PartitionAssistantComponent,
|
||||
RestoreImageComponent,
|
||||
SoftwareComponent,
|
||||
CreateSoftwareComponent,
|
||||
SoftwareProfileComponent,
|
||||
|
@ -189,6 +202,14 @@ import { ClientsComponent } from './components/ogboot/pxe/clients/clients.compon
|
|||
ShowTemplateContentComponent,
|
||||
AddClientsToPxeComponent,
|
||||
ClientsComponent,
|
||||
RepositoriesComponent,
|
||||
CreateRepositoryComponent,
|
||||
ExecuteCommandComponent,
|
||||
ExecuteCommandOuComponent,
|
||||
DeployImageComponent,
|
||||
MainRepositoryViewComponent,
|
||||
ExecuteCommandOuComponent,
|
||||
EnvVarsComponent,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
@ -217,6 +238,14 @@ import { ClientsComponent } from './components/ogboot/pxe/clients/clients.compon
|
|||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
MatSliderModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
JoyrideModule.forRoot(),
|
||||
ToastrModule.forRoot(
|
||||
{
|
||||
timeOut: 5000,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<div class="container">
|
||||
<button mat-fab color="primary" class="fab-button" routerLink="/users">
|
||||
<mat-icon>group</mat-icon>
|
||||
<span i18n="@@labelUsers">Usuarios</span>
|
||||
<span>{{ 'labelUsers' | translate }}</span>
|
||||
</button>
|
||||
<button mat-fab color="primary" class="fab-button" routerLink="/user-groups">
|
||||
<mat-icon>admin_panel_settings</mat-icon>
|
||||
<span i18n="@@labelRoles">Roles</span>
|
||||
<span>{{ 'labelRoles' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -3,55 +3,55 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||
import { AdminComponent } from './admin.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { By } from '@angular/platform-browser';
|
||||
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, // Importa RouterTestingModule para manejar routerLink
|
||||
MatButtonModule, // Importa MatButtonModule para botones
|
||||
MatIconModule // Importa MatIconModule para iconos
|
||||
RouterTestingModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
TranslateModule.forRoot()
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges(); // Detecta cambios para renderizar el componente
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('debería crear el componente', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('debería contener dos botones', () => {
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
expect(buttons.length).toBe(2); // Verifica que hay dos botones
|
||||
it('debería renderizar dos botones', () => {
|
||||
const buttons = fixture.nativeElement.querySelectorAll('button');
|
||||
expect(buttons.length).toBe(2);
|
||||
});
|
||||
|
||||
it('el primer botón debería tener el texto "Usuarios"', () => {
|
||||
const firstButton = fixture.debugElement.query(By.css('.fab-button:first-child'));
|
||||
expect(firstButton.nativeElement.textContent).toContain('Usuarios'); // Verifica que el texto sea "Usuarios"
|
||||
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');
|
||||
});
|
||||
|
||||
it('el segundo botón debería tener el texto "Roles"', () => {
|
||||
const secondButton = fixture.debugElement.query(By.css('.fab-button:last-child'));
|
||||
expect(secondButton.nativeElement.textContent).toContain('Roles'); // Verifica que el texto sea "Roles"
|
||||
});
|
||||
|
||||
it('el primer botón debería tener el routerLink correcto', () => {
|
||||
const firstButton = fixture.debugElement.query(By.css('.fab-button:first-child'));
|
||||
expect(firstButton.nativeElement.getAttribute('ng-reflect-router-link')).toBe('/users'); // Verifica el routerLink
|
||||
});
|
||||
|
||||
it('el segundo botón debería tener el routerLink correcto', () => {
|
||||
const secondButton = fixture.debugElement.query(By.css('.fab-button:last-child'));
|
||||
expect(secondButton.nativeElement.getAttribute('ng-reflect-router-link')).toBe('/user-groups'); // Verifica el routerLink
|
||||
it('debería aplicar la clase "fab-button" a ambos botones', () => {
|
||||
const buttons = fixture.nativeElement.querySelectorAll('button');
|
||||
buttons.forEach((button: HTMLElement) => {
|
||||
expect(button.classList).toContain('fab-button');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
.env-settings {
|
||||
padding: 16px;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mat-table {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.value-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
|
||||
button {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<div class="env-settings">
|
||||
<h1>Editar Variables de Entorno</h1>
|
||||
|
||||
<mat-table [dataSource]="envVars" class="mat-elevation-z8">
|
||||
<!-- Nombre de la variable -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef> Variable </mat-header-cell>
|
||||
<mat-cell *matCellDef="let variable">{{ variable.name }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Valor de la variable -->
|
||||
<ng-container matColumnDef="value">
|
||||
<mat-header-cell *matHeaderCellDef> Valor </mat-header-cell>
|
||||
<mat-cell *matCellDef="let variable">
|
||||
<mat-form-field class="value-input">
|
||||
<input matInput [(ngModel)]="variable.value" />
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<div class="actions" >
|
||||
<button mat-raised-button color="primary" (click)="saveEnvVars()">Guardar Cambios</button>
|
||||
<button mat-raised-button color="accent" (click)="loadEnvVars()">Recargar</button>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,62 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { EnvVarsComponent } from './env-vars.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { ReactiveFormsModule, FormsModule, FormBuilder } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
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';
|
||||
|
||||
describe('EnvVarsComponent', () => {
|
||||
let component: EnvVarsComponent;
|
||||
let fixture: ComponentFixture<EnvVarsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [EnvVarsComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
BrowserAnimationsModule,
|
||||
MatTableModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EnvVarsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import { Component } from '@angular/core';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-env-vars',
|
||||
templateUrl: './env-vars.component.html',
|
||||
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`;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadEnvVars();
|
||||
}
|
||||
|
||||
loadEnvVars(): void {
|
||||
this.http.get<{ vars: Record<string, string> }>(this.apiUrl).subscribe({
|
||||
next: (response) => {
|
||||
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.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveEnvVars(): void {
|
||||
const vars = this.envVars.reduce((acc, variable) => {
|
||||
acc[variable.name] = variable.value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
this.http.post(this.apiUrl, { vars }).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Variables de entorno guardadas correctamente.');
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error al guardar las variables de entorno:', err);
|
||||
this.toastService.error('No se pudieron cargar las variables de entorno.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,22 +1,22 @@
|
|||
<h1 mat-dialog-title i18n="@@dialogTitleAddRole">Añadir Rol</h1>
|
||||
<h1 mat-dialog-title>{{ 'dialogTitleAddRole' | translate }}</h1>
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="roleForm" class="role-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label i18n="@@labelRoleName">Nombre</mat-label>
|
||||
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
|
||||
<section class="example-section">
|
||||
<h4 i18n="@@sectionTitlePermissions">Permisos:</h4>
|
||||
<p><mat-checkbox formControlName="superAdmin" i18n="@@checkboxSuperAdmin">Super Admin</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="orgAdmin" i18n="@@checkboxOrgAdmin">Admin de Unidad Organizativa</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="orgOperator" i18n="@@checkboxOrgOperator">Operador de Unidad Organizativa</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="orgMinimal" i18n="@@checkboxOrgMinimal">Unidad Organizativa Mínima</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="userRole" i18n="@@checkboxUserRole">Usuario</mat-checkbox></p>
|
||||
<h4>{{ 'sectionTitlePermissions' | translate }}</h4>
|
||||
<p><mat-checkbox formControlName="superAdmin">{{ 'checkboxSuperAdmin' | translate }}</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="orgAdmin">{{ 'checkboxOrgAdmin' | translate }}</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="orgOperator">{{ 'checkboxOrgOperator' | translate }}</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="orgMinimal">{{ 'checkboxOrgMinimal' | translate }}</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="userRole">{{ 'checkboxUserRole' | translate }}</mat-checkbox></p>
|
||||
</section>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()" i18n="@@buttonCancel">Cancelar</button>
|
||||
<button mat-button (click)="onSubmit()" i18n="@@buttonAdd">Añadir</button>
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()">{{ 'buttonAdd' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Administrar Roles</h2>
|
||||
<h2 class="title">{{ 'adminRolesTitle' | translate }}</h2>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="addUser()">Añadir rol</button>
|
||||
<button mat-flat-button color="primary" (click)="addUser()">
|
||||
{{ 'addRole' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre de rol</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-label>{{ 'searchRoleLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
|
@ -26,12 +28,12 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let role" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editRole(role)" i18n="@@editImage">
|
||||
<button mat-icon-button color="primary" (click)="editRole(role)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteRole(role)" i18n="@@buttonDelete" [disabled]="role.permissions.includes('ROLE_SUPER_ADMIN')">
|
||||
<button mat-icon-button color="warn" (click)="deleteRole(role)" [disabled]="role.permissions.includes('ROLE_SUPER_ADMIN')">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { MatLabel } from '@angular/material/form-field';
|
|||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatHint } from '@angular/material/form-field';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
describe('RolesComponent', () => {
|
||||
let component: RolesComponent;
|
||||
let fixture: ComponentFixture<RolesComponent>;
|
||||
|
@ -27,7 +28,8 @@ describe('RolesComponent', () => {
|
|||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RolesComponent],
|
||||
imports: [MatDivider, MatFormField, MatLabel, MatIcon, MatHint, MatPaginator],
|
||||
imports: [MatDivider, MatFormField, MatLabel, MatIcon, MatHint, MatPaginator,
|
||||
TranslateModule.forRoot()],
|
||||
providers: [
|
||||
{ provide: MatDialog, useValue: matDialogSpy },
|
||||
{ provide: HttpClient, useValue: httpClientSpy },
|
||||
|
@ -49,4 +51,18 @@ describe('RolesComponent', () => {
|
|||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a default itemsPerPage value', () => {
|
||||
expect(component.itemsPerPage).toBeDefined();
|
||||
});
|
||||
|
||||
it('should initialize the dataSource', () => {
|
||||
expect(component.dataSource).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have a defined columns array', () => {
|
||||
expect(component.columns).toBeDefined();
|
||||
expect(component.columns.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<h1 mat-dialog-title i18n="@@dialogTitleAddRole">Añadir Usuario</h1>
|
||||
<h1 mat-dialog-title>{{ 'dialogTitleAddUser' | translate }}</h1>
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="userForm" class="user-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label i18n="@@addUserlabelUsername">Nombre de usuario</mat-label>
|
||||
<mat-label>{{ 'addUserlabelUsername' | translate }}</mat-label>
|
||||
<input matInput formControlName="username" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label i18n="@@addUserlabelPassword">Contraseña</mat-label>
|
||||
<mat-label>{{ 'addUserlabelPassword' | translate }}</mat-label>
|
||||
<input matInput formControlName="password" type="password" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label i18n="@@labelRole">Rol</mat-label>
|
||||
<mat-label>{{ 'labelRole' | translate }}</mat-label>
|
||||
<mat-select formControlName="role" required>
|
||||
<mat-option *ngFor="let group of userGroups" [value]="group['@id']">
|
||||
{{ group.name }}
|
||||
|
@ -20,16 +20,16 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label i18n="@@labelOrganizationalUnit">Unidad organiativa</mat-label>
|
||||
<mat-label>{{ 'labelOrganizationalUnit' | translate }}</mat-label>
|
||||
<mat-select multiple formControlName="organizationalUnits">
|
||||
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit['@id']">
|
||||
{{unit.name}}
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()" i18n="@@buttonCancel">Cancelar</button>
|
||||
<button mat-button (click)="onSubmit()" i18n="@@buttonAdd">Añadir</button>
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()">{{ 'buttonAdd' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
<h1 mat-dialog-title i18n="@@dialogTitleEditUser">Editar Usuario</h1>
|
||||
<h1 mat-dialog-title>{{ 'dialogTitleEditUser' | translate }}</h1>
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="userForm" class="user-form">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@labelCurrentPassword">Contraseña actual</mat-label>
|
||||
<mat-label>{{ 'labelCurrentPassword' | translate }}</mat-label>
|
||||
<input matInput formControlName="currentPassword" type="password">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@labelNewPassword">Nueva contraseña</mat-label>
|
||||
<mat-label>{{ 'labelNewPassword' | translate }}</mat-label>
|
||||
<input matInput formControlName="newPassword" type="password">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@labelRepeatPassword">Repite la contraseña</mat-label>
|
||||
<mat-label>{{ 'labelRepeatPassword' | translate }}</mat-label>
|
||||
<input matInput formControlName="repeatNewPassword" type="password">
|
||||
</mat-form-field>
|
||||
|
||||
<div class="error-message" *ngIf="passwordMismatch" i18n="@@errorPasswordMismatch">
|
||||
Las contraseñas no coinciden
|
||||
<div class="error-message" *ngIf="passwordMismatch">
|
||||
{{ 'errorPasswordMismatch' | translate }}
|
||||
</div>
|
||||
|
||||
<div class="error-message" *ngIf="updateError" i18n="@@errorUpdate">
|
||||
<div class="error-message" *ngIf="updateError">
|
||||
{{ resetPasswordError }}
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()" i18n="@@buttonCancel">Cancelar</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="loading" i18n="@@buttonEdit">Editar</button>
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="loading">{{ 'buttonEdit' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Administrar usuarios</h2>
|
||||
<h2 class="title">{{ 'adminImagesTitle' | translate }}</h2>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="addUser()">Añadir usuarios</button>
|
||||
<button mat-flat-button color="primary" (click)="addUser()">
|
||||
{{ 'addUser' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre de usuario</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
|
@ -26,10 +28,14 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editUser(user)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" (click)="deleteUser(user)" i18n="@@buttonDelete"><mat-icon>delete</mat-icon></button>
|
||||
<button mat-icon-button color="primary" (click)="editUser(user)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteUser(user)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
|
|
|
@ -6,16 +6,11 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
|
|||
import { ToastrService } from 'ngx-toastr';
|
||||
import { of } from 'rxjs';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
class MockUserService {
|
||||
getUsers() {
|
||||
return of({
|
||||
'hydra:member': [
|
||||
{ id: 1, username: 'user1', allowedOrganizationalUnits: [], roles: ['admin'] },
|
||||
{ id: 2, username: 'user2', allowedOrganizationalUnits: [], roles: ['user'] }
|
||||
]
|
||||
});
|
||||
}
|
||||
class MockToastrService {
|
||||
success() {}
|
||||
error() {}
|
||||
}
|
||||
|
||||
describe('UsersComponent', () => {
|
||||
|
@ -29,14 +24,13 @@ describe('UsersComponent', () => {
|
|||
MatTableModule,
|
||||
MatDialogModule,
|
||||
HttpClientTestingModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ useClass: MockUserService },
|
||||
{ provide: ToastrService, useValue: { success: () => {} } },
|
||||
{ provide: ToastrService, useClass: MockToastrService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
})
|
||||
.compileComponents();
|
||||
schemas: [NO_ERRORS_SCHEMA], // Ignorar elementos desconocidos
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UsersComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@ -47,4 +41,27 @@ describe('UsersComponent', () => {
|
|||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have default values for pagination', () => {
|
||||
expect(component.itemsPerPage).toBe(10);
|
||||
expect(component.page).toBe(0);
|
||||
expect(component.pageSizeOptions).toEqual([5, 10, 20, 40, 100]);
|
||||
});
|
||||
|
||||
it('should call search on init', () => {
|
||||
spyOn(component, 'search');
|
||||
component.ngOnInit();
|
||||
expect(component.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should initialize the dataSource', () => {
|
||||
expect(component.dataSource).toBeDefined();
|
||||
});
|
||||
|
||||
it('should define displayedColumns', () => {
|
||||
expect(component.displayedColumns).toBeDefined();
|
||||
expect(component.displayedColumns).toContain('id');
|
||||
expect(component.displayedColumns).toContain('username');
|
||||
expect(component.displayedColumns).toContain('roles');
|
||||
expect(component.displayedColumns).toContain('actions');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Administrar calendarios</h2>
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}" class="title">{{ 'adminCalendarsTitle' | translate }}</h2>
|
||||
<div class="calendar-button-row">
|
||||
<button mat-flat-button color="primary" (click)="addImage()">Añadir calendario</button>
|
||||
<button joyrideStep="addButtonStep" text="{{ 'addButtonStepText' | translate }}" mat-flat-button color="primary" (click)="addImage()">
|
||||
{{ 'addCalendar' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre de calendario</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-form-field joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}" appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'searchCalendarLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
|
@ -19,29 +25,36 @@
|
|||
</div>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image" >
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef === 'isDefault' || column.columnDef === 'installed'">
|
||||
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
||||
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||
</mat-icon>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef !== 'isDefault' && column.columnDef !== 'installed' && column.columnDef !== 'downloadUrl'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<ng-container matColumnDef="actions" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let calendar" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editCalendar(calendar)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
||||
<button *ngIf="!syncUds" mat-icon-button color="primary" (click)="sync(calendar)"><mat-icon>sync</mat-icon></button>
|
||||
<button *ngIf="syncUds" mat-icon-button color="primary"><mat-spinner diameter="24"></mat-spinner></button>
|
||||
<button mat-icon-button color="warn" (click)="deleteCalendar(calendar)" i18n="@@buttonDelete"><mat-icon>delete</mat-icon></button>
|
||||
<button mat-icon-button color="primary" (click)="editCalendar(calendar)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="!syncUds" mat-icon-button color="primary" (click)="sync(calendar)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="syncUds" mat-icon-button color="primary">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteCalendar(calendar)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
|||
import { FormsModule } from '@angular/forms';
|
||||
import { CalendarComponent } from './calendar.component';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { JoyrideModule, JoyrideService } from 'ngx-joyride';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('CalendarComponent', () => {
|
||||
let component: CalendarComponent;
|
||||
|
@ -34,8 +36,10 @@ describe('CalendarComponent', () => {
|
|||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
MatProgressSpinner
|
||||
]
|
||||
MatProgressSpinner,
|
||||
JoyrideModule.forRoot(),
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ToastrService } from "ngx-toastr";
|
|||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-calendar',
|
||||
|
@ -51,14 +52,14 @@ 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 toastService: ToastrService,
|
||||
private joyrideService: JoyrideService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -71,7 +72,6 @@ export class CalendarComponent implements OnInit {
|
|||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
console.log('The dialog was closed');
|
||||
this.search();
|
||||
});
|
||||
}
|
||||
|
@ -84,24 +84,21 @@ export class CalendarComponent implements OnInit {
|
|||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching og lives', error);
|
||||
console.error('Error fetching calendars', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
sync(calendar: any): void {
|
||||
console.log('Syncing calendars');
|
||||
this.syncUds = true;
|
||||
this.http.post(`${this.apiUrl}/${calendar.uuid}/sync-uds`, {}).subscribe({
|
||||
next: () => {
|
||||
console.log('Calendars synced successfully');
|
||||
this.toastService.success('Calendarios sincronizados correctamente');
|
||||
this.search();
|
||||
this.syncUds = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al sincronizar los calendarios:', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.syncUds = false;
|
||||
}
|
||||
|
@ -133,16 +130,13 @@ export class CalendarComponent implements OnInit {
|
|||
|
||||
this.http.delete(apiUrl).subscribe({
|
||||
next: () => {
|
||||
console.log('Calendar deleted successfully');
|
||||
this.search();
|
||||
this.toastService.success('Calendar deleted successfully');
|
||||
},
|
||||
error: (error) => {
|
||||
error: () => {
|
||||
this.toastService.error('Error deleting calendar');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('calendar deletion cancelled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -155,8 +149,7 @@ export class CalendarComponent implements OnInit {
|
|||
this.length = response['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al cargar las imágenes:', error);
|
||||
error: () => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
|
@ -167,4 +160,12 @@ export class CalendarComponent implements OnInit {
|
|||
this.itemsPerPage = event.pageSize;
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: ['titleStep', 'addButtonStep', 'searchStep', 'tableStep', 'actionsStep'],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Añadir' }} calendario</h2>
|
||||
<h2 mat-dialog-title>{{ isEditMode ? ('editCalendar' | translate) : ('addCalendar' | translate) }}</h2>
|
||||
<mat-dialog-content class="form-container">
|
||||
<mat-slide-toggle [(ngModel)]="isRemoteAvailable" class="example-margin">¿Disponibilidad remoto?</mat-slide-toggle>
|
||||
<mat-slide-toggle [(ngModel)]="isRemoteAvailable" class="example-margin">
|
||||
{{ 'remoteAvailability' | translate }}
|
||||
</mat-slide-toggle>
|
||||
|
||||
<div *ngIf="!isRemoteAvailable" class="form-group">
|
||||
<mat-label>Selecciona los días de la semana</mat-label>
|
||||
<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]">
|
||||
<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>
|
||||
|
@ -19,32 +21,32 @@
|
|||
|
||||
<div class="time-fields">
|
||||
<mat-form-field appearance="fill" class="time-field">
|
||||
<mat-label>Hora de inicio</mat-label>
|
||||
<input matInput [(ngModel)]="busyFromHour" type="time" placeholder="Selecciona la hora de inicio" [required]="!isRemoteAvailable">
|
||||
<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>Hora de fin</mat-label>
|
||||
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="Selecciona la hora de fin" [required]="!isRemoteAvailable">
|
||||
<mat-label>{{ 'endTime' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRemoteAvailable" class="form-group">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Razón</mat-label>
|
||||
<input matInput [(ngModel)]="availableReason" placeholder="Razon para la excepción" [required]="isRemoteAvailable">
|
||||
<mat-label>{{ 'reasonLabel' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="availableReason" placeholder="{{ 'reasonPlaceholder' | translate }}" [required]="isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
<div class="time-fields">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Fecha de inicio</mat-label>
|
||||
<mat-label>{{ 'startDate' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="picker1" [(ngModel)]="availableFromDate" [required]="isRemoteAvailable">
|
||||
<mat-hint>MM/DD/YYYY</mat-hint>
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker1"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker1></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Fecha de fin</mat-label>
|
||||
<mat-label>{{ 'endDate' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="picker2" [(ngModel)]="availableToDate" [required]="isRemoteAvailable">
|
||||
<mat-hint>MM/DD/YYYY</mat-hint>
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker2"></mat-datepicker-toggle>
|
||||
|
@ -55,12 +57,12 @@
|
|||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">Cancelar</button>
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button
|
||||
mat-button
|
||||
(click)="submitRule()"
|
||||
cdkFocusInitial
|
||||
[disabled]="(!isRemoteAvailable && (!busyFromHour || !busyToHour)) || (isRemoteAvailable && (!availableReason || !availableFromDate || !availableToDate))">
|
||||
{{ isEditMode ? 'Guardar' : 'Añadir' }}
|
||||
{{ isEditMode ? ('buttonSave' | translate) : ('buttonAdd' | translate) }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Añadir' }} calendario</h2>
|
||||
<h2 mat-dialog-title>{{ isEditMode ? ('editCalendar' | translate) : ('addCalendar' | translate) }}</h2>
|
||||
<mat-dialog-content class="form-container">
|
||||
<!-- Campo para el nombre -->
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Nombre</mat-label>
|
||||
<mat-label>{{ 'labelName' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="name" required>
|
||||
<mat-icon *ngIf="isEditMode" matSuffix (click)="submitForm()">mode_edit</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div *ngIf="isEditMode" mat-subheader>Reglas</div>
|
||||
<div *ngIf="isEditMode" mat-subheader>{{ 'rulesHeader' | translate }}</div>
|
||||
<button mat-flat-button color="primary" *ngIf="isEditMode" (click)="createRule()" style="padding: 10px;">
|
||||
Añadir regla
|
||||
{{ 'addRule' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -21,16 +22,16 @@
|
|||
<div class="list-item-content">
|
||||
<mat-icon matListItemIcon>event_available</mat-icon>
|
||||
<div class="text-content">
|
||||
<div matListItemTitle>{{ rule.isRemoteAvailable ? 'Disponible' : 'No disponible ( periodo presencial )' }}</div>
|
||||
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('statusAvailable' | translate) : ('statusUnavailable' | translate) }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyWeekDaysMap }}</div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">{{ rule.availableReason }} | {{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</div>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
<button mat-icon-button color="primary" class="right-icon" (click)="createRule(rule)" i18n="@@editImage">
|
||||
<button mat-icon-button color="primary" class="right-icon" (click)="createRule(rule)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" class="right-icon" (click)="deleteCalendarRule(rule)" >
|
||||
<button mat-icon-button color="warn" class="right-icon" (click)="deleteCalendarRule(rule)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -41,6 +42,8 @@
|
|||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">Cancelar</button>
|
||||
<button mat-button (click)="submitForm()" [disabled]="!name || name === ''" cdkFocusInitial> Guardar </button>
|
||||
<button mat-button (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="submitForm()" [disabled]="!name || name === ''" cdkFocusInitial>
|
||||
{{ 'buttonSave' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,25 +1,32 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminCommandGroupsTitle">Administrar Grupos de Comandos</h2>
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandGroupsTitle' | translate }}</h2>
|
||||
<div class="command-groups-button-row">
|
||||
<button mat-flat-button color="primary" (click)="openCreateCommandGroupModal()">Añadir Grupo de Comandos</button>
|
||||
<button mat-flat-button color="primary" (click)="openCreateCommandGroupModal()" joyrideStep="addCommandGroupStep" text="{{ 'addCommandGroupStepText' | translate }}">
|
||||
{{ 'addCommandGroup' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
<div class="search-container">
|
||||
|
||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre de grupo</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-label>{{ 'searchGroupNameLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<div *ngIf="loading" class="loading-container" joyrideStep="loadingStep" text="{{ 'loadingStepText' | translate }}">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let commandGroup">
|
||||
|
@ -27,8 +34,8 @@
|
|||
{{ column.cell(commandGroup) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'commands'">
|
||||
<button mat-button [matMenuTriggerFor]="menu">Ver comandos</button>
|
||||
<ng-container *ngIf="column.columnDef === 'commands'" joyrideStep="viewCommandsStep" text="{{ 'viewCommandsStepText' | translate }}">
|
||||
<button mat-button [matMenuTriggerFor]="menu">{{ 'viewCommands' | translate }}</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of commandGroup.commands">
|
||||
{{ command.name }}
|
||||
|
@ -37,13 +44,18 @@
|
|||
</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 client" style="text-align: center;">
|
||||
<button mat-icon-button color="info" (click)="viewGroupDetails(client)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
|
||||
<button mat-icon-button color="primary" (click)="editCommandGroup(client)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let client" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<button mat-icon-button color="info" (click)="viewGroupDetails(client)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="editCommandGroup(client)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteCommandGroup(client)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -53,7 +65,7 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container">
|
||||
<div class="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}">
|
||||
<mat-paginator [length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageIndex]="page"
|
||||
|
|
|
@ -7,6 +7,7 @@ import { DetailCommandGroupComponent } from './detail-command-group/detail-comma
|
|||
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
|
||||
@Component({
|
||||
selector: 'app-commands-groups',
|
||||
|
@ -48,7 +49,8 @@ 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) {}
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
|
@ -114,4 +116,21 @@ export class CommandsGroupsComponent implements OnInit {
|
|||
this.length = event.length;
|
||||
this.search();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleStep',
|
||||
'addCommandGroupStep',
|
||||
'searchStep',
|
||||
'tableStep',
|
||||
'viewCommandsStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
.create-command-group-container {
|
||||
padding: 20px;
|
||||
max-width: 800px; /* Ancho máximo del contenedor */
|
||||
margin: auto; /* Centra el contenedor en la pantalla */
|
||||
background-color: #fff; /* Fondo blanco para el contenedor */
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
|
@ -24,14 +24,14 @@
|
|||
width: 48%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 200px; /* Limita la altura máxima para evitar desbordamiento */
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto; /* Scroll para la tabla si hay demasiados comandos */
|
||||
border: 1px solid #ccc; /* Borde para la tabla */
|
||||
border-radius: 4px; /* Bordes redondeados */
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.selected-commands-list {
|
||||
|
@ -40,7 +40,7 @@
|
|||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
flex: 1;
|
||||
overflow-y: auto; /* Scroll para los comandos seleccionados */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.commands-container {
|
||||
|
@ -60,7 +60,7 @@
|
|||
|
||||
.remove-icon {
|
||||
cursor: pointer;
|
||||
color: #f44336; /* Rojo para eliminar */
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
|
@ -74,29 +74,27 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.available-commands, .selected-commands {
|
||||
width: 48%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 500px; /* Limita la altura máxima para evitar desbordamiento */
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto; /* Scroll para la tabla si hay demasiados comandos */
|
||||
border: 1px solid #ccc; /* Borde para la tabla */
|
||||
border-radius: 4px; /* Bordes redondeados */
|
||||
max-height: 400px; /* Establece la altura máxima */
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
/* Para asegurar que el componente sea responsivo en pantallas pequeñas */
|
||||
@media (max-width: 600px) {
|
||||
.command-selection {
|
||||
flex-direction: column; /* Cambia a columna en pantallas pequeñas */
|
||||
flex-direction: column;
|
||||
}
|
||||
.available-commands, .selected-commands {
|
||||
width: 100%; /* Ocupa el ancho completo */
|
||||
margin-bottom: 20px; /* Espacio entre elementos */
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<h2 mat-dialog-title>{{ editing ? 'Editar' : 'Crear' }} grupo de comando</h2>
|
||||
<h2 mat-dialog-title>{{ editing ? ('editCommandGroup' | translate) : ('createCommandGroup' | translate) }}</h2>
|
||||
<mat-dialog-content class="form-container">
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
|
@ -6,24 +6,24 @@
|
|||
|
||||
<form *ngIf="!loading" class="command-group-form" (ngSubmit)="onSubmit()">
|
||||
<mat-form-field>
|
||||
<mat-label>Nombre del Grupo</mat-label>
|
||||
<mat-label>{{ 'groupNameLabel' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="groupName" name="groupName" required />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-slide-toggle [(ngModel)]="enabled" name="enabled">Habilitado</mat-slide-toggle>
|
||||
<mat-slide-toggle [(ngModel)]="enabled" name="enabled">{{ 'enabledToggle' | translate }}</mat-slide-toggle>
|
||||
|
||||
<div class="command-selection">
|
||||
<div class="available-commands">
|
||||
<h3>Comandos Disponibles</h3>
|
||||
<h3>{{ 'availableCommandsTitle' | translate }}</h3>
|
||||
<div class="table-wrapper">
|
||||
<table mat-table [dataSource]="availableCommands" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Nombre </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'nameColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef> Acciones </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'actionsColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let command">
|
||||
<button mat-icon-button type="button" (click)="addCommand(command)">
|
||||
<mat-icon>add</mat-icon>
|
||||
|
@ -38,18 +38,16 @@
|
|||
</div>
|
||||
|
||||
<div class="selected-commands">
|
||||
<h3>Comandos Seleccionados</h3>
|
||||
<div class="selected-commands-list">
|
||||
<h3>{{ 'selectedCommandsTitle' | translate }}</h3>
|
||||
<div class="selected-commands-list" cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<div class="commands-container">
|
||||
<ng-container *ngFor="let command of selectedCommands; let last = last">
|
||||
<div *ngFor="let command of selectedCommands" cdkDrag>
|
||||
<div class="command-item">
|
||||
<mat-icon class="drag-handle" cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ command.name }}
|
||||
<mat-icon class="remove-icon" (click)="removeCommand(command)">close</mat-icon>
|
||||
</div>
|
||||
<ng-container *ngIf="!last">
|
||||
<mat-icon class="chevron-icon">chevron_right</mat-icon>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,6 +56,6 @@
|
|||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">Cancelar</button>
|
||||
<button mat-button (click)="onSubmit()" cdkFocusInitial> Guardar </button>
|
||||
<button mat-button (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Component, OnInit, Inject } from '@angular/core';
|
|||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-command-group',
|
||||
|
@ -55,7 +56,9 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
}
|
||||
|
||||
addCommand(command: any): void {
|
||||
this.selectedCommands.push(command);
|
||||
if (!this.selectedCommands.includes(command)) {
|
||||
this.selectedCommands.push(command);
|
||||
}
|
||||
}
|
||||
|
||||
removeCommand(command: any): void {
|
||||
|
@ -65,6 +68,10 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<any[]>): void {
|
||||
moveItemInArray(this.selectedCommands, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
const payload = {
|
||||
name: this.groupName,
|
||||
|
@ -81,6 +88,7 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
},
|
||||
error: (error) => {
|
||||
console.error('Error actualizando el grupo de comandos', error);
|
||||
this.toastService.error('Error al actualizar el grupo de comandos');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -91,6 +99,7 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
},
|
||||
error: (error) => {
|
||||
console.error('Error creando el grupo de comandos', error);
|
||||
this.toastService.error('Error al crear el grupo de comandos');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="detail-command-group-container">
|
||||
<h2>Detalles del Grupo de Comandos</h2>
|
||||
<h2>{{ 'commandGroupDetailsTitle' | translate }}</h2>
|
||||
|
||||
<!-- Indicador de carga -->
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
|
@ -9,17 +9,17 @@
|
|||
<mat-card *ngIf="!loading">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ data.name }}</mat-card-title>
|
||||
<mat-card-subtitle>Creado por: {{ data.createdBy }}</mat-card-subtitle>
|
||||
<mat-card-subtitle>{{ 'createdBy' | translate }}: {{ data.createdBy }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<p><strong>ID del Grupo:</strong> {{ data.uuid }}</p>
|
||||
<p><strong>Fecha de Creación:</strong> {{ data.createdAt | date:'short' }}</p>
|
||||
<p><strong>{{ 'groupId' | translate }}:</strong> {{ data.uuid }}</p>
|
||||
<p><strong>{{ 'creationDate' | translate }}:</strong> {{ data.createdAt | date:'short' }}</p>
|
||||
|
||||
<h3>Comandos Incluidos:</h3>
|
||||
<h3>{{ 'includedCommands' | translate }}</h3>
|
||||
<table mat-table [dataSource]="data.commands" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Nombre </th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'nameColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -37,16 +37,16 @@
|
|||
<!-- Sección para seleccionar clientes -->
|
||||
<div class="additional-section" *ngIf="showClientSelect && !loading">
|
||||
<form [formGroup]="form">
|
||||
<h4>Selecciona los clientes:</h4>
|
||||
<h4>{{ 'selectClients' | translate }}</h4>
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Clientes</mat-label>
|
||||
<mat-label>{{ 'clientsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple (selectionChange)="onClientSelectionChange($event)">
|
||||
<mat-option *ngFor="let client of clients" [value]="client.uuid">
|
||||
{{ client.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="form.get('selectedClients')?.invalid && form.get('selectedClients')?.touched">
|
||||
Debes seleccionar al menos un cliente.
|
||||
{{ 'selectAtLeastOneClient' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
@ -54,8 +54,8 @@
|
|||
|
||||
<div class="command-group-actions" *ngIf="!loading">
|
||||
<button mat-flat-button color="primary" (click)="toggleClientSelect()">
|
||||
{{ showClientSelect ? 'Ejecutar' : 'Programar Ejecución' }}
|
||||
{{ showClientSelect ? ('execute' | translate) : ('scheduleExecution' | translate) }}
|
||||
</button>
|
||||
<button mat-flat-button color="warn" (click)="close()">Cancelar</button>
|
||||
<button mat-flat-button color="warn" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,59 +1,64 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title">Administrar Tareas</h2>
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'manageTasksTitle' | translate }}</h2>
|
||||
<div class="task-button-row">
|
||||
<button mat-flat-button color="primary" (click)="openCreateTaskModal()">Añadir Tarea</button>
|
||||
<button mat-flat-button color="primary" (click)="openCreateTaskModal()" joyrideStep="addTaskStep" text="{{ 'addTaskStepText' | translate }}">
|
||||
{{ 'addTask' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
||||
<div class="search-container">
|
||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>Buscar tarea</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
||||
<mat-label>{{ 'searchTaskLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="tasks" class="mat-elevation-z8">
|
||||
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container matColumnDef="taskid">
|
||||
<th mat-header-cell *matHeaderCellDef> Id</th>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'idColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.id }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="notes">
|
||||
<th mat-header-cell *matHeaderCellDef> Info</th>
|
||||
<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> Creado por </th>
|
||||
<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> Fecha de Ejecución </th>
|
||||
<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> Estado </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.enabled ? 'Habilitado' : 'Deshabilitado' }} </td>
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'statusColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.enabled ? ('enabled' | translate) : ('disabled' | translate) }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<td mat-cell *matCellDef="let task" style="text-align: center;">
|
||||
<button mat-icon-button color="info" (click)="viewTaskDetails(task)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
|
||||
<button mat-icon-button color="primary" (click)="editTask(task)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
||||
<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>
|
||||
<button mat-icon-button color="primary" (click)="editTask(task)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteTask(task)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -63,7 +68,8 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<mat-paginator [length]="length"
|
||||
<mat-paginator joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}"
|
||||
[length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
|
|
|
@ -10,6 +10,8 @@ import { FormsModule } from '@angular/forms';
|
|||
import { MatInputModule } from '@angular/material/input';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { JoyrideModule, JoyrideService, JoyrideStepService } from 'ngx-joyride';
|
||||
describe('CommandsTaskComponent', () => {
|
||||
let component: CommandsTaskComponent;
|
||||
let fixture: ComponentFixture<CommandsTaskComponent>;
|
||||
|
@ -25,7 +27,9 @@ describe('CommandsTaskComponent', () => {
|
|||
MatPaginatorModule,
|
||||
FormsModule,
|
||||
MatInputModule,
|
||||
BrowserAnimationsModule
|
||||
BrowserAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
declarations: [CommandsTaskComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ToastrService } from 'ngx-toastr';
|
|||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-commands-task',
|
||||
|
@ -23,7 +24,8 @@ export class CommandsTaskComponent implements OnInit {
|
|||
loading: boolean = false;
|
||||
private apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService) {}
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTasks();
|
||||
|
@ -93,4 +95,20 @@ export class CommandsTaskComponent implements OnInit {
|
|||
this.itemsPerPage = event.pageSize;
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleStep',
|
||||
'addTaskStep',
|
||||
'searchStep',
|
||||
'tableStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,10 +12,16 @@
|
|||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
margin-bottom: 16px; /* Espaciado entre campos */
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
|
@ -1,108 +1,111 @@
|
|||
<h2 mat-dialog-title class="dialog-title">{{ editing ? 'Editar Tarea' : 'Crear Tarea' }}</h2>
|
||||
<h2 mat-dialog-title class="dialog-title">{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}</h2>
|
||||
|
||||
<form [formGroup]="taskForm" class="task-form">
|
||||
<mat-dialog-content>
|
||||
<mat-horizontal-stepper linear>
|
||||
<!-- Paso 1: Información y Selecciona Comandos -->
|
||||
<mat-step label="Información y Selecciona Comandos">
|
||||
<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-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Comandos</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">Este campo es obligatorio</mat-error>
|
||||
</mat-form-field>
|
||||
<h3 class="section-title">Información</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<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-form-field>
|
||||
|
||||
<div class="button-container">
|
||||
<button mat-raised-button color="primary" matStepperNext [disabled]="taskForm.get('commandGroup')?.invalid">Continuar</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
<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>
|
||||
<textarea matInput formControlName="notes" placeholder="{{ 'notesPlaceholder' | translate }}"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Paso 2: Selecciona Comandos Individuales -->
|
||||
<mat-step label="Selecciona Comandos Individuales">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Comandos Individuales (Opcional)</mat-label>
|
||||
<mat-select formControlName="extraCommands" multiple>
|
||||
<mat-option *ngFor="let command of availableIndividualCommands" [value]="command.uuid">
|
||||
{{ command.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<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>
|
||||
|
||||
<div class="button-container">
|
||||
<button mat-button matStepperPrevious>Atrás</button>
|
||||
<button mat-raised-button color="primary" matStepperNext>Continuar</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
<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-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Paso 3: Fecha de Ejecución y Hora -->
|
||||
<mat-step label="Fecha de Ejecución y Hora">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Fecha de Ejecución</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="date" placeholder="Selecciona una fecha">
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
<mat-error *ngIf="taskForm.get('date')?.invalid">Este campo es obligatorio</mat-error>
|
||||
</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>Hora de Ejecución</mat-label>
|
||||
<input matInput type="time" formControlName="time" placeholder="Selecciona una hora">
|
||||
<mat-error *ngIf="taskForm.get('time')?.invalid">Este campo es obligatorio</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>
|
||||
|
||||
<div class="button-container">
|
||||
<button mat-button matStepperPrevious>Atrás</button>
|
||||
<button mat-raised-button color="primary" matStepperNext>Continuar</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
<h3 class="section-title">{{ 'destinationSelectionSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectOrganizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Paso 4: Selecciona Unidad Organizacional, Aula y Clientes -->
|
||||
<mat-step label="Selecciona Unidad Organizacional, Aula y Clientes">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Unidad Organizacional</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">Este campo es obligatorio</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectClassroomLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
|
||||
<mat-option *ngFor="let child of selectedUnitChildren" [value]="child['@id']">
|
||||
{{ child.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona aula</mat-label>
|
||||
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
|
||||
<mat-option *ngFor="let child of selectedUnitChildren" [value]="child['@id']">
|
||||
{{ child.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectClientsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
|
||||
{{ 'selectAllClients' | translate }}
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
|
||||
{{ client.name }} ({{ client.ip }})
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Clientes</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
|
||||
Seleccionar todos
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
|
||||
{{ client.name }} ({{ client.ip }})
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="button-container">
|
||||
<button mat-raised-button color="primary" (click)="saveTask()">{{ 'buttonSave' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button mat-button matStepperPrevious>Atrás</button>
|
||||
<button mat-raised-button color="primary" (click)="saveTask()">Guardar</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
<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>
|
||||
|
||||
<div class="button-container">
|
||||
<button mat-raised-button color="primary" (click)="saveTask()">Guardar</button>
|
||||
</div>
|
||||
|
||||
</mat-horizontal-stepper>
|
||||
</mat-dialog-content>
|
||||
</form>
|
||||
|
|
|
@ -49,8 +49,6 @@ export class CreateTaskComponent implements OnInit {
|
|||
this.editing = true;
|
||||
this.loadTaskData(this.data.task);
|
||||
}
|
||||
|
||||
console.log(this.data);
|
||||
}
|
||||
|
||||
loadCommandGroups(): void {
|
||||
|
@ -101,6 +99,76 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
private collectClassrooms(unit: any): any[] {
|
||||
let classrooms = [];
|
||||
if (unit.type === 'classroom') {
|
||||
classrooms.push(unit);
|
||||
}
|
||||
if (unit.children && unit.children.length > 0) {
|
||||
for (let child of unit.children) {
|
||||
classrooms = classrooms.concat(this.collectClassrooms(child));
|
||||
}
|
||||
}
|
||||
return classrooms;
|
||||
}
|
||||
|
||||
onOrganizationalUnitChange(): void {
|
||||
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
|
||||
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
|
||||
|
||||
if (selectedUnit) {
|
||||
this.selectedUnitChildren = this.collectClassrooms(selectedUnit);
|
||||
} else {
|
||||
this.selectedUnitChildren = [];
|
||||
}
|
||||
|
||||
this.taskForm.patchValue({ selectedChild: '', selectedClients: [] });
|
||||
this.selectedClients = [];
|
||||
this.selectedClientIds.clear();
|
||||
}
|
||||
|
||||
onChildChange(): void {
|
||||
const selectedChildId = this.taskForm.get('selectedChild')?.value;
|
||||
|
||||
if (!selectedChildId) {
|
||||
this.selectedClients = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}${selectedChildId}`.replace(/([^:]\/)\/+/g, '$1');
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
(data) => {
|
||||
if (Array.isArray(data.clients) && data.clients.length > 0) {
|
||||
this.selectedClients = data.clients;
|
||||
} else {
|
||||
this.selectedClients = [];
|
||||
this.toastr.warning('El aula seleccionada no tiene clientes.');
|
||||
}
|
||||
|
||||
this.taskForm.patchValue({ selectedClients: [] });
|
||||
this.selectedClientIds.clear();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los detalles del aula seleccionada');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
const allSelected = this.areAllSelected();
|
||||
if (allSelected) {
|
||||
this.selectedClientIds.clear();
|
||||
} else {
|
||||
this.selectedClients.forEach(client => this.selectedClientIds.add(client.uuid));
|
||||
}
|
||||
this.taskForm.get('selectedClients')!.setValue(Array.from(this.selectedClientIds));
|
||||
}
|
||||
|
||||
areAllSelected(): boolean {
|
||||
return this.selectedClients.length > 0 && this.selectedClients.every(client => this.selectedClientIds.has(client.uuid));
|
||||
}
|
||||
|
||||
onCommandGroupChange(): void {
|
||||
const selectedGroupId = this.taskForm.get('commandGroup')?.value;
|
||||
this.http.get<any>(`${this.baseUrl}/command-groups/${selectedGroupId}`).subscribe(
|
||||
|
@ -113,40 +181,6 @@ export class CreateTaskComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
onOrganizationalUnitChange(): void {
|
||||
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
|
||||
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
|
||||
this.selectedUnitChildren = selectedUnit ? selectedUnit.children : [];
|
||||
}
|
||||
|
||||
onChildChange(): void {
|
||||
const selectedChildId = this.taskForm.get('selectedChild')?.value;
|
||||
this.http.get<any>(`${this.baseUrl}${selectedChildId}`).subscribe(
|
||||
(data) => {
|
||||
this.selectedClients = data.clients;
|
||||
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));
|
||||
}
|
||||
this.taskForm.get('selectedClients')!.setValue(Array.from(this.selectedClientIds));
|
||||
}
|
||||
|
||||
areAllSelected(): boolean {
|
||||
return this.selectedClients.length > 0 && this.selectedClients.every(client => this.selectedClientIds.has(client.uuid));
|
||||
}
|
||||
|
||||
saveTask(): void {
|
||||
if (this.taskForm.invalid) {
|
||||
this.toastr.error('Por favor, rellene todos los campos obligatorios');
|
||||
|
@ -156,14 +190,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;
|
||||
? formData.extraCommands.map((id: any) => `/commands/${id}`)
|
||||
: null;
|
||||
|
||||
const payload: any = {
|
||||
commandGroups: formData.commandGroup ? [`/command-groups/${formData.commandGroup}`] : null,
|
||||
dateTime: dateTime,
|
||||
notes: formData.notes || '',
|
||||
clients: Array.from(this.selectedClientIds).map((uuid: string) => `/clients/${uuid}`),
|
||||
clients: Array.from(this.selectedClientIds).map((uuid: string) => `/clients/${uuid}`),
|
||||
};
|
||||
|
||||
if (selectedCommands) {
|
||||
|
|
|
@ -1,56 +1,55 @@
|
|||
<div class="detail-task-container">
|
||||
<h2>Detalles de la Tarea</h2>
|
||||
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-subtitle>Creado por: {{ task.createdBy }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<p><strong>ID de la Tarea:</strong> {{ task.uuid }}</p>
|
||||
<p><strong>Estado:</strong> {{ task.status }}</p>
|
||||
<p><strong>Fecha de Creación:</strong> {{ task.createdAt | date: 'short' }}</p>
|
||||
<p><strong>Notas:</strong> {{ task.notes }}</p>
|
||||
|
||||
<h3>Grupos de Comandos Incluidos:</h3>
|
||||
<table mat-table [dataSource]="task.commandGroups" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Grupo de Comandos </th>
|
||||
<td mat-cell *matCellDef="let group"> {{ group.name }} </td>
|
||||
<h2>{{ 'taskDetailsTitle' | translate }}</h2>
|
||||
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-subtitle>{{ 'createdBy' | translate }}: {{ task.createdBy }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<p><strong>{{ 'taskId' | translate }}:</strong> {{ task.uuid }}</p>
|
||||
<p><strong>{{ 'status' | translate }}:</strong> {{ task.status }}</p>
|
||||
<p><strong>{{ 'creationDate' | translate }}:</strong> {{ task.createdAt | date: 'short' }}</p>
|
||||
<p><strong>{{ 'notes' | translate }}:</strong> {{ task.notes }}</p>
|
||||
|
||||
<h3>{{ 'includedCommandGroups' | translate }}</h3>
|
||||
<table mat-table [dataSource]="task.commandGroups" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'commandGroupColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let group"> {{ group.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="uuid">
|
||||
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||
<td mat-cell *matCellDef="let group"> {{ group.uuid }} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['name', 'uuid']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['name', 'uuid'];"></tr>
|
||||
</table>
|
||||
|
||||
<h3>{{ 'commandsToExecute' | translate }}</h3>
|
||||
<div *ngFor="let group of task.commandGroups">
|
||||
<p><strong>{{ 'group' | translate }}: </strong>{{ group.name }}</p>
|
||||
<table mat-table [dataSource]="group.commands" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="commandName">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'commandColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="uuid">
|
||||
|
||||
<ng-container matColumnDef="commandUuid">
|
||||
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||
<td mat-cell *matCellDef="let group"> {{ group.uuid }} </td>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.uuid }} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['name', 'uuid']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['name', 'uuid'];"></tr>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['commandName', 'commandUuid']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['commandName', 'commandUuid'];"></tr>
|
||||
</table>
|
||||
|
||||
<h3>Comandos a ejecutar:</h3>
|
||||
<div *ngFor="let group of task.commandGroups">
|
||||
<p><strong>Grupo: </strong>{{ group.name }}</p>
|
||||
<table mat-table [dataSource]="group.commands" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="commandName">
|
||||
<th mat-header-cell *matHeaderCellDef> Comando </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="commandUuid">
|
||||
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.uuid }} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['commandName', 'commandUuid']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['commandName', 'commandUuid'];"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<div class="task-actions">
|
||||
<button mat-flat-button class="cancel-button" (click)="closeDialog()">Cerrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<div class="task-actions">
|
||||
<button mat-flat-button class="cancel-button" (click)="closeDialog()">{{ 'buttonClose' | translate }}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -70,13 +70,23 @@ table {
|
|||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.mat-chip-readonly-true {
|
||||
background-color: #4CAF50 !important;
|
||||
color: white !important;
|
||||
.chip-failed {
|
||||
background-color: #f15d5d !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mat-chip-readonly-false {
|
||||
background-color: #F44336 !important;
|
||||
color: white !important;
|
||||
.chip-success {
|
||||
background-color: #32c532 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-pending {
|
||||
background-color: #bebdbd !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.chip-in-progress {
|
||||
background-color: #f5a623 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminCommandsTitle">Trazas de comandos y procedimientos</h2>
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' | translate }}</h2>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="resetFilters()">Reiniciar filtros</button>
|
||||
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="{{ 'resetFiltersStepText' | translate }}">
|
||||
{{ 'resetFilters' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-select">
|
||||
<input type="text" matInput [formControl]="clientControl" [matAutocomplete]="clientAuto" placeholder="Seleccione un cliente">
|
||||
<mat-form-field appearance="fill" class="search-select" joyrideStep="clientSelectStep" text="{{ 'clientSelectStepText' | translate }}">
|
||||
<input type="text" matInput [formControl]="clientControl" [matAutocomplete]="clientAuto" placeholder="{{ 'selectClientPlaceholder' | translate }}">
|
||||
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient" (optionSelected)="onOptionClientSelected($event.option.value)">
|
||||
<mat-option *ngFor="let client of filteredClients | async" [value]="client">
|
||||
{{ client.name }}
|
||||
|
@ -16,28 +22,57 @@
|
|||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-select">
|
||||
<input type="text" matInput [formControl]="commandControl" [matAutocomplete]="commandAuto" placeholder="Seleccione un comando">
|
||||
<mat-form-field appearance="fill" class="search-select" joyrideStep="commandSelectStep" text="{{ 'commandSelectStepText' | translate }}">
|
||||
<input type="text" matInput [formControl]="commandControl" [matAutocomplete]="commandAuto" placeholder="{{ 'selectCommandPlaceholder' | 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]="'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>
|
||||
|
||||
<!-- Indicador de carga -->
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de trazas -->
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="traces" class="mat-elevation-z8">
|
||||
<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">
|
||||
{{ column.cell(trace) }}
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'status'; else defaultCell">
|
||||
<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 === 'success' ? 'Finalizado con éxito' :
|
||||
trace.status === 'pending' ? 'Pendiente de ejecutar' :
|
||||
trace.status === 'in-progress' ? 'Ejecutando' :
|
||||
trace.status
|
||||
}}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #defaultCell>
|
||||
{{ column.cell(trace) }}
|
||||
</ng-template>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -46,7 +81,7 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container">
|
||||
<div class="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}">
|
||||
<mat-paginator [length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageIndex]="page"
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
|
|||
import { FormControl } from '@angular/forms';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
|
||||
@Component({
|
||||
selector: 'app-task-logs',
|
||||
|
@ -32,7 +33,7 @@ export class TaskLogsComponent implements OnInit {
|
|||
{
|
||||
columnDef: 'command',
|
||||
header: 'Comando',
|
||||
cell: (trace: any) => `${trace.command?.name}`
|
||||
cell: (trace: any) => `${trace.command}`
|
||||
},
|
||||
{
|
||||
columnDef: 'client',
|
||||
|
@ -44,16 +45,21 @@ export class TaskLogsComponent implements OnInit {
|
|||
header: 'Estado',
|
||||
cell: (trace: any) => `${trace.status}`
|
||||
},
|
||||
{
|
||||
columnDef: 'jobId',
|
||||
header: 'Hilo de trabajo',
|
||||
cell: (trace: any) => `${trace.jobId}`
|
||||
},
|
||||
{
|
||||
columnDef: 'executedAt',
|
||||
header: 'Programación de ejecución',
|
||||
cell: (trace: any) => `${this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
header: 'Fecha de creación',
|
||||
cell: (trace: any) => `${this.datePipe.transform(trace.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
}
|
||||
columnDef: 'finishedAt',
|
||||
header: 'Finalización',
|
||||
cell: (trace: any) => `${this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
},
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef)];
|
||||
|
||||
|
@ -63,7 +69,8 @@ export class TaskLogsComponent implements OnInit {
|
|||
filteredCommands!: Observable<any[]>;
|
||||
commandControl = new FormControl();
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
constructor(private http: HttpClient,
|
||||
private joyrideService: JoyrideService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTraces();
|
||||
|
@ -183,4 +190,20 @@ export class TaskLogsComponent implements OnInit {
|
|||
this.length = event.length;
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleStep',
|
||||
'resetFiltersStep',
|
||||
'clientSelectStep',
|
||||
'commandSelectStep',
|
||||
'tableStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminCommandsTitle">Administrar Comandos</h2>
|
||||
<div class="command-button-row">
|
||||
<button mat-flat-button color="primary" (click)="openCreateCommandModal()">Añadir Comando</button>
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'CommandsTitle' | translate }}</h2>
|
||||
<div class="command-button-row" joyrideStep="addCommandStep" text="{{ 'addCommandStepText' | translate }}">
|
||||
<button mat-flat-button color="primary" (click)="openCreateCommandModal()">{{ 'addCommand' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
||||
<div class="search-container">
|
||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre de comando</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-label>{{ 'searchCommandLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<div *ngIf="loading" class="loading-container" joyrideStep="loadingStep" text="{{ 'loadingStepText' | translate }}">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let command">
|
||||
|
@ -35,11 +39,12 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<td mat-cell *matCellDef="let client" style="text-align: center;">
|
||||
<button mat-icon-button color="info" (click)="viewDetails($event, client)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
|
||||
<button mat-icon-button color="primary" (click)="editCommand($event, client)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" (click)="deleteCommand($event, client)"><mat-icon i18n="@@deleteElementTooltip">delete</mat-icon></button>
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let command" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<button mat-icon-button color="info" (click)="executeCommand($event, command)"><mat-icon>play_arrow</mat-icon></button>
|
||||
<button mat-icon-button color="info" (click)="viewDetails($event, command)"><mat-icon>visibility</mat-icon></button>
|
||||
<button mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)"><mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" [disabled]="command.readOnly" (click)="deleteCommand($event, command)"><mat-icon>delete</mat-icon></button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -48,7 +53,7 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container">
|
||||
<div class="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}">
|
||||
<mat-paginator [length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageIndex]="page"
|
||||
|
|
|
@ -17,6 +17,8 @@ import { MatSelectModule } from '@angular/material/select';
|
|||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NgxChartsModule } from '@swimlane/ngx-charts';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { JoyrideModule } from 'ngx-joyride';
|
||||
|
||||
describe('CommandsComponent', () => {
|
||||
let component: CommandsComponent;
|
||||
|
@ -44,7 +46,9 @@ describe('CommandsComponent', () => {
|
|||
ReactiveFormsModule,
|
||||
MatSelectModule,
|
||||
NgxChartsModule,
|
||||
DatePipe
|
||||
DatePipe,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
|
|
|
@ -7,6 +7,8 @@ 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 { JoyrideService } from 'ngx-joyride';
|
||||
|
||||
@Component({
|
||||
selector: 'app-commands',
|
||||
|
@ -48,7 +50,8 @@ 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) {}
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
|
@ -74,7 +77,7 @@ export class CommandsComponent implements OnInit {
|
|||
this.dialog.open(CommandDetailComponent, {
|
||||
width: '800px',
|
||||
data: command,
|
||||
}).afterClosed().subscribe(() => this.search());
|
||||
});
|
||||
}
|
||||
|
||||
openCreateCommandModal(): void {
|
||||
|
@ -111,10 +114,38 @@ export class CommandsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
executeCommand(event: MouseEvent, command: any): void {
|
||||
this.dialog.open(ExecuteCommandComponent, {
|
||||
width: '50%',
|
||||
data: { commandData: command }
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
console.log('Comando ejecutado con éxito');
|
||||
} else {
|
||||
console.log('Ejecución de comando cancelada');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.length = event.length;
|
||||
this.search();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleStep',
|
||||
'addCommandStep',
|
||||
'searchStep',
|
||||
'tableStep',
|
||||
'actionsStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,30 +1,29 @@
|
|||
<h2 mat-dialog-title>{{ commandId ? 'Editar' : 'Crear' }} Comando</h2>
|
||||
<h2 mat-dialog-title>{{ commandId ? ('editCommandTitle' | translate) : ('createCommandTitle' | translate) }}</h2>
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="createCommandForm" (ngSubmit)="onSubmit()" class="command-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Nombre</mat-label>
|
||||
<input matInput formControlName="name" placeholder="Nombre del comando" required>
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" placeholder="{{ 'commandNamePlaceholder' | translate }}" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Script</mat-label>
|
||||
<textarea matInput formControlName="script" placeholder="Script del comando"></textarea>
|
||||
<mat-label>{{ 'scriptLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="script" placeholder="{{ 'commandScriptPlaceholder' | translate }}"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<mat-checkbox formControlName="readOnly">Solo lectura</mat-checkbox>
|
||||
<mat-checkbox formControlName="enabled">Habilitado</mat-checkbox>
|
||||
<mat-checkbox formControlName="readOnly">{{ 'readOnlyLabel' | translate }}</mat-checkbox>
|
||||
<mat-checkbox formControlName="enabled">{{ 'enabledLabel' | translate }}</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Comentarios</mat-label>
|
||||
<textarea matInput formControlName="comments" placeholder="Comentarios"></textarea>
|
||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="comments" placeholder="{{ 'commentsPlaceholder' | translate }}"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onCancel($event)">Cancelar</button>
|
||||
<button mat-button (click)="onSubmit()" cdkFocusInitial> Guardar </button>
|
||||
<button mat-button (click)="onCancel($event)">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
||||
|
|
|
@ -7,11 +7,13 @@ import { ToastrService, ToastrModule } from 'ngx-toastr';
|
|||
import { DataService } from '../data.service';
|
||||
import { CreateCommandComponent } from './create-command.component';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field'; // Import for mat-form-field
|
||||
import { MatInputModule } from '@angular/material/input'; // Import for matInput
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox'; // Import for mat-checkbox
|
||||
import { MatButtonModule } from '@angular/material/button'; // Import for mat-button
|
||||
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 { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('CreateCommandComponent', () => {
|
||||
let component: CreateCommandComponent;
|
||||
let fixture: ComponentFixture<CreateCommandComponent>;
|
||||
|
@ -20,14 +22,15 @@ describe('CreateCommandComponent', () => {
|
|||
await TestBed.configureTestingModule({
|
||||
declarations: [CreateCommandComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
BrowserAnimationsModule,
|
||||
ToastrModule.forRoot(),
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
BrowserAnimationsModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
|
|
|
@ -1,38 +1,31 @@
|
|||
<div class="modal-container">
|
||||
<h3 class="modal-title">Detalles del Comando</h3>
|
||||
<h3 class="modal-title">{{ 'commandDetailsTitle' | translate }}</h3>
|
||||
<div class="modal-content">
|
||||
<p><strong>Nombre:</strong> {{ data.name }}</p>
|
||||
<p><strong>Comentarios:</strong> {{ data.comments }}</p>
|
||||
<p><strong>Creado por:</strong> {{ data.createdBy }}</p>
|
||||
<p><strong>Fecha de Creación:</strong> {{ data.createdAt | date:'medium' }}</p>
|
||||
<p><strong>{{ 'nameLabel' | translate }}:</strong> {{ data.name }}</p>
|
||||
<p><strong>{{ 'commentsLabel' | translate }}:</strong> {{ data.comments }}</p>
|
||||
<p><strong>{{ 'createdByLabel' | translate }}:</strong> {{ data.createdBy }}</p>
|
||||
<p><strong>{{ 'creationDateLabel' | translate }}:</strong> {{ data.createdAt | date:'medium' }}</p>
|
||||
|
||||
<div class="script-display">
|
||||
<h4>Script:</h4>
|
||||
<h4>{{ 'scriptLabel' | translate }}:</h4>
|
||||
<pre>{{ data.script }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="additional-section" *ngIf="showClientSelect">
|
||||
<form [formGroup]="form">
|
||||
<h4>Selecciona los clientes:</h4>
|
||||
<h4>{{ 'selectClientsTitle' | translate }}</h4>
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Clientes</mat-label>
|
||||
<mat-label>{{ 'clientsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple (selectionChange)="onClientSelectionChange($event)">
|
||||
<mat-option *ngFor="let client of clients" [value]="client.uuid">
|
||||
{{ client.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="form.get('selectedClients')?.invalid && form.get('selectedClients')?.touched">
|
||||
Debes seleccionar al menos un cliente.
|
||||
{{ 'selectAtLeastOneClientError' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button mat-button color="primary" class="primary-button" [disabled]="false" (click)="execute()" >
|
||||
{{ showClientSelect ? 'Ejecutar' : 'Configurar ejecución' }}
|
||||
</button>
|
||||
<button mat-button (click)="cancel()">Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -38,54 +38,12 @@ export class CommandDetailComponent implements OnInit {
|
|||
this.http.get<any>(`${this.baseUrl}/clients?page=1&itemsPerPage=30`).subscribe(response => {
|
||||
this.clients = response['hydra:member'];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
if (!this.showClientSelect) {
|
||||
this.showClientSelect = true;
|
||||
} else {
|
||||
if (this.form.get('selectedClients')?.value.length > 0) {
|
||||
|
||||
const payload = {
|
||||
clients: this.form.value.selectedClients.map((uuid: any) => `/clients/${uuid}`)
|
||||
};
|
||||
|
||||
const apiUrl = `${this.baseUrl}/commands/${this.data.uuid}/execute`;
|
||||
this.http.post(apiUrl, payload).subscribe({
|
||||
next: () => {
|
||||
this.dialogRef.close();
|
||||
this.toastService.success('Command executed successfully');
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error executing command:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.dialogRef.close();
|
||||
} else {
|
||||
this.form.get('selectedClients')?.markAsTouched();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClientSelectionChange(event: any): void {
|
||||
this.canExecute = this.form.get('selectedClients')?.value.length > 0;
|
||||
}
|
||||
|
||||
onScheduleChange(event: any): void {
|
||||
this.showDatePicker = event.checked;
|
||||
if (event.checked) {
|
||||
this.form.get('scheduleDate')?.setValidators(Validators.required);
|
||||
this.form.get('scheduleTime')?.setValidators(Validators.required);
|
||||
} else {
|
||||
this.form.get('scheduleDate')?.clearValidators();
|
||||
this.form.get('scheduleTime')?.clearValidators();
|
||||
}
|
||||
this.form.get('scheduleDate')?.updateValueAndValidity();
|
||||
this.form.get('scheduleTime')?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
edit(): void {
|
||||
const dialogRef = this.dialog.open(CreateCommandComponent, {
|
||||
width: '600px',
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
.form-container {
|
||||
padding: 20px 24px;
|
||||
width: 100%; /* Asegura que el formulario ocupe el ancho completo */
|
||||
}
|
||||
|
||||
.command-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 15px 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
padding: 24px;
|
||||
width: 100%; /* Ocupar el ancho completo del modal */
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<h2 mat-dialog-title>{{ 'executeCommandTitle' | translate }}</h2>
|
||||
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="form" class="command-form">
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="unit">
|
||||
<mat-option *ngFor="let unit of units" [value]="unit.uuid">{{ unit.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'subOrganizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="childUnit">
|
||||
<mat-option *ngFor="let child of childUnits" [value]="child.uuid">{{ child.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label>{{ 'clientsLabel' | translate }}</label>
|
||||
<div *ngIf="clients.length > 0">
|
||||
<mat-checkbox *ngFor="let client of clients"
|
||||
(change)="toggleClientSelection(client.uuid)"
|
||||
[checked]="form.get('clientSelection')?.value.includes(client.uuid)">
|
||||
{{ client.name }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div *ngIf="clients.length === 0">
|
||||
<p>{{ 'noClientsAvailable' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="closeModal()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="executeCommand()" [disabled]="!form.get('clientSelection')?.value.length">{{ 'buttonExecute' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,66 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ExecuteCommandComponent } from './execute-command.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { ReactiveFormsModule, FormsModule, FormBuilder } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../data.service';
|
||||
|
||||
describe('ExecuteCommandComponent', () => {
|
||||
let component: ExecuteCommandComponent;
|
||||
let fixture: ComponentFixture<ExecuteCommandComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ExecuteCommandComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
BrowserAnimationsModule,
|
||||
MatTableModule,
|
||||
MatSelectModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
}
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ExecuteCommandComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-command',
|
||||
templateUrl: './execute-command.component.html',
|
||||
styleUrls: ['./execute-command.component.css']
|
||||
})
|
||||
export class ExecuteCommandComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
units: any[] = [];
|
||||
childUnits: any[] = [];
|
||||
clients: any[] = [];
|
||||
selectedClients: any[] = [];
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<ExecuteCommandComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private http: HttpClient,
|
||||
private fb: FormBuilder
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
unit: [null],
|
||||
childUnit: [null],
|
||||
clientSelection: [[]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadUnits();
|
||||
this.form.get('unit')?.valueChanges.subscribe(value => this.onUnitChange(value));
|
||||
this.form.get('childUnit')?.valueChanges.subscribe(value => this.onChildUnitChange(value));
|
||||
}
|
||||
|
||||
loadUnits(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
|
||||
response => {
|
||||
this.units = response['hydra:member'].filter((unit: { type: string; }) => unit.type === 'organizational-unit');
|
||||
},
|
||||
error => console.error('Error fetching organizational units:', error)
|
||||
);
|
||||
}
|
||||
|
||||
onUnitChange(unitId: string): void {
|
||||
const unit = this.units.find(unit => unit.uuid === unitId);
|
||||
this.childUnits = unit ? this.getAllChildren(unit) : [];
|
||||
this.clients = [];
|
||||
this.form.patchValue({ childUnit: null, clientSelection: [] });
|
||||
}
|
||||
|
||||
getAllChildren(unit: any): any[] {
|
||||
let allChildren = [];
|
||||
if (unit.children && unit.children.length > 0) {
|
||||
for (const child of unit.children) {
|
||||
allChildren.push(child);
|
||||
allChildren = allChildren.concat(this.getAllChildren(child));
|
||||
}
|
||||
}
|
||||
return allChildren;
|
||||
}
|
||||
|
||||
onChildUnitChange(childUnitId: string): void {
|
||||
const childUnit = this.childUnits.find(unit => unit.uuid === childUnitId);
|
||||
this.clients = childUnit && childUnit.clients ? childUnit.clients : [];
|
||||
this.form.patchValue({ clientSelection: [] });
|
||||
}
|
||||
|
||||
executeCommand(): void {
|
||||
const payload = {
|
||||
clients: ['/clients/'+this.form.get('clientSelection')?.value]
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/commands/${this.data.commandData.uuid}/execute`, payload)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
console.log('Comando ejecutado con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al ejecutar el comando:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeModal(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
toggleClientSelection(clientId: string): void {
|
||||
const selectedClients = this.form.get('clientSelection')?.value;
|
||||
if (selectedClients.includes(clientId)) {
|
||||
this.form.get('clientSelection')?.setValue(selectedClients.filter((id: string) => id !== clientId));
|
||||
} else {
|
||||
this.form.get('clientSelection')?.setValue([...selectedClients, clientId]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
.groupLists-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
@ -11,130 +10,23 @@
|
|||
margin: 10px;
|
||||
}
|
||||
|
||||
.search-container mat-form-field {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex-grow: 1;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.unidad-card, .elements-card {
|
||||
flex: 1 1 45%;
|
||||
background-color: #fafafa;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.element-content {
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.details-card, .classroom-view {
|
||||
flex: 1 1 25%;
|
||||
}
|
||||
|
||||
mat-card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.title-with-breadcrumb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
mat-card-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
||||
mat-card-subtitle a {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: #929292;
|
||||
}
|
||||
|
||||
mat-card-subtitle a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.groups-button-row {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-content mat-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.clickable-item:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.actions mat-icon {
|
||||
cursor: pointer;
|
||||
margin-left: 16px;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.actions mat-icon:hover {
|
||||
color: #212121;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.classroomBtn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -144,7 +36,7 @@ mat-spinner {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header mat-form-field {
|
||||
|
@ -162,102 +54,169 @@ mat-spinner {
|
|||
width: 300px;
|
||||
}
|
||||
|
||||
.saved-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.results {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.results-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
height: 250px;
|
||||
height: auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
margin: 10px 10px;
|
||||
}
|
||||
|
||||
.result-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
.result-card.small-card {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 10px;
|
||||
margin: 10px 10px 10px 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.result-checkbox {
|
||||
float: right;
|
||||
margin: 0;
|
||||
.result-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
background-color: #fff;
|
||||
max-width: 300px;
|
||||
margin: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
&.card-og-live {
|
||||
background-color: yellow; /* Verde */
|
||||
color: white;
|
||||
}
|
||||
&.card-busy {
|
||||
background-color: indianred; /* Naranja */
|
||||
color: white;
|
||||
}
|
||||
&.card-windows {
|
||||
background-color: cornflowerblue; /* Azul */
|
||||
color: white;
|
||||
}
|
||||
&.card-linux {
|
||||
background-color: mediumpurple; /* Púrpura */
|
||||
color: white;
|
||||
}
|
||||
&.card-macos {
|
||||
background-color: cornflowerblue; /* Rojo */
|
||||
color: white;
|
||||
}
|
||||
&.card-off {
|
||||
background-color: #9e9e9e; /* Gris */
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.result-container:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
margin: 8px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
padding-top: 10px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.result-type {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.result-ip, .result-mac, .result-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 0.9rem;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.result-type,
|
||||
.result-ip,
|
||||
.result-mac,
|
||||
.result-status,
|
||||
.result-internal-units,
|
||||
.result-clients {
|
||||
font-size: 0.9rem;
|
||||
color: #007bff;
|
||||
margin: 5px 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
.result-checkbox {
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-og-live {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-busy {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-windows {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-linux {
|
||||
background-color: #9c27b0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-macos {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-off {
|
||||
background-color: #9e9e9e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin-bottom: 20px;
|
||||
margin-right: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.red-card {
|
||||
background-color: #f35f53;
|
||||
background-color: #f35f53;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.green-card {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.view-mode-buttons {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.view-mode-buttons button.active {
|
||||
|
@ -265,16 +224,12 @@ mat-card {
|
|||
color: #3f51b5;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.result-card-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
|
@ -283,7 +238,7 @@ mat-card {
|
|||
}
|
||||
|
||||
.result-card-list .result-title {
|
||||
font-size: 14px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
@ -292,13 +247,87 @@ mat-card {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%; /* Ajusta según el contenedor padre */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-results p {
|
||||
font-size: 1.5rem; /* Tamaño de fuente más grande */
|
||||
font-weight: bold; /* Negrita para mejor visibilidad */
|
||||
color: #555; /* Cambia el color según tu diseño */
|
||||
}
|
||||
|
||||
.result-card-list p {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.result-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilo general para la tarjeta */
|
||||
.result-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.result-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Centrar contenido para clientes */
|
||||
.centered-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.client-info p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.client-info .client-name {
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.client-info .client-text {
|
||||
font-size: 0.8rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.client-info strong {
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.client-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<h2 class="title" i18n="@@searchTitle">Búsqueda avanzada</h2>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<mat-form-field>
|
||||
|
@ -11,16 +10,18 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
||||
<div class="view-mode-buttons">
|
||||
<div class="view-mode-buttons" joyrideStep="viewModeStep" text="Elige cómo quieres ver los resultados: en cuadrícula o en lista.">
|
||||
<button mat-button (click)="changeViewMode('grid')" [class.active]="viewMode === 'grid'">
|
||||
<mat-icon>grid_view</mat-icon> Cuadrícula
|
||||
</button>
|
||||
<button mat-button (click)="changeViewMode('list')" [class.active]="viewMode === 'list'">
|
||||
<mat-icon>list</mat-icon> Lista
|
||||
</button>
|
||||
<button mat-button (click)="toggleSelectAll()">
|
||||
<mat-icon>checkbox</mat-icon> Seleccionar/Deseleccionar Todos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
|
@ -32,35 +33,122 @@
|
|||
<mat-option value="client" i18n="@@clientsOption">Clientes</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label i18n="@@nameLabel">Nombre</mat-label>
|
||||
<input matInput placeholder="Unidad organizativa" (input)="applyFilter()" [(ngModel)]="filterName" i18n-placeholder="@@namePlaceholder">
|
||||
<input matInput placeholder="Unidad organizativa" (input)="applyFilter()" [(ngModel)]="filterName"
|
||||
i18n-placeholder="@@namePlaceholder">
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container *ngIf="selectedFilter1 === 'ou'">
|
||||
|
||||
<mat-form-field [disabled]="selectedFilter1 === 'ou'">
|
||||
<mat-label i18n="@@unitTypeLabel">Tipo de unidad</mat-label>
|
||||
<mat-select [(value)]="selectedFilter2" (selectionChange)="applyFilter()">
|
||||
<mat-option value="organizational-unit" i18n="@@organizationalUnitOption">Unidad organizativa</mat-option>
|
||||
<mat-option value="classroom-group" i18n="@@classroomsGroupOption">Grupos de aulas</mat-option>
|
||||
<mat-option value="classroom" i18n="@@classroomOption">Aulas</mat-option>
|
||||
<mat-option value="client-group" i18n="@@clientGroupOption">Grupos de clientes</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label i18n="@@floorLabel" class="temp_filter">Planta</mat-label>
|
||||
<mat-select [(value)]="selectedFilter1">
|
||||
<mat-option value="none" i18n="@@noneOption">Ninguno</mat-option>
|
||||
<mat-option value="option1" i18n="@@option1">Planta 1</mat-option>
|
||||
<mat-option value="option2" i18n="@@option2">Planta 2</mat-option>
|
||||
<mat-option value="option3" i18n="@@option3">Planta 3</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<!-- FILTROS CLIENTES -->
|
||||
|
||||
<ng-container *ngIf="selectedFilter1 === 'client'">
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label i18n="@@selectAnotherOptionLabel" class="temp_filter">Sistema Operativo</mat-label>
|
||||
<mat-select multiple [(value)]="selectedFilterOS">
|
||||
<mat-option value="none" i18n="@@noneOption">Ninguno</mat-option>
|
||||
<mat-option value="Windows 10 Education 1803 64 bits">Windows 10 Education 1803 64 bits</mat-option>
|
||||
<mat-option value="Ubuntu 18.04.1 LTS 64 bits">Ubuntu 18.04.1 LTS 64 bits</mat-option>
|
||||
<mat-option value="Ubuntu 16.04.4 LTS 64 bits">Ubuntu 16.04.4 LTS 64 bits</mat-option>
|
||||
<mat-option value="DATA">RESTO DE OPCIONES TBI</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<button mat-raised-button color="primary" (click)="toggleSelectAll()">Seleccionar/Deseleccionar Todos</button>
|
||||
<button mat-raised-button color="primary" (click)="saveFilters()" i18n="@@saveFiltersButton">Guardar Filtros</button>
|
||||
<button mat-raised-button color="accent" (click)="sendActions()" i18n="@@sendFiltersButton" [disabled]="selectedElements.length === 0">Enviar Acción</button>
|
||||
<button mat-flat-button color="primary" [disabled]="selectedElements.length === 0" (click)="onPxeBootFile()">Añadir fichero PXE</button>
|
||||
<mat-form-field>
|
||||
<mat-label i18n="@@selectStateLabel" class="temp_filter">Estado</mat-label>
|
||||
<mat-select multiple [(value)]="selectedFilterStatus">
|
||||
<mat-option value="off" i18n="@@offOption">off</mat-option>
|
||||
<mat-option value="initializing" i18n="@@initializingOption">initializing</mat-option>
|
||||
<mat-option value="oglive" i18n="@@ogliveOption">oglive</mat-option>
|
||||
<mat-option value="busy" i18n="@@busyOption">busy</mat-option>
|
||||
<mat-option value="linux" i18n="@@linuxOption">linux</mat-option>
|
||||
<mat-option value="linux_session" i18n="@@linuxSessionOption">linux_session</mat-option>
|
||||
<mat-option value="macos" i18n="@@macosOption">macos</mat-option>
|
||||
<mat-option value="windows" i18n="@@windowsOption">windows</mat-option>
|
||||
<mat-option value="windows_session" i18n="@@windowsSessionOption">windows_session</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label class="temp_filter">IP</mat-label>
|
||||
<input matInput placeholder="Dírección IP" (input)="applyFilter()" i18n [(ngModel)]="filterIP">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label class="temp_filter">MAC</mat-label>
|
||||
<input matInput placeholder="Dírección IP" (input)="applyFilter()" i18n [(ngModel)]="filterMAC">
|
||||
</mat-form-field>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<div class="button-group">
|
||||
<button mat-raised-button color="primary" (click)="saveFilters()" i18n="@@saveFiltersButton" joyrideStep="saveFiltersStep" text="Guarda tus filtros seleccionados para usarlos en el futuro.">Guardar Filtros</button>
|
||||
<button mat-raised-button color="accent" (click)="sendActions()" i18n="@@sendFiltersButton" [disabled]="selectedElements.length === 0" joyrideStep="sendActionStep" text="Envía una acción a los elementos seleccionados.">Enviar Acción</button>
|
||||
<button mat-flat-button color="primary" [disabled]="selectedElements.length === 0" (click)="onPxeBootFile()" joyrideStep="addPxeStep" text="Añade un archivo PXE a los elementos seleccionados.">Añadir fichero PXE</button>
|
||||
<button mat-raised-button color="primary" [matMenuTriggerFor]="menu" [disabled]="selectedFilter1 === 'ou'">
|
||||
Asistentes
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item [disabled]="selectedElements.length > 1 || !selectedElements.length" (click)="onCommandSelect('partition')">Asistente de particionado</button>
|
||||
<button mat-menu-item [disabled]="selectedElements.length > 1 || !selectedElements.length" (click)="onCommandSelect('create-image')">Crear una imagen</button>
|
||||
<button mat-menu-item [disabled]="selectedElements.length > 1 || !selectedElements.length" (click)="onCommandSelect('deploy-image')">Desplegar una imagen</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results">
|
||||
<ng-container *ngIf="filteredResults && filteredResults.length > 0; else noResults">
|
||||
<ng-container *ngIf="viewMode === 'grid'">
|
||||
<mat-grid-list cols="4" rowHeight="1:1">
|
||||
<mat-grid-list cols="7" rowHeight="1:1">
|
||||
<mat-grid-tile *ngFor="let result of filteredResults">
|
||||
<mat-card class="result-card">
|
||||
<mat-checkbox [checked]="isSelected(result.name)" (change)="onCheckboxChange($event, result.name, result['@id'])" class="result-checkbox"></mat-checkbox>
|
||||
<mat-card-title class="result-title">{{ result.name }}</mat-card-title>
|
||||
<mat-card-content class="result-content">
|
||||
<p class="result-type">{{ result.type !== 'client' ? result.type : '' }}</p>
|
||||
<p class="result-ip" *ngIf="result.type === 'client'">{{ result.ip }}</p>
|
||||
<p class="result-mac" *ngIf="result.type === 'client'">{{ result.mac }}</p>
|
||||
<p class="result-status" *ngIf="result.type === 'client'">{{ result.status }}</p>
|
||||
<p *ngIf="result.type !== 'client'" i18n="@@internalUnits" class="result-internal-units">Unidades internas: {{ result.children.length }}</p>
|
||||
<p *ngIf="result.type !== 'client'" i18n="@@clients" class="result-clients">Clientes: {{ result.clients.length }}</p>
|
||||
<mat-card class="result-card small-card" [ngClass]="{
|
||||
'card-og-live': result.status === 'og-live',
|
||||
'card-busy': result.status === 'busy',
|
||||
'card-windows': result.status === 'windows' || result.status === 'windows-session',
|
||||
'card-linux': result.status === 'linux' || result.status === 'linux-session',
|
||||
'card-macos': result.status === 'macos',
|
||||
'card-off': result.status === 'off'
|
||||
}">
|
||||
<mat-checkbox
|
||||
[(ngModel)]="result.selected"
|
||||
(change)="onCheckboxChange($event, result.name, result['@id'])"
|
||||
class="result-checkbox">
|
||||
</mat-checkbox>
|
||||
<mat-card-title *ngIf="result.type !== 'client'" class="result-title">{{ result.name }}</mat-card-title>
|
||||
<mat-card-content *ngIf="result.type === 'client'" class="result-content centered-content" >
|
||||
<div class="client-info">
|
||||
<p class="client-name">{{ result.name }}</p>
|
||||
<p class="client-text">{{ result.ip }}</p>
|
||||
<p class="client-text"> {{ result.mac }}</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-content *ngIf="result.type !== 'client'" class="result-content">
|
||||
<p i18n="@@internalUnits" class="result-internal-units">Unidades internas: {{ result.children.length }}</p>
|
||||
<p i18n="@@clients" class="result-clients">Clientes: {{ result.clients.length }}</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</mat-grid-tile>
|
||||
|
@ -70,27 +158,35 @@
|
|||
<ng-container *ngIf="viewMode === 'list'">
|
||||
<div class="result-list" *ngFor="let result of filteredResults">
|
||||
<mat-card class="result-card-list">
|
||||
<mat-checkbox [checked]="isSelected(result.name)" (change)="onCheckboxChange($event, result.name, result['@id'])" class="result-checkbox"></mat-checkbox>
|
||||
<mat-checkbox [(ngModel)]="result.selected" (change)="onCheckboxChange($event, result.name, result['@id'])" class="result-checkbox"></mat-checkbox>
|
||||
<mat-card-title class="result-title">{{ result.name }}</mat-card-title>
|
||||
<mat-card-content class="result-content">
|
||||
<p class="result-type">{{ result.type !== 'client' ? result.type : '' }}</p>
|
||||
<p class="result-ip" *ngIf="result.type === 'client'">{{ result.ip }}</p>
|
||||
<p class="result-mac" *ngIf="result.type === 'client'">{{ result.mac }}</p>
|
||||
<p class="result-status" *ngIf="result.type === 'client'">{{ result.status }}</p>
|
||||
<p *ngIf="result.type !== 'client'" i18n="@@internalUnits" class="result-internal-units">
|
||||
Unidades internas: {{ result.children.length }}
|
||||
</p>
|
||||
<p *ngIf="result.type !== 'client'" i18n="@@clients" class="result-clients">
|
||||
Clientes: {{ result.clients.length }}
|
||||
</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="paginator-container">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions" (page)="onPageChange($event)">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page"
|
||||
[pageSizeOptions]="pageSizeOptions" (page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #noResults>
|
||||
<p i18n="@@noResultsMessage">No hay resultados para mostrar.</p>
|
||||
<div class="no-results">
|
||||
<p i18n="@@noResultsMessage">No hay resultados para mostrar.</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,7 @@ import { Router } from '@angular/router';
|
|||
import {
|
||||
CreatePxeBootFileComponent
|
||||
} from "../../../ogboot/pxe-boot-files/create-pxeBootFile/create-pxe-boot-file/create-pxe-boot-file.component";
|
||||
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
|
||||
@Component({
|
||||
selector: 'app-advanced-search',
|
||||
|
@ -69,7 +69,8 @@ export class AdvancedSearchComponent {
|
|||
private toastService: ToastrService,
|
||||
private _bottomSheet: MatBottomSheet,
|
||||
private http: HttpClient,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private joyrideService: JoyrideService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -388,29 +389,37 @@ export class AdvancedSearchComponent {
|
|||
|
||||
}
|
||||
|
||||
|
||||
onCheckboxChange(event: any, name: string, uuid: string) {
|
||||
if (event.checked) {
|
||||
this.selectedElements.push(uuid);
|
||||
if (!this.selectedElements.includes(uuid)) {
|
||||
this.selectedElements.push(uuid);
|
||||
}
|
||||
} else {
|
||||
const index = this.selectedElements.indexOf(name);
|
||||
const index = this.selectedElements.indexOf(uuid);
|
||||
if (index > -1) {
|
||||
this.selectedElements.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.isAllSelected = this.selectedElements.length === this.filteredResults.length;
|
||||
|
||||
this.isAllSelected = this.filteredResults.every(result =>
|
||||
this.selectedElements.includes(result['@id'])
|
||||
);
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
|
||||
|
||||
if (this.isAllSelected) {
|
||||
this.selectedElements = this.filteredResults.map(result => result['@id']);
|
||||
} else {
|
||||
this.selectedElements = [];
|
||||
}
|
||||
|
||||
this.filteredResults.forEach(result => {
|
||||
result.selected = this.isAllSelected;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
isSelected(name: string): boolean {
|
||||
return this.selectedElements.includes(name);
|
||||
|
@ -428,6 +437,42 @@ export class AdvancedSearchComponent {
|
|||
});
|
||||
}
|
||||
|
||||
onCommandSelect(action: any): void {
|
||||
if (action === 'partition') {
|
||||
this.openPartitionAssistant();
|
||||
}
|
||||
|
||||
if (action === 'create-image') {
|
||||
this.openCreateImageAssistant();
|
||||
}
|
||||
|
||||
if (action === 'deploy-image') {
|
||||
this.openDeployImageAssistant();
|
||||
}
|
||||
}
|
||||
|
||||
openPartitionAssistant(): void {
|
||||
const client = this.selectedElements[0];
|
||||
console.log(client)
|
||||
this.router.navigate([`${client}/partition-assistant`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
openCreateImageAssistant(): void {
|
||||
const client = this.selectedElements[0];
|
||||
this.router.navigate([`${client}/create-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
openDeployImageAssistant(): void {
|
||||
const client = this.selectedElements[0];
|
||||
this.router.navigate([`${client}/deploy-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
onDobleClick(event: MouseEvent, data: any, type: string): void {
|
||||
if (type === 'client') {
|
||||
this.router.navigate(['client', data]);
|
||||
|
@ -436,4 +481,22 @@ export class AdvancedSearchComponent {
|
|||
}
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'title2Step',
|
||||
'filterSelectionStep',
|
||||
'viewModeStep',
|
||||
'filtersStep',
|
||||
'selectAllStep',
|
||||
'saveFiltersStep',
|
||||
'sendActionStep',
|
||||
'addPxeStep',
|
||||
'resultsStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -144,6 +144,13 @@
|
|||
gap: 20px;
|
||||
}
|
||||
|
||||
.client-button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between; /* Distribuye el espacio entre los gráficos */
|
||||
gap: 20px; /* Añade espacio entre los gráficos */
|
||||
}
|
||||
|
||||
.buttons-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -205,3 +212,37 @@
|
|||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.circular-chart {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.circle-bg {
|
||||
fill: none;
|
||||
stroke: #f0f0f0;
|
||||
stroke-width: 3.8;
|
||||
}
|
||||
|
||||
.circle {
|
||||
fill: none;
|
||||
stroke-width: 3.8;
|
||||
stroke-linecap: round;
|
||||
animation: progress 1s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Define colores distintos para cada partición */
|
||||
.partition-0 { stroke: #00bfa5; } /* Ejemplo: verde */
|
||||
.partition-1 { stroke: #ff6f61; } /* Ejemplo: rojo */
|
||||
.partition-2 { stroke: #ffb400; } /* Ejemplo: amarillo */
|
||||
.partition-3 { stroke: #3498db; } /* Ejemplo: azul */
|
||||
|
||||
/* Texto en el centro del gráfico */
|
||||
.percentage {
|
||||
fill: #333;
|
||||
font-size: 0.7rem;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,89 +1,82 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Datos de cliente</h2>
|
||||
<div class="calendar-button-row">
|
||||
<button mat-flat-button color="primary" [matMenuTriggerFor]="commandMenu">Comandos</button>
|
||||
|
||||
<h2 class="title">{{ 'clientDetailsTitle' | translate }}</h2>
|
||||
<div class="client-button-row">
|
||||
<button mat-flat-button color="primary" (click)="onEditClick($event, clientData.uuid)" i18n="@@editImage">Editar</button>
|
||||
<button mat-flat-button color="primary" [matMenuTriggerFor]="commandMenu">{{ 'commandsButton' | translate }}</button>
|
||||
</div>
|
||||
<mat-menu #commandMenu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of commands" (click)="onCommandSelect(command)">
|
||||
<button mat-menu-item *ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
|
||||
{{ command.name }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
<mat-tab-group dynamicHeight>
|
||||
<mat-tab label="Datos generales">
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading" class="client-info">
|
||||
<div class="info-section">
|
||||
<mat-tab-group dynamicHeight>
|
||||
<mat-tab label="Datos generales">
|
||||
<div *ngIf="!loading" class="client-info">
|
||||
<div class="info-section">
|
||||
<div class="two-column-table">
|
||||
<div class="table-row" *ngFor="let clientData of generalData">
|
||||
<div class="column property">{{ clientData?.property }}</div>
|
||||
<div class="column value">{{ clientData?.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="Propiedades de red">
|
||||
<div class="two-column-table">
|
||||
<div class="table-row" *ngFor="let clientData of networkData">
|
||||
<div class="column property">{{ clientData?.property }}</div>
|
||||
<div class="column value">{{ clientData?.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image" >
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<mat-chip color="primary" >
|
||||
{{ (image.size / 1024).toFixed(2) }} GB
|
||||
</mat-chip>
|
||||
</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="charts-wrapper">
|
||||
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
|
||||
<div class="charts-row">
|
||||
<div *ngFor="let disk of diskUsageData" class="disk-usage">
|
||||
<h3>Disco {{ disk.diskNumber }}</h3>
|
||||
<div class="chart">
|
||||
<svg viewBox="0 0 36 36" class="circular-chart green">
|
||||
<path class="circle-bg"
|
||||
d="M18 2.0845
|
||||
a 15.9155 15.9155 0 0 1 0 31.831
|
||||
a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||
<path class="circle"
|
||||
[attr.stroke-dasharray]="disk.percentage + ', 100'"
|
||||
d="M18 2.0845
|
||||
a 15.9155 15.9155 0 0 1 0 31.831
|
||||
a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||
<text x="18" y="20.35" class="percentage">{{ disk.percentage }}%</text>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Usado: {{ disk.used }} GB ({{ disk.percentage }}%)</p>
|
||||
<p>Total: {{ disk.total }} GB</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="Discos/Particiones">
|
||||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image" >
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<mat-chip color="primary" >
|
||||
{{ (image.size / 1024).toFixed(2) }} GB
|
||||
</mat-chip>
|
||||
</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="charts-wrapper">
|
||||
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
|
||||
<div class="charts-row">
|
||||
<div *ngFor="let disk of chartDisk" class="disk-usage">
|
||||
<ngx-charts-pie-chart
|
||||
[view]="view"
|
||||
[results]="disk.chartData"
|
||||
[legend]="showLegend">
|
||||
</ngx-charts-pie-chart>
|
||||
|
||||
<h3>Disco {{ disk.diskNumber }}</h3>
|
||||
<p>Usado: {{ (disk.used).toFixed(2) }} GB ({{ disk.percentage }}%)</p>
|
||||
<p>Total: {{ disk.total }} GB</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
|
|
@ -4,6 +4,8 @@ import {DatePipe} from "@angular/common";
|
|||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {PartitionAssistantComponent} from "./partition-assistant/partition-assistant.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {Router} from "@angular/router";
|
||||
import {EditClientComponent} from "../../shared/clients/edit-client/edit-client.component";
|
||||
|
||||
interface ClientInfo {
|
||||
property: string;
|
||||
|
@ -30,6 +32,24 @@ export class ClientMainViewComponent implements OnInit {
|
|||
diskUsageData: any[] = [];
|
||||
partitions: any[] = [];
|
||||
commands: any[] = [];
|
||||
chartDisk: any[] = [];
|
||||
view: [number, number] = [600, 300];
|
||||
showLegend: boolean = true;
|
||||
|
||||
arrayCommands: any[] = [
|
||||
{name: 'Enceder', slug: 'power-on'},
|
||||
{name: 'Apagar', slug: 'power-off'},
|
||||
{name: 'Reiniciar', slug: 'reboot'},
|
||||
{name: 'Iniciar Sesión', slug: 'login'},
|
||||
{name: 'Crear Image', slug: 'create-image'},
|
||||
{name: 'Deploy Image', slug: 'deploy-image'},
|
||||
{name: 'Eliminar Imagen Cache', slug: 'delete-image-cache'},
|
||||
{name: 'Particionar y Formatear', slug: 'partition'},
|
||||
{name: 'Inventario Software', slug: 'software-inventory'},
|
||||
{name: 'Inventario Hardware', slug: 'hardware-inventory'},
|
||||
{name: 'Ejecutar script', slug: 'run-script'},
|
||||
];
|
||||
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
columns = [
|
||||
{
|
||||
|
@ -57,12 +77,21 @@ export class ClientMainViewComponent implements OnInit {
|
|||
header: 'Uso',
|
||||
cell: (partition: any) => `${partition.memoryUsage} %`
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => `${partition.operativeSystem?.name}`
|
||||
},
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef)];
|
||||
isDiskUsageEmpty: boolean = true;
|
||||
loading: boolean = true;
|
||||
|
||||
constructor(private http: HttpClient, private dialog: MatDialog) {
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private dialog: MatDialog,
|
||||
private router: Router
|
||||
) {
|
||||
const url = window.location.href;
|
||||
const segments = url.split('/');
|
||||
this.clientUuid = segments[segments.length - 1];
|
||||
|
@ -95,22 +124,23 @@ export class ClientMainViewComponent implements OnInit {
|
|||
this.networkData = [
|
||||
{ property: 'Remote Pc', value: this.clientData.remotePc || '' },
|
||||
{ property: 'Subred', value: this.clientData?.subnet || '' },
|
||||
{ property: 'OGlive', value: '' },
|
||||
{ property: 'OGlive', value: this.clientData?.ogLive?.name || '' },
|
||||
{ property: 'Autoexec', value: '' },
|
||||
{ property: 'Repositorio', value: '' },
|
||||
{ property: 'Repositorio', value: this.clientData?.repository?.name || '' },
|
||||
{ property: 'Validación', value: this.clientData?.organizationalUnit?.networkSettings?.validation || '' },
|
||||
{ property: 'Pxe', value: this.clientData?.template.name || '' },
|
||||
{ property: 'Pxe', value: this.clientData?.template?.name || '' },
|
||||
{ property: 'Creado por', value: this.clientData?.createdBy || '' }
|
||||
];
|
||||
}
|
||||
|
||||
calculateDiskUsage() {
|
||||
const diskUsageMap = new Map<number, { total: number, used: number }>();
|
||||
const diskUsageMap = new Map<number, { total: number, used: number, partitions: any[] }>();
|
||||
|
||||
this.partitions.forEach((partition: any) => {
|
||||
const diskNumber = partition.diskNumber;
|
||||
|
||||
if (!diskUsageMap.has(diskNumber)) {
|
||||
diskUsageMap.set(diskNumber, { total: 0, used: 0 });
|
||||
diskUsageMap.set(diskNumber, { total: 0, used: 0, partitions: [] });
|
||||
}
|
||||
|
||||
const diskData = diskUsageMap.get(diskNumber);
|
||||
|
@ -118,17 +148,53 @@ export class ClientMainViewComponent implements OnInit {
|
|||
if (partition.partitionNumber === 0) {
|
||||
diskData!.total = Number((partition.size / 1024).toFixed(2));
|
||||
} else {
|
||||
diskData!.used += Number(((partition.size * (partition.memoryUsage / 100))/ 1024).toFixed(2));
|
||||
diskData!.used += Number((partition.size / 1024).toFixed(2));
|
||||
diskData!.partitions.push(partition);
|
||||
}
|
||||
});
|
||||
|
||||
this.diskUsageData = Array.from(diskUsageMap.entries()).map(([diskNumber, { total, used }]) => {
|
||||
const percentage = total > 0 ? Math.round((used / total) * 100) : 0;
|
||||
return { diskNumber, total, used, percentage };
|
||||
this.chartDisk = Array.from(diskUsageMap.entries()).map(([diskNumber, { total, used, partitions }]) => {
|
||||
const partitionData = partitions.map(partition => ({
|
||||
name: `Partición ${partition.partitionNumber}`,
|
||||
value: Number((partition.size / 1024).toFixed(2))
|
||||
}));
|
||||
|
||||
const freeSpace = total - used;
|
||||
if (freeSpace > 0) {
|
||||
partitionData.push({
|
||||
name: 'Espacio libre',
|
||||
value: Number(freeSpace.toFixed(2))
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
diskNumber,
|
||||
chartData: partitionData,
|
||||
total,
|
||||
used,
|
||||
percentage: total > 0 ? Math.round((used / total) * 100) : 0
|
||||
};
|
||||
});
|
||||
|
||||
this.diskUsageData = this.chartDisk;
|
||||
this.isDiskUsageEmpty = this.diskUsageData.length === 0;
|
||||
}
|
||||
|
||||
onEditClick(event: MouseEvent, uuid: string): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' } );
|
||||
dialogRef.afterClosed().subscribe();
|
||||
}
|
||||
|
||||
getStrokeOffset(partitions: any[], index: number): number {
|
||||
const totalSize = partitions.reduce((acc, part) => acc + (part.size / 1024), 0);
|
||||
|
||||
const offset = partitions.slice(0, index).reduce((acc, part) => acc + (part.size / 1024), 0);
|
||||
console.log(offset, totalSize)
|
||||
|
||||
return totalSize > 0 ? (offset / totalSize) : 0;
|
||||
}
|
||||
|
||||
loadPartitions(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}`).subscribe({
|
||||
next: data => {
|
||||
|
@ -153,20 +219,35 @@ export class ClientMainViewComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
onCommandSelect(command: any): void {
|
||||
if (command.name === 'Particionar y Formatear') {
|
||||
this.openPartitionDialog();
|
||||
onCommandSelect(action: any): void {
|
||||
if (action === 'partition') {
|
||||
this.openPartitionAssistant();
|
||||
}
|
||||
|
||||
if (action === 'create-image') {
|
||||
this.openCreateImageAssistant();
|
||||
}
|
||||
|
||||
if (action === 'deploy-image') {
|
||||
this.openDeployImageAssistant();
|
||||
}
|
||||
}
|
||||
|
||||
openPartitionDialog(): void {
|
||||
const dialogRef = this.dialog.open(PartitionAssistantComponent, {
|
||||
width: '1000px',
|
||||
data: this.clientData['@id']
|
||||
openPartitionAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
console.log('El diálogo se cerró', result);
|
||||
openCreateImageAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/create-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
openDeployImageAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
.title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.calendar-button-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card.unidad-card {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@subnetsTitle">Crear Imagen desde {{ clientName }}</h2>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" (click)="save()">Guardar y ejecutar</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Nombre canónico</mat-label>
|
||||
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione imagen creada previamente</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage">
|
||||
<mat-option>--</mat-option>
|
||||
<mat-option *ngFor="let image of images" [value]="image['@id']">{{ image.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{{ column.cell(image) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
|
@ -0,0 +1,79 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CreateImageComponent } from './create-image.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { of } from 'rxjs';
|
||||
import { DataService } from '../../client-tab-view/data.service';
|
||||
|
||||
describe('CreateImageComponent', () => {
|
||||
let component: CreateImageComponent;
|
||||
let fixture: ComponentFixture<CreateImageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CreateImageComponent,
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
MatTabsModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
BrowserAnimationsModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
snapshot: {
|
||||
paramMap: {
|
||||
get: (key: string) => 'valorSimulado'
|
||||
}
|
||||
},
|
||||
params: of({ id: 'valorSimulado' })
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreateImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,171 @@
|
|||
import {Component, EventEmitter, Output} from '@angular/core';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {MatDivider} from "@angular/material/divider";
|
||||
import {NgForOf, NgIf} from "@angular/common";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {
|
||||
MatCell, MatCellDef,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef,
|
||||
MatTable,
|
||||
MatTableDataSource
|
||||
} from "@angular/material/table";
|
||||
import {MatChip} from "@angular/material/chips";
|
||||
import {MatCheckbox} from "@angular/material/checkbox";
|
||||
import {SelectionModel} from "@angular/cdk/collections";
|
||||
import {MatRadioButton, MatRadioGroup} from "@angular/material/radio";
|
||||
import {MatFormField, MatLabel} from "@angular/material/form-field";
|
||||
import {MatOption} from "@angular/material/autocomplete";
|
||||
import {MatSelect} from "@angular/material/select";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-image',
|
||||
templateUrl: './create-image.component.html',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatButton,
|
||||
MatDivider,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
ReactiveFormsModule,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatHeaderCellDef,
|
||||
MatCell,
|
||||
MatCellDef,
|
||||
MatChip,
|
||||
MatHeaderRow,
|
||||
MatRow,
|
||||
MatHeaderRowDef,
|
||||
MatRowDef,
|
||||
MatCheckbox,
|
||||
MatRadioGroup,
|
||||
MatRadioButton,
|
||||
MatFormField,
|
||||
MatLabel,
|
||||
MatOption,
|
||||
MatSelect,
|
||||
MatInput,
|
||||
FormsModule
|
||||
],
|
||||
styleUrl: './create-image.component.css'
|
||||
})
|
||||
export class CreateImageComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
|
||||
errorMessage = '';
|
||||
clientId: string | null = null;
|
||||
partitions: any[] = [];
|
||||
images: any[] = [];
|
||||
clientName: string = '';
|
||||
selectedImage: string | null = null;
|
||||
selectedPartition: any = null;
|
||||
name: string = '';
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'diskNumber',
|
||||
header: 'Disco',
|
||||
cell: (partition: any) => `${partition.diskNumber}`
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionNumber',
|
||||
header: 'Particion',
|
||||
cell: (partition: any) => `${partition.partitionNumber}`
|
||||
},
|
||||
{
|
||||
columnDef: 'size',
|
||||
header: 'Tamaño',
|
||||
cell: (partition: any) => `${partition.size} MB`
|
||||
},
|
||||
{
|
||||
columnDef: 'filesystem',
|
||||
header: 'Sistema de ficheros',
|
||||
cell: (partition: any) => `${partition.filesystem}`
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => `${partition.operativeSystem?.name}`
|
||||
}
|
||||
];
|
||||
|
||||
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
|
||||
selection = new SelectionModel(true, []);
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
this.loadPartitions();
|
||||
this.loadImages();
|
||||
}
|
||||
|
||||
loadPartitions() {
|
||||
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.clientName = response.name;
|
||||
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadImages() {
|
||||
const url = `${this.baseUrl}/images?created=false&page=1&itemsPerPage=1000`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
this.images = response['hydra:member'];
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar las imágenes:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const payload = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
name: this.name,
|
||||
image: this.selectedImage,
|
||||
partition: this.selectedPartition['@id'],
|
||||
source: 'assistant'
|
||||
};
|
||||
|
||||
|
||||
this.http.post(`${this.baseUrl}/images`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Petición de creación de imagen enviada');
|
||||
this.router.navigate(['/images']);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.option-container {
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.deploy-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
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;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@subnetsTitle">Deploy imagen en {{ clientName }}</h2>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" (click)="save()">Guardar</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<div class="option-container">
|
||||
<mat-radio-group [(ngModel)]="selectedOption" name="selectedOption" aria-label="Selecciona una opcion">
|
||||
<mat-radio-button value="update-cache">Actualizar cache</mat-radio-button>
|
||||
<mat-radio-button value="deploy-image">Deploy imagen</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="deploy-container">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage">
|
||||
<mat-option *ngFor="let image of images" [value]="image['@id']">{{ image.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione método de deploy</mat-label>
|
||||
<mat-select [(ngModel)]="selectedMethod" name="selectedMethod">
|
||||
<mat-option *ngFor="let method of deployMethods" [value]="method">{{ method }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" name="selectedPartition">
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
{{ column.cell(image) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
<h3 *ngIf="isMethod('multicast')" class="input-group">Opciones multicast</h3>
|
||||
<h3 *ngIf="isMethod('torrent')" class="input-group">Opciones torrent</h3>
|
||||
<div *ngIf="isMethod('multicast')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Puerto</mat-label>
|
||||
<input matInput [(ngModel)]="mcastPort" name="mcastPort">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Dirección</mat-label>
|
||||
<input matInput [(ngModel)]="mcastIp" name="mcastIp">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label i18n="@@mcastModeLabel">Modo Multicast</mat-label>
|
||||
<mat-select [(ngModel)]="mcastMode" name="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Velocidad</mat-label>
|
||||
<input matInput [(ngModel)]="mcastSpeed" name="mcastSpeed">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Máximo Clientes</mat-label>
|
||||
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Tiempo Máximo de Espera</mat-label>
|
||||
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isMethod('torrent')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
||||
<mat-select [(ngModel)]="p2pMode" name="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Semilla</mat-label>
|
||||
<input matInput [(ngModel)]="p2pTime" name="p2pTime">
|
||||
</mat-form-field>
|
||||
</div>
|
|
@ -0,0 +1,72 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DeployImageComponent } from './deploy-image.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { ReactiveFormsModule, FormsModule, FormBuilder } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatRadioModule } from '@angular/material/radio'; // Importar MatRadioModule
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../../client-tab-view/data.service';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
|
||||
describe('DeployImageComponent', () => {
|
||||
let component: DeployImageComponent;
|
||||
let fixture: ComponentFixture<DeployImageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DeployImageComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatDividerModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
BrowserAnimationsModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
provideRouter([]),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
}
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DeployImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,173 @@
|
|||
import {Component, EventEmitter, Output} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {SelectionModel} from "@angular/cdk/collections";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-deploy-image',
|
||||
templateUrl: './deploy-image.component.html',
|
||||
styleUrl: './deploy-image.component.css'
|
||||
})
|
||||
export class DeployImageComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
|
||||
errorMessage = '';
|
||||
clientId: string | null = null;
|
||||
partitions: any[] = [];
|
||||
images: any[] = [];
|
||||
clientName: string = '';
|
||||
selectedImage: string | null = null;
|
||||
selectedOption: string | null = null;
|
||||
selectedMethod: string | null = null;
|
||||
selectedPartition: any = null;
|
||||
mcastIp: string = '';
|
||||
mcastPort: string = '';
|
||||
mcastMode: string = '';
|
||||
mcastSpeed: Number = 0;
|
||||
mcastMaxClients: Number = 0;
|
||||
mcastMaxTime: Number = 0;
|
||||
p2pMode: string = '';
|
||||
p2pTime: Number = 0;
|
||||
name: string = '';
|
||||
|
||||
protected p2pModeOptions = [
|
||||
{ name: 'Leecher', value: 'p2p-mode-leecher' },
|
||||
{ name: 'Peer', value: 'p2p-mode-peer' },
|
||||
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
||||
];
|
||||
protected multicastModeOptions = [
|
||||
{"name": 'Half duplex', "value": "half-duplex"},
|
||||
{"name": 'Full duplex', "value": "full-duplex"},
|
||||
];
|
||||
|
||||
allMethods = [
|
||||
'multicast',
|
||||
'multicast-direct',
|
||||
'unicast',
|
||||
'unicast-direct',
|
||||
'torrent'
|
||||
];
|
||||
|
||||
updateCacheMethods = [
|
||||
'multicast',
|
||||
'unicast',
|
||||
'torrent'
|
||||
];
|
||||
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'diskNumber',
|
||||
header: 'Disco',
|
||||
cell: (partition: any) => `${partition.diskNumber}`
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionNumber',
|
||||
header: 'Particion',
|
||||
cell: (partition: any) => `${partition.partitionNumber}`
|
||||
},
|
||||
{
|
||||
columnDef: 'size',
|
||||
header: 'Tamaño',
|
||||
cell: (partition: any) => `${partition.size} MB`
|
||||
},
|
||||
{
|
||||
columnDef: 'filesystem',
|
||||
header: 'Sistema de ficheros',
|
||||
cell: (partition: any) => `${partition.filesystem}`
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => `${partition.operativeSystem?.name}`
|
||||
}
|
||||
];
|
||||
|
||||
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
|
||||
selection = new SelectionModel(true, []);
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
this.loadPartitions();
|
||||
this.loadImages();
|
||||
}
|
||||
|
||||
get deployMethods() {
|
||||
return this.selectedOption === 'update-cache' ? this.updateCacheMethods : this.allMethods;
|
||||
}
|
||||
|
||||
isMethod(method: string): boolean {
|
||||
return this.selectedMethod === method;
|
||||
}
|
||||
|
||||
loadPartitions() {
|
||||
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.clientName = response.name;
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0;
|
||||
});
|
||||
this.p2pMode = response.organizationalUnit?.networkSettings?.p2pMode;
|
||||
this.p2pTime = response.organizationalUnit?.networkSettings?.p2pTime;
|
||||
this.mcastSpeed = response.organizationalUnit?.networkSettings?.mcastSpeed;
|
||||
this.mcastMode = response.organizationalUnit?.networkSettings?.mcastMode;
|
||||
this.mcastPort = response.organizationalUnit?.networkSettings?.mcastPort;
|
||||
this.mcastIp = response.organizationalUnit?.networkSettings?.mcastIp;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadImages() {
|
||||
const url = `${this.baseUrl}/images?page=1&itemsPerPage=1000`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
this.images = response['hydra:member'];
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar las imágenes:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const payload = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
method: this.selectedMethod,
|
||||
partition: this.selectedPartition['@id'],
|
||||
p2pMode: this.p2pMode,
|
||||
p2pTime: this.p2pTime,
|
||||
mcastIp: this.mcastIp,
|
||||
mcastPort: this.mcastPort,
|
||||
mcastMode: this.mcastMode,
|
||||
mcastSpeed: this.mcastSpeed,
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Imagen creada exitosamente');
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,20 +5,12 @@
|
|||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
background-color: #cecbcb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header label {
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.disk-size {
|
||||
|
@ -44,6 +36,11 @@
|
|||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
border-right: 2px solid white; /* Borde de separación */
|
||||
}
|
||||
|
||||
.partition-segment:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.partition-table {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<h2 mat-dialog-title>Asistente de particionado</h2>
|
||||
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="partition-assistant" *ngFor="let disk of disks; let i = index">
|
||||
<div class="header">
|
||||
|
@ -8,66 +7,61 @@
|
|||
<span class="disk-size">Tamaño: {{ (disk.totalDiskSize / 1024).toFixed(2) }} GB</span>
|
||||
</div>
|
||||
|
||||
<div class="partition-bar">
|
||||
<div
|
||||
*ngFor="let partition of disk.partitions"
|
||||
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
|
||||
class="partition-segment"
|
||||
>
|
||||
{{ partition.type }} ({{ (partition.size / 1024).toFixed(2) }} GB)
|
||||
</div>
|
||||
<div class="partition-bar">
|
||||
<div
|
||||
*ngFor="let partition of disk.partitions"
|
||||
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
|
||||
class="partition-segment"
|
||||
>
|
||||
{{ partition.type }} ({{ (partition.size / 1024).toFixed(2) }} GB)
|
||||
</div>
|
||||
|
||||
<button mat-flat-button color="primary" (click)="addPartition(disk.diskNumber)"> + </button>
|
||||
|
||||
<table class="partition-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Partición</th>
|
||||
<th>Tipo partición</th>
|
||||
<th>Tamaño (MB)</th>
|
||||
<th>Uso (%)</th>
|
||||
<th>Formatear</th>
|
||||
<th>Eliminar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let partition of disk.partitions; let j = index">
|
||||
<td>{{ partition.partitionNumber }}</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.type">
|
||||
<option value="NTFS">NTFS</option>
|
||||
<option value="LINUX">LINUX</option>
|
||||
<option value="CACHE">CACHE</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.size"
|
||||
(input)="updatePartitionSize(disk.diskNumber, j, partition.size)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.memoryUsage"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" [(ngModel)]="partition.format" />
|
||||
</td>
|
||||
<td>
|
||||
<button (click)="removePartition(disk.diskNumber, partition)" class="remove-btn">X</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<button mat-flat-button color="primary" (click)="addPartition(disk.diskNumber)"> + </button>
|
||||
|
||||
<table class="partition-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Partición</th>
|
||||
<th>Tipo partición</th>
|
||||
<th>Tamaño (MB)</th>
|
||||
<th>Uso (%)</th>
|
||||
<th>Formatear</th>
|
||||
<th>Eliminar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let partition of disk.partitions; let j = index">
|
||||
<td>{{ partition.partitionNumber }}</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.type">
|
||||
<option value="NTFS">NTFS</option>
|
||||
<option value="LINUX">LINUX</option>
|
||||
<option value="CACHE">CACHE</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.size"
|
||||
(input)="updatePartitionSize(disk.diskNumber, j, partition.size)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.memoryUsage"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" [(ngModel)]="partition.format" />
|
||||
</td>
|
||||
<td>
|
||||
<button (click)="removePartition(disk.diskNumber, partition)" class="remove-btn">X</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="save()">Guardar</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -2,6 +2,7 @@ import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/c
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
|
||||
interface Partition {
|
||||
uuid?: string;
|
||||
|
@ -26,6 +27,8 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
|
||||
errorMessage = '';
|
||||
originalPartitions: any[] = [];
|
||||
clientId: string | null = null;
|
||||
data: any = {};
|
||||
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[] }[] = [];
|
||||
|
||||
private apiUrl: string = this.baseUrl + '/partitions';
|
||||
|
@ -33,15 +36,16 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
||||
this.loadPartitions();
|
||||
}
|
||||
|
||||
loadPartitions() {
|
||||
const url = `${this.baseUrl}${this.data}`;
|
||||
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response) => {
|
||||
this.data = response;
|
||||
|
@ -76,7 +80,7 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
type: partition.type || partition.filesystem || 'NTFS',
|
||||
sizeBytes: partition.size,
|
||||
format: false,
|
||||
color: '#' + Math.floor(Math.random() * 16777215).toString(16),
|
||||
color: '#1f1b91',
|
||||
percentage: 0
|
||||
});
|
||||
}
|
||||
|
@ -107,7 +111,6 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
|
||||
if (disk) {
|
||||
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize);
|
||||
console.log(remainingGB)
|
||||
if (remainingGB > 0) {
|
||||
const maxPartitionNumber =
|
||||
disk.partitions.length > 0 ? Math.max(...disk.partitions.map((p) => p.partitionNumber)) : 0;
|
||||
|
@ -137,20 +140,41 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize) + partition.size;
|
||||
|
||||
if (size > remainingGB) {
|
||||
this.errorMessage = `El tamaño de la partición no puede superar el espacio libre (${remainingGB.toFixed(
|
||||
2
|
||||
)} GB).`;
|
||||
this.errorMessage = `El tamaño de la partición no puede superar el espacio libre (${remainingGB.toFixed(2)} GB).`;
|
||||
} else {
|
||||
this.errorMessage = '';
|
||||
partition.size = size;
|
||||
partition.sizeBytes = size;
|
||||
|
||||
partition.percentage = (size / disk.totalDiskSize) * 100;
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatePartitionPercentage(diskNumber: number, index: number, percentage: number) {
|
||||
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
|
||||
if (disk) {
|
||||
const partition = disk.partitions[index];
|
||||
|
||||
const newSizeMB = (percentage / 100) * disk.totalDiskSize;
|
||||
|
||||
const totalPercentage = disk.partitions.reduce((sum, part) => sum + (part === partition ? percentage : part.percentage), 0);
|
||||
|
||||
if (totalPercentage > 100) {
|
||||
this.errorMessage = 'El tamaño total en porcentaje de las particiones no puede exceder el 100%';
|
||||
partition.percentage = 100 - (totalPercentage - percentage);
|
||||
} else {
|
||||
this.errorMessage = '';
|
||||
partition.percentage = percentage;
|
||||
partition.size = newSizeMB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
getRemainingGB(partitions: Partition[], totalDiskSize: number): number {
|
||||
console.log(totalDiskSize)
|
||||
const totalUsedGB = partitions.reduce((acc, partition) => acc + partition.size, 0);
|
||||
return Math.max(0, totalDiskSize - totalUsedGB);
|
||||
}
|
||||
|
@ -216,14 +240,14 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
memoryUsage: partition.memoryUsage,
|
||||
size: partition.size,
|
||||
filesystem: partition.type,
|
||||
client: this.data['@id']
|
||||
client: `/clients/${this.clientId}`
|
||||
};
|
||||
|
||||
if (isNew) {
|
||||
this.http.post(this.apiUrl, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Partición creada exitosamente');
|
||||
this.loadPartitions();
|
||||
window.location.reload();
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al crear la partición:', error);
|
||||
|
@ -235,7 +259,7 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
this.http.patch(patchUrl, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Partición actualizada exitosamente');
|
||||
this.loadPartitions();
|
||||
window.location.reload();
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al actualizar la partición:', error);
|
||||
|
@ -246,7 +270,6 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
removePartition(diskNumber: number, partition: Partition) {
|
||||
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
|
||||
|
||||
|
@ -261,7 +284,7 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
this.http.delete(deleteUrl).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Partición eliminada exitosamente');
|
||||
this.loadPartitions();
|
||||
window.location.reload();
|
||||
},
|
||||
(error) => {}
|
||||
);
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
.partition-assistant {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.partition-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.partition-table th {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
padding: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.partition-table td {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.partition-table select {
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
button.mat-flat-button {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button.mat-flat-button:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
<h2>Asistente de imagenes en disco</h2>
|
||||
<h2>{{ 'diskImageAssistantTitle' | translate }}</h2>
|
||||
<div *ngFor="let disk of disks" class="partition-assistant">
|
||||
<div class="header">
|
||||
<label>Disco {{ disk.diskNumber }}</label>
|
||||
<label>{{ 'diskLabel' | translate }} {{ disk.diskNumber }}</label>
|
||||
</div>
|
||||
|
||||
<table class="partition-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Partición</th>
|
||||
<th>Imagen ISO</th>
|
||||
<th>OgLive</th>
|
||||
<th>{{ 'partitionColumn' | translate }}</th>
|
||||
<th>{{ 'isoImageColumn' | translate }}</th>
|
||||
<th>{{ 'ogliveColumn' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -17,7 +17,7 @@
|
|||
<td>{{ partition.partitionNumber }}</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.associatedImageId" (change)="onImageSelected(partition, $event)" name="associatedImage-{{partition.partitionNumber}}">
|
||||
<option value="">Seleccionar imagen</option>
|
||||
<option value="">{{ 'selectImageOption' | translate }}</option>
|
||||
<option *ngFor="let image of availableImages" [value]="image['@id']">
|
||||
{{ image.name }}
|
||||
</option>
|
||||
|
@ -25,7 +25,7 @@
|
|||
</td>
|
||||
<td>
|
||||
<select (change)="onOgLiveSelected(partition, $event)">
|
||||
<option value="">Seleccionar OgLive</option>
|
||||
<option value="">{{ 'selectOgLiveOption' | translate }}</option>
|
||||
<option *ngFor="let ogLive of availableOgLives" [value]="ogLive">{{ ogLive }}</option>
|
||||
</select>
|
||||
</td>
|
||||
|
@ -35,5 +35,5 @@
|
|||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button mat-flat-button color="primary" (click)="saveAssociations()">Guardar Asociaciones</button>
|
||||
<button mat-flat-button color="primary" (click)="saveAssociations()">{{ 'saveAssociationsButton' | translate }}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
interface Image {
|
||||
'@id': string;
|
||||
'@type': string;
|
||||
name: string;
|
||||
description: string;
|
||||
comments: string;
|
||||
uuid: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface Partition {
|
||||
diskNumber: number;
|
||||
partitionNumber: number;
|
||||
associatedImageId?: string;
|
||||
associatedOgLive?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-restore-image',
|
||||
templateUrl: './restore-image.component.html',
|
||||
styleUrls: ['./restore-image.component.css']
|
||||
})
|
||||
export class RestoreImageComponent implements OnInit {
|
||||
@Input() data: any;
|
||||
|
||||
disks: { diskNumber: number; partitions: Partition[] }[] = [];
|
||||
availableImages: Image[] = [];
|
||||
availableOgLives: string[] = [];
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeDisks();
|
||||
this.fetchAvailableImages();
|
||||
this.availableOgLives = ['LiveCD1', 'LiveCD2', 'LiveCD3'];
|
||||
}
|
||||
|
||||
initializeDisks() {
|
||||
const partitionsFromData = this.data.partitions;
|
||||
const disksMap = new Map<number, Partition[]>();
|
||||
|
||||
partitionsFromData.forEach((partition: any) => {
|
||||
if (!disksMap.has(partition.diskNumber)) {
|
||||
disksMap.set(partition.diskNumber, []);
|
||||
}
|
||||
|
||||
disksMap.get(partition.diskNumber)!.push({
|
||||
diskNumber: partition.diskNumber,
|
||||
partitionNumber: partition.partitionNumber
|
||||
});
|
||||
});
|
||||
|
||||
disksMap.forEach((partitions, diskNumber) => {
|
||||
this.disks.push({ diskNumber, partitions });
|
||||
});
|
||||
}
|
||||
|
||||
fetchAvailableImages() {
|
||||
const url = 'http://127.0.0.1:8001/images?page=1&itemsPerPage=30';
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
this.availableImages = response['hydra:member'];
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al obtener las imágenes:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onImageSelected(partition: Partition, event: Event) {
|
||||
const selectElement = event.target as HTMLSelectElement;
|
||||
partition.associatedImageId = selectElement.value;
|
||||
}
|
||||
|
||||
onOgLiveSelected(partition: Partition, event: Event) {
|
||||
const selectElement = event.target as HTMLSelectElement;
|
||||
partition.associatedOgLive = selectElement.value;
|
||||
}
|
||||
|
||||
saveAssociations() {
|
||||
this.disks.forEach(disk => {
|
||||
disk.partitions.forEach(partition => {
|
||||
if (partition.associatedImageId || partition.associatedOgLive) {
|
||||
console.log(
|
||||
`Guardando para disco ${partition.diskNumber}, partición ${partition.partitionNumber}, ` +
|
||||
`Imagen ID: ${partition.associatedImageId}, OgLive: ${partition.associatedOgLive}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -62,3 +62,31 @@ button{
|
|||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chip-busy {
|
||||
background-color: indianred !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.chip-og-live {
|
||||
background-color: yellow !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.chip-windows,
|
||||
.chip-windows-session,
|
||||
.chip-macos {
|
||||
background-color: cornflowerblue !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-linux,
|
||||
.chip-linux-session {
|
||||
background-color: mediumpurple !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-off {
|
||||
background-color: darkgrey !important;
|
||||
color: white;
|
||||
}
|
||||
|
|
|
@ -1,89 +1,124 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Administrar clientes</h2>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="resetFilters()">Reiniciar filtros</button>
|
||||
<button mat-flat-button color="primary" (click)="addClient($event)">Añadir cliente</button>
|
||||
</div>
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="clientTabtitleStep" text="En esta pantalla se podran administrar todos los clientes del sistema sin jerarquias.">{{ 'adminImagesTitle' | translate }}</h2>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="clientTabResetFiltersStep" text="Reinicia los filtros aplicados para mostrar todos los clientes.">{{ 'resetFiltersButton' | translate }}</button>
|
||||
<button mat-flat-button color="primary" (click)="addClient($event)" joyrideStep="clientTabaddClientStep" text="Añade un nuevo cliente a la lista.">{{ 'addClientButton' | translate }}</button>
|
||||
</div>
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre de cliente</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar IP</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar MAC</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['mac']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="search-select">
|
||||
<mat-label i18n="@@organizational-unit-label">U. Organizativa</mat-label>
|
||||
<mat-select [(ngModel)]="filters['organizationalUnit.id']" (selectionChange)="search()">
|
||||
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit.id" >
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let client" >
|
||||
<ng-container *ngIf="column.columnDef === 'name'">
|
||||
<div class="client-info">
|
||||
<div class="client-name">{{ client.name }}</div>
|
||||
<div class="client-ip">{{ client.ip }}</div>
|
||||
<div class="client-mac">{{ client.mac }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'status'">
|
||||
<mat-chip>
|
||||
{{ client.status }}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef !== 'status' && column.columnDef !== 'name'" >
|
||||
{{ column.cell(client) }}
|
||||
</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 client" style="text-align: center;">
|
||||
<button *ngIf="!syncStatus" mat-icon-button color="primary" (click)="getStatus(client)"><mat-icon>sync</mat-icon></button>
|
||||
<button *ngIf="syncStatus" mat-icon-button color="primary"><mat-spinner diameter="24"></mat-spinner></button>
|
||||
<button mat-icon-button color="info" (click)="handleClientClick($event, client)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
|
||||
<button mat-icon-button color="primary" (click)="onEditClick($event, client.uuid)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" (click)="onDeleteClick($event, client)">
|
||||
<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">
|
||||
<mat-paginator [length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageIndex]="page"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
||||
<div class="search-container" joyrideStep="clientTabsearchContainerStep" text="Filtra los clientes por nombre, IP, MAC o unidad organizativa.">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">{{ 'searchClientNameLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">{{ 'searchIPLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">{{ 'searchMACLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['mac']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-select" >
|
||||
<mat-label i18n="@@organizational-unit-label">{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="filters['organizationalUnit.id']" (selectionChange)="search()">
|
||||
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit.id" >
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="clientTabtableStep" text="Lista de clientes filtrados por tus criterios de búsqueda.">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let client" >
|
||||
<ng-container *ngIf="column.columnDef === 'name'">
|
||||
<div class="client-info">
|
||||
<div class="client-name">{{ client.name }}</div>
|
||||
<div class="client-ip">{{ client.ip }}</div>
|
||||
<div class="client-mac">{{ client.mac }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'status'">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-og-live': client.status === 'og-live',
|
||||
'chip-busy': client.status === 'busy',
|
||||
'chip-windows': client.status === 'windows' || client.status === 'windows-session',
|
||||
'chip-linux': client.status === 'linux' || client.status === 'linux-session',
|
||||
'chip-macos': client.status === 'macos',
|
||||
'chip-off': client.status === 'off'
|
||||
}">
|
||||
{{ client.status }}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef !== 'status' && column.columnDef !== 'name'">
|
||||
{{ column.cell(client) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">
|
||||
{{ 'columnActions' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let client" style="text-align: center;" joyrideStep="clientTabactionsStep"
|
||||
text="Acciones disponibles para cada cliente, como ver, editar o eliminar.">
|
||||
|
||||
<button
|
||||
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
|
||||
mat-icon-button color="primary"
|
||||
(click)="getStatus(client)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="syncStatus && syncingClientId === client.uuid"
|
||||
mat-icon-button color="primary">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button color="info" (click)="handleClientClick($event, client)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="onEditClick($event, client.uuid)" i18n="@@editImage">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="onDeleteClick($event, client)">
|
||||
<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="clientTabpaginationStep" text="Navega entre las páginas de resultados utilizando el paginador.">
|
||||
<mat-paginator [length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageIndex]="page"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,7 @@ import { throwError } from 'rxjs';
|
|||
import { catchError } from 'rxjs/operators';
|
||||
import {ClientViewComponent} from "../../shared/client-view/client-view.component";
|
||||
import { Router } from '@angular/router';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
|
||||
@Component({
|
||||
selector: 'app-client-tab-view',
|
||||
|
@ -31,6 +32,7 @@ export class ClientTabViewComponent {
|
|||
filters: { [key: string]: string } = {};
|
||||
organizationalUnits: any[] = [];
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
syncingClientId: number | null = null;
|
||||
|
||||
private apiUrl = `${this.baseUrl}/clients`;
|
||||
|
||||
|
@ -78,7 +80,8 @@ export class ClientTabViewComponent {
|
|||
public dialog: MatDialog,
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private joyrideService: JoyrideService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -134,16 +137,20 @@ export class ClientTabViewComponent {
|
|||
}
|
||||
|
||||
getStatus(client: any): void {
|
||||
this.syncingClientId = client.uuid;
|
||||
this.syncStatus = true;
|
||||
|
||||
this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.search()
|
||||
this.search();
|
||||
this.syncStatus = false;
|
||||
this.syncingClientId = null;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
this.syncStatus = false;
|
||||
this.syncingClientId = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -151,7 +158,7 @@ export class ClientTabViewComponent {
|
|||
|
||||
handleClientClick(event: MouseEvent, client: any): void {
|
||||
event.stopPropagation();
|
||||
this.router.navigate(['client', client.uuid], { state: { clientData: client } });
|
||||
this.router.navigate(['clients', client.uuid], { state: { clientData: client } });
|
||||
|
||||
}
|
||||
|
||||
|
@ -190,4 +197,20 @@ export class ClientTabViewComponent {
|
|||
this.length = event.length;
|
||||
this.getClients();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'clientTabtitleStep',
|
||||
'clientTabResetFiltersStep',
|
||||
'clientTabaddClientStep',
|
||||
'clientTabsearchContainerStep',
|
||||
'clientTabtableStep',
|
||||
'clientTabactionsStep',
|
||||
'clientTabpaginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,15 +39,17 @@
|
|||
.mat-chip-success {
|
||||
background-color: #4CAF50 !important;
|
||||
color: white !important;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mat-chip-error {
|
||||
background-color: #F44336 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.button-row{
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.calendar-ico{
|
||||
margin-top: 5px;
|
||||
color: gray;
|
||||
}
|
||||
|
|
|
@ -1,61 +1,68 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Administrar unidades organizativas</h2>
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="titleS4tep" text="Gestiona las unidades organizativas desde esta pantalla.">{{ 'resetFiltersButton' | translate }}</h2>
|
||||
<div class="button-row">
|
||||
<button mat-flat-button color="primary" (click)="resetFilters()">Reiniciar filtros</button>
|
||||
<button mat-flat-button color="primary" (click)="addOrganizationalUnit($event)">Añadir OU</button>
|
||||
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="Reinicia los filtros aplicados para mostrar todas las unidades organizativas.">Reiniciar filtros</button>
|
||||
<button mat-flat-button color="primary" (click)="addOrganizationalUnit($event)" joyrideStep="addOUStep" text="Añade una nueva unidad organizativa.">{{ 'addOUButton' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
<div class="search-container">
|
||||
|
||||
<div class="search-container" joyrideStep="searchContainerStep" text="Filtra las unidades organizativas por nombre o tipo.">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre de OU</mat-label>
|
||||
<mat-label i18n="@@searchLabel">{{ 'searchLabelOu' | translate }}</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
<mat-hint i18n="@@searchHint">{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-boolean">
|
||||
<mat-label i18n="@@searchLabel">Tipo</mat-label>
|
||||
<mat-select [(ngModel)]="filters['type']" (selectionChange)="search()" placeholder="Seleccionar opción" >
|
||||
<mat-option [value]="''">Todos</mat-option>
|
||||
<mat-option [value]="'organizational-unit'">Centro</mat-option>
|
||||
<mat-option [value]="'classrooms-group'">Grupos de aulas</mat-option>
|
||||
<mat-option [value]="'classroom'">Aula</mat-option>
|
||||
<mat-option [value]="'clients-group'">Grupos de PCs</mat-option>
|
||||
<mat-label i18n="@@searchLabel">{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="filters['type']" (selectionChange)="search()" placeholder="Seleccionar opción">
|
||||
<mat-option [value]="''">{{ 'allOption' | translate }}</mat-option>
|
||||
<mat-option [value]="'organizational-unit'">{{ 'centerOption' | translate }}</mat-option>
|
||||
<mat-option [value]="'classrooms-group'">{{ 'classroomsGroupOption' | translate }}</mat-option>
|
||||
<mat-option [value]="'classroom'">{{ 'classroomOption' | translate }}</mat-option>
|
||||
<mat-option [value]="'clients-group'">{{ 'clientsGroupOption' | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Aquí se muestran las unidades organizativas 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 ou" >
|
||||
<td mat-cell *matCellDef="let ou">
|
||||
<ng-container *ngIf="column.columnDef !== 'available' && column.columnDef !== 'type'">
|
||||
{{ column.cell(ou) }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'available'" >
|
||||
<mat-chip *ngIf="ou.available" class="mat-chip-success"><mat-icon style="color:white;">check</mat-icon></mat-chip>
|
||||
<mat-chip *ngIf="!ou.available" class="mat-chip-error"> <mat-icon style="color:white;">close</mat-icon></mat-chip>
|
||||
<ng-container *ngIf="column.columnDef === 'available'">
|
||||
<mat-chip *ngIf="ou.available" class="mat-chip-success"><mat-icon class="calendar-ico">event_available</mat-icon></mat-chip>
|
||||
<mat-chip *ngIf="!ou.available" class="mat-chip-error"><mat-icon class="calendar-ico">event_busy</mat-icon></mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'type'" >
|
||||
<mat-chip> {{ ou.type }} </mat-chip>
|
||||
<ng-container *ngIf="column.columnDef === 'type'">
|
||||
<mat-chip>{{ ou.type }}</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 ou" style="text-align: center;">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let ou" style="text-align: center;" joyrideStep="actionsStep" text="Usa estas opciones para ver, editar o eliminar una unidad organizativa.">
|
||||
<button mat-icon-button color="info" (click)="onShowClick($event, ou)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
|
||||
<button mat-icon-button color="primary" (click)="onEditClick($event, ou.uuid)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="primary" (click)="onEditClick($event, ou.uuid)" i18n="@@editImage"><mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" (click)="onDeleteClick($event, ou)"><mat-icon>delete</mat-icon></button>
|
||||
<button mat-icon-button color="info" [matMenuTriggerFor]="menu">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item [disabled]="ou.type !== 'classroom'" (click)="roomMap(ou)">
|
||||
<span i18n="@@viewTreeMenu">Plano de aula</span>
|
||||
<span i18n="@@viewTreeMenu"> {{ 'roomMapOption' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item [disabled]="ou.type !== 'organizational-unit'" (click)="onTreeClick(ou)">
|
||||
<span i18n="@@viewTreeMenu">Ver organigrama</span>
|
||||
<span i18n="@@viewTreeMenu">{{ 'viewTreeMenu' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
|
@ -64,7 +71,8 @@
|
|||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
<div class="paginator-container">
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep" text="Navega entre las páginas de unidades organizativas con el paginador.">
|
||||
<mat-paginator [length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageIndex]="page"
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from "../../shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component";
|
||||
import {ClassroomViewDialogComponent} from "../../shared/classroom-view/classroom-view-modal";
|
||||
import {TreeViewComponent} from "../../shared/tree-view/tree-view.component";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
|
||||
@Component({
|
||||
selector: 'app-organizational-unit-tab-view',
|
||||
|
@ -79,7 +80,8 @@ export class OrganizationalUnitTabViewComponent {
|
|||
private dataService: DataService,
|
||||
public dialog: MatDialog,
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient
|
||||
private http: HttpClient,
|
||||
private joyrideService: JoyrideService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -169,4 +171,21 @@ export class OrganizationalUnitTabViewComponent {
|
|||
this.length = event.length;
|
||||
this.getOrganizationalUnits();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleS4tep',
|
||||
'resetFiltersStep',
|
||||
'addOUStep',
|
||||
'searchContainerStep',
|
||||
'tableStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
.card {
|
||||
flex-grow: 1;
|
||||
margin: 10px;
|
||||
|
||||
border: 2px solid rgba(102, 102, 102, 0.103)
|
||||
}
|
||||
|
||||
.header-container {
|
||||
|
@ -28,8 +30,17 @@
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
.unidad-card, .elements-card {
|
||||
flex: 1 1 45%;
|
||||
.unidad-card {
|
||||
flex: 1 1 20%;
|
||||
background-color: #fafafa;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
box-shadow: none !important;
|
||||
|
||||
}
|
||||
|
||||
.elements-card {
|
||||
flex: 1 1 75%;
|
||||
background-color: #fafafa;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
|
@ -183,13 +194,13 @@ mat-spinner {
|
|||
.result-card {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
height: 250px; /* Fijo para mantener la forma cuadrada */
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
@ -199,3 +210,84 @@ mat-spinner {
|
|||
mat-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mat-tooltip {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.classroom-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px; /* Espacio entre elementos */
|
||||
justify-content: flex-start; /* Alinea los elementos a la izquierda */
|
||||
}
|
||||
|
||||
.classroom-item {
|
||||
flex: 0 1 calc(16.66% - 16px); /* 6 columnas (para pantallas grandes) */
|
||||
box-sizing: border-box; /* Incluye padding y borde en el cálculo del ancho */
|
||||
}
|
||||
|
||||
.classroom-card {
|
||||
width: 100%; /* Asegura que el card ocupe todo el espacio del item */
|
||||
}
|
||||
|
||||
/* Pantallas medianas: 4 columnas */
|
||||
@media (max-width: 1024px) {
|
||||
.classroom-item {
|
||||
flex: 0 1 calc(25% - 16px); /* 4 columnas */
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas pequeñas: 2 columnas */
|
||||
@media (max-width: 768px) {
|
||||
.classroom-item {
|
||||
flex: 0 1 calc(50% - 16px); /* 2 columnas */
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas muy pequeñas: 1 columna */
|
||||
@media (max-width: 480px) {
|
||||
.classroom-item {
|
||||
flex: 0 1 100%; /* 1 columna */
|
||||
}
|
||||
}
|
||||
|
||||
.client-text {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-og-live {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-busy {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-windows {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-linux {
|
||||
background-color: #9c27b0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-macos {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-off {
|
||||
background-color: #9e9e9e;
|
||||
color: white;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
<mat-tab-group (selectedTabChange)="onTabChange($event)">
|
||||
<mat-tab label="General">
|
||||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminGroupsTitle">Administrar grupos</h2>
|
||||
<div class="groups-button-row">
|
||||
<button mat-flat-button color="primary" (click)="addOU($event)" i18n="@@newOrganizationalUnitButton">Nueva Unidad Organizativa</button>
|
||||
<button mat-flat-button color="primary" (click)="addClient($event)" i18n="@@newClientButton">Nuevo Cliente</button>
|
||||
<button mat-raised-button (click)="openBottomSheet()" i18n="@@legendButton">Leyenda</button>
|
||||
<mat-tab label="{{ 'generalTabLabel' | translate }}">
|
||||
<div class="header-container" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">{{ 'adminGroupsTitle' | translate }}</h2>
|
||||
<div class="groups-button-row" joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}">
|
||||
<button mat-flat-button color="primary" (click)="addOU($event)" matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">{{ 'newOrganizationalUnitButton' | translate }}</button>
|
||||
<button mat-flat-button color="primary" (click)="addClient($event)" matTooltipShowDelay="1000">{{ 'newClientButton' | translate }}</button>
|
||||
<button mat-raised-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}" matTooltipShowDelay="1000">{{ 'legendButton' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="groupLists-container">
|
||||
<mat-card class="card unidad-card">
|
||||
<mat-card-title i18n="@@organizationalUnitTitle">Centros</mat-card-title>
|
||||
<mat-card class="card unidad-card" joyrideStep="unitStep" text="{{ 'unitStepText' | translate }}" matTooltipShowDelay="1000" matTooltipPosition="above">
|
||||
<mat-card-title>{{ 'organizationalUnitTitle' | translate }}</mat-card-title>
|
||||
<mat-card-content>
|
||||
<mat-spinner *ngIf="loading"></mat-spinner>
|
||||
<mat-list *ngIf="!loading">
|
||||
<mat-list-item *ngFor="let unidad of organizationalUnits"
|
||||
[ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}" (click)="onSelectUnidad(unidad)">
|
||||
<mat-list-item *ngFor="let unidad of organizationalUnits" [ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}" (click)="onSelectUnidad(unidad)">
|
||||
<div class="item-content">
|
||||
<mat-icon>apartment</mat-icon>
|
||||
{{ unidad.name }}
|
||||
|
@ -23,49 +26,28 @@
|
|||
<mat-icon mat-button [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">more_vert</mat-icon>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="onTreeClick($event, unidad)">
|
||||
<mat-icon
|
||||
#tooltip="matTooltip"
|
||||
matTooltip="Visualizar en forma de arbol"
|
||||
matTooltipHideDelay="0"
|
||||
i18n="@@viewTreeTooltip">account_tree
|
||||
</mat-icon>
|
||||
<span i18n="@@viewTreeMenu">Ver organigrama</span>
|
||||
<mat-icon matTooltip="{{ 'viewTreeTooltip' | translate }}" matTooltipHideDelay="0">account_tree</mat-icon>
|
||||
<span>{{ 'viewTreeMenu' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="onEditClick($event, unidad.type, unidad.uuid)">
|
||||
<mat-icon
|
||||
#tooltip="matTooltip"
|
||||
matTooltip="Editar unidad organizativa"
|
||||
matTooltipHideDelay="0"
|
||||
i18n="@@editUnitTooltip">edit
|
||||
</mat-icon>
|
||||
<span i18n="@@editUnitMenu">Editar</span>
|
||||
<mat-icon matTooltip="{{ 'editUnitTooltip' | translate }}" matTooltipHideDelay="0">edit</mat-icon>
|
||||
<span>{{ 'editUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="onShowClick($event, unidad)">
|
||||
<mat-icon
|
||||
#tooltip="matTooltip"
|
||||
matTooltip="Visualizar unidad organizativa"
|
||||
matTooltipHideDelay="0"
|
||||
i18n="@@viewUnitTool">visibility
|
||||
</mat-icon>
|
||||
<span i18n="@@viewUnitMenu">Visualizar datos</span>
|
||||
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
|
||||
<span>{{ 'viewUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="addOU($event, unidad)">
|
||||
<mat-icon
|
||||
#tooltip="matTooltip"
|
||||
matTooltip="Crear unidad organizativa interna"
|
||||
matTooltipHideDelay="0"
|
||||
i18n="@@addInternalUnitTool">add_home_work
|
||||
</mat-icon>
|
||||
<span i18n="@@addInternalUnitMenu">Añadir unidad organizativa</span>
|
||||
<mat-icon matTooltip="{{ 'addInternalUnitTooltip' | translate }}" matTooltipHideDelay="0">add_home_work</mat-icon>
|
||||
<span>{{ 'addInternalUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="addClient($event, unidad)">
|
||||
<mat-icon
|
||||
#tooltip="matTooltip"
|
||||
matTooltip="Crear cliente en esta unidad organizativa"
|
||||
matTooltipHideDelay="0"
|
||||
i18n="@@addClientDevice">devices
|
||||
</mat-icon>
|
||||
<span i18n="@@addClientMenu">Crear cliente</span>
|
||||
<mat-icon matTooltip="{{ 'addClientTooltip' | translate }}" matTooltipHideDelay="0">devices</mat-icon>
|
||||
<span>{{ 'addClientMenu' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</span>
|
||||
|
@ -74,10 +56,11 @@
|
|||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="card elements-card">
|
||||
<mat-card-title>
|
||||
<div class="title-with-breadcrumb">
|
||||
<span i18n="@@internalElementsTitle"></span>
|
||||
<span>{{ 'internalElementsTitle' | translate }}</span>
|
||||
<mat-card-subtitle>
|
||||
<ng-container *ngFor="let crumb of breadcrumb; let i = index">
|
||||
<a (click)="navigateToBreadcrumb(i)">{{ crumb }}</a>
|
||||
|
@ -88,12 +71,16 @@
|
|||
</mat-card-title>
|
||||
<mat-card-content class="element-content">
|
||||
<mat-spinner *ngIf="loadingChildren"></mat-spinner>
|
||||
<mat-list *ngIf="!loadingChildren">
|
||||
|
||||
<!-- Mostrar lista normal si no es un aula -->
|
||||
<mat-list *ngIf="!loadingChildren && selectedDetail?.type !== 'classroom'">
|
||||
<div *ngIf="children.length === 0" class="empty-list">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span i18n="@@noInternalElementsMessage">No hay elementos internos</span>
|
||||
<span>{{ 'noInternalElementsMessage' | translate }}</span>
|
||||
</div>
|
||||
<mat-list-item *ngFor="let child of children" [ngClass]="{'selected-item': child === selectedUnidad, 'clickable-item': true}" (click)="onSelectChild(child)">
|
||||
<mat-list-item *ngFor="let child of children"
|
||||
[ngClass]="{'selected-item': child === selectedUnidad, 'clickable-item': true}"
|
||||
(click)="onSelectChild(child)">
|
||||
<div class="item-content">
|
||||
<mat-icon [ngSwitch]="child.type">
|
||||
<ng-container *ngSwitchCase="'organizational-unit'">apartment</ng-container>
|
||||
|
@ -103,46 +90,67 @@
|
|||
<ng-container *ngSwitchCase="'clients-group'">lan</ng-container>
|
||||
<ng-container *ngSwitchDefault>help_outline</ng-container>
|
||||
</mat-icon>
|
||||
{{child.name}}
|
||||
{{ child.name }}
|
||||
<div class="actions">
|
||||
<mat-icon mat-button [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">more_vert</mat-icon>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, child.type, child.uuid)">
|
||||
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Editar elemento" matTooltipHideDelay="0" i18n="@@editElementTooltip">edit</mat-icon>
|
||||
<span i18n="@@editElementMenu">Editar</span>
|
||||
</button>
|
||||
<button *ngIf="child.type !== 'client'" mat-menu-item (click)="onShowClick($event, child)">
|
||||
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Visualizar unidad organizativa" matTooltipHideDelay="0" i18n="@@viewUnitTooltip">visibility</mat-icon>
|
||||
<span i18n="@@viewUnitMenu">Visualizar datos</span>
|
||||
</button>
|
||||
<button *ngIf="child.type !== 'client'" mat-menu-item (click)="addOU($event, child)">
|
||||
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Crear unidad organizativa interna" matTooltipHideDelay="0" i18n="@@addInternalUnitTooltip">add_home_work</mat-icon>
|
||||
<span i18n="@@addInternalUnitMenu">Añadir unidad organizativa</span>
|
||||
</button>
|
||||
<button *ngIf="child.type !== 'client'" mat-menu-item (click)="addClient($event, child)">
|
||||
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Crear cliente en esta unidad organizativa" matTooltipHideDelay="0" i18n="@@addClientTooltip">devices</mat-icon>
|
||||
<span i18n="@@addClientMenu">Crear cliente</span>
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'editElementMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, child.uuid, child.name, child.type)">
|
||||
<mat-icon class="delete-icon" #tooltip="matTooltip" matTooltip="Borrar elemento" matTooltipHideDelay="0" i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
<span i18n="@@deleteElementMenu">Borrar elemento</span>
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'deleteElementMenu' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
||||
<!-- Mostrar cuadrícula si es un aula -->
|
||||
<div *ngIf="selectedDetail?.type === 'classroom'" class="classroom-grid">
|
||||
<div *ngFor="let pc of selectedDetail.clients" class="classroom-item">
|
||||
<mat-card class="classroom-card" [ngClass]="{
|
||||
'card-og-live': pc.status === 'og-live',
|
||||
'card-busy': pc.status === 'busy',
|
||||
'card-windows': pc.status === 'windows' || pc.status === 'windows-session',
|
||||
'card-linux': pc.status === 'linux' || pc.status === 'linux-session',
|
||||
'card-macos': pc.status === 'macos',
|
||||
'card-off': pc.status === 'off'
|
||||
}">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="client-name">{{ pc.name }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p class="client-text">{{ pc.ip }}</p>
|
||||
<p class="client-text">{{ pc.mac }}</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="end">
|
||||
<button mat-icon-button color="primary" (click)="onEditClick($event, 'client', pc.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="onDeleteClick($event, pc.uuid, pc.name, 'client')">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab i18n-label label="Búsqueda avanzada">
|
||||
|
||||
<mat-tab label="{{ 'advancedSearchTabLabel' | translate }}">
|
||||
<app-advanced-search></app-advanced-search>
|
||||
</mat-tab>
|
||||
<mat-tab i18n-label label="Clientes">
|
||||
|
||||
<mat-tab label="{{ 'clientsTabLabel' | translate }}">
|
||||
<app-client-tab-view #clientTab></app-client-tab-view>
|
||||
</mat-tab>
|
||||
<mat-tab i18n-label label="Unidades organizativas">
|
||||
|
||||
<mat-tab label="{{ 'organizationalUnitsTabLabel' | translate }}">
|
||||
<app-organizational-unit-tab-view #organizationalUnitTab></app-organizational-unit-tab-view>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
|
|
@ -25,6 +25,9 @@ import {ClientTabViewComponent} from "./components/client-tab-view/client-tab-vi
|
|||
import {
|
||||
OrganizationalUnitTabViewComponent
|
||||
} from "./components/organizational-unit-tab-view/organizational-unit-tab-view.component";
|
||||
import { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component';
|
||||
import { ExecuteCommandOuComponent } from './shared/execute-command-ou/execute-command-ou.component';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
|
||||
@Component({
|
||||
selector: 'app-groups',
|
||||
|
@ -67,7 +70,8 @@ export class GroupsComponent implements OnInit {
|
|||
public dialog: MatDialog,
|
||||
private toastService: ToastrService,
|
||||
private _bottomSheet: MatBottomSheet,
|
||||
private http: HttpClient
|
||||
private http: HttpClient,
|
||||
private joyrideService: JoyrideService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -137,11 +141,8 @@ export class GroupsComponent implements OnInit {
|
|||
this.breadcrumb = this.breadcrumb.slice(0, index + 1);
|
||||
const target = this.breadcrumbData[index];
|
||||
this.breadcrumbData = this.breadcrumbData.slice(0, index + 1);
|
||||
if (target.type === 'client') {
|
||||
this.selectedDetail = target;
|
||||
} else {
|
||||
this.loadChildrenAndClients(target.id);
|
||||
}
|
||||
this.selectedDetail = target;
|
||||
this.loadChildrenAndClients(target.id);
|
||||
}
|
||||
|
||||
loadChildrenAndClients(id: string): void {
|
||||
|
@ -298,6 +299,21 @@ export class GroupsComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
onExecuteCommand(event: MouseEvent, child: any, name: string, type:string): void {
|
||||
console.log('Executing command on:', child);
|
||||
|
||||
this.dialog.open(ExecuteCommandOuComponent, {
|
||||
width: '50%',
|
||||
data: { childUnitUuid: child }
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
console.log('Comando ejecutado con éxito');
|
||||
} else {
|
||||
console.log('Ejecución de comando cancelada');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openSnackBar(isError: boolean, message: string) {
|
||||
if (isError) {
|
||||
this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error');
|
||||
|
@ -432,4 +448,11 @@ export class GroupsComponent implements OnInit {
|
|||
const dialogRef = this.dialog.open(AcctionsModalComponent, { data: { selectedElements: this.selectedElements }, width: '700px'});
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<h1 mat-dialog-title i18n="@@actions-modal-title">Acciones</h1>
|
||||
<h1 mat-dialog-title>{{ 'actionsModalTitle' | translate }}</h1>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="button-container">
|
||||
<button
|
||||
*ngFor="let command of commands"
|
||||
*ngFor="let command of arrayCommands"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
class="button-action"
|
||||
|
|
|
@ -14,31 +14,21 @@ import { RouterLink } from '@angular/router';
|
|||
export class AcctionsModalComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
selectedElements: any;
|
||||
commands: any[] = [];
|
||||
displayedColumns: string[] = ['name', 'createdBy', 'createdAt'];
|
||||
filteredCommands = [...this.commands];
|
||||
|
||||
arrayCommands: any[] = [
|
||||
{name: 'Enceder', slug: 'power-on'},
|
||||
{name: 'Apagar', slug: 'power-off'},
|
||||
{name: 'Reiniciar', slug: 'reboot'},
|
||||
{name: 'Iniciar Sesión', slug: 'login'},
|
||||
{name: 'Inventario Software', slug: 'software-inventory'},
|
||||
{name: 'Inventario Hardware', slug: 'hardware-inventory'},
|
||||
{name: 'Ejecutar script', slug: 'run-script'},
|
||||
];
|
||||
|
||||
private apiUrl = `${this.baseUrl}/commands?page=1&itemsPerPage=40`;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadCommands();
|
||||
}
|
||||
|
||||
applyFilter(event: Event) {
|
||||
const filterValue = (event.target as HTMLInputElement).value.toLowerCase();
|
||||
this.filteredCommands = this.commands.filter(command =>
|
||||
command.name.toLowerCase().includes(filterValue)
|
||||
);
|
||||
}
|
||||
|
||||
loadCommands(): void {
|
||||
this.http.get<any>(this.apiUrl).subscribe(
|
||||
(data) => {
|
||||
this.commands = data['hydra:member'];
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching commands', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<mat-dialog-content class="classroom">
|
||||
<div *ngFor="let group of groupedClients" class="classroom-group">
|
||||
<div class="misc-clients">
|
||||
<div class="classroom-board" cdkDrag cdkDragBoundary=".classroom">Pizarra digital</div>
|
||||
<img mat-card-image src="assets/images/proyector.png" alt="Proyector" class="proyector-image" cdkDrag cdkDragBoundary=".classroom"/>
|
||||
<div class="classroom-board" cdkDrag cdkDragBoundary=".classroom">{{ 'digitalBoard' | translate }}</div>
|
||||
<img mat-card-image src="assets/images/proyector.png" alt="{{ 'projectorAlt' | translate }}" class="proyector-image" cdkDrag cdkDragBoundary=".classroom"/>
|
||||
</div>
|
||||
<div *ngFor="let row of group.clientRows" class="client-row">
|
||||
<div class="client-container" *ngFor="let client of row" cdkDrag [cdkDragFreeDragPosition]="client.dragPosition" (cdkDragMoved)="onDragMoved($event, client)" cdkDragBoundary=".classroom">
|
||||
<div class="client-box" (dblclick)="handleClientClick(client)">
|
||||
<mat-card appearance="outlined">
|
||||
<div class="client-image-container">
|
||||
<img mat-card-image src="assets/images/client.png" alt="Client" class="client-image"/>
|
||||
<img mat-card-image src="assets/images/client.png" alt="{{ 'clientAlt' | translate }}" class="client-image"/>
|
||||
<div class="client-info">
|
||||
<div class="client-name">{{ client.name }}</div>
|
||||
<div class="client-details">
|
||||
|
@ -24,5 +24,5 @@
|
|||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-raised-button color="primary" class="saveDisposition-btn" (click)="saveDisposition()">Guardar disposición</button>
|
||||
<button mat-raised-button color="primary" class="saveDisposition-btn" (click)="saveDisposition()">{{ 'saveDispositionButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,63 +1,56 @@
|
|||
<h1 mat-dialog-title i18n="@@client-properties-title">Propiedades cliente</h1>
|
||||
<h1 mat-dialog-title>{{ 'clientPropertiesTitle' | translate }}</h1>
|
||||
<mat-dialog-content>
|
||||
<mat-tab-group dynamicHeight>
|
||||
|
||||
<mat-tab label="Datos generales">
|
||||
|
||||
<mat-tab label="{{ 'generalDataTab' | translate }}">
|
||||
<table mat-table [dataSource]="generalData" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="property">
|
||||
<th mat-header-cell *matHeaderCellDef class="fixed-column" i18n="@@property-header">Propiedad</th>
|
||||
<td mat-cell *matCellDef="let element" class="fixed-column"> {{element.property}} </td>
|
||||
<th mat-header-cell *matHeaderCellDef class="fixed-column">{{ 'propertyHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element" class="fixed-column"> {{ element.property }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@value-header">Valor</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.value}} </td>
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'valueHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.value }} </td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</mat-tab>
|
||||
<mat-tab label="Propiedades de red">
|
||||
|
||||
<mat-tab label="{{ 'networkPropertiesTab' | translate }}">
|
||||
<table mat-table [dataSource]="networkData" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="property">
|
||||
<th mat-header-cell *matHeaderCellDef class="fixed-column" i18n="@@property-header">Propiedad</th>
|
||||
<td mat-cell *matCellDef="let element" class="fixed-column"> {{element.property}} </td>
|
||||
<th mat-header-cell *matHeaderCellDef class="fixed-column">{{ 'propertyHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element" class="fixed-column"> {{ element.property }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@value-header">Valor</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.value}} </td>
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'valueHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.value }} </td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</mat-tab>
|
||||
<mat-tab label="Propiedades del aula">
|
||||
|
||||
<mat-tab label="{{ 'classroomPropertiesTab' | translate }}">
|
||||
<table mat-table [dataSource]="classroomData" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="property">
|
||||
<th mat-header-cell *matHeaderCellDef class="fixed-column" i18n="@@property-header">Propiedad</th>
|
||||
<td mat-cell *matCellDef="let element" class="fixed-column"> {{element.property}} </td>
|
||||
<th mat-header-cell *matHeaderCellDef class="fixed-column">{{ 'propertyHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element" class="fixed-column"> {{ element.property }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@value-header">Valor</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.value}} </td>
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'valueHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.value }} </td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</mat-tab>
|
||||
<!-- <mat-tab label="Acciones">
|
||||
<div class="button-column">
|
||||
<button mat-flat-button color="primary" class="button-action button-encender" i18n="@@power-on-button">Encender</button>
|
||||
<button mat-flat-button color="accent" class="button-action button-apagar" i18n="@@power-off-button">Apagar</button>
|
||||
<button mat-flat-button color="warn" class="button-action button-resetear" i18n="@@reset-button">Resetear</button>
|
||||
<button mat-flat-button class="button-action button-otros-1" i18n="@@other-actions-1">Otras acciones 1</button>
|
||||
<button mat-flat-button class="button-action button-otros-2" i18n="@@other-actions-2">Otras acciones 2</button>
|
||||
<button mat-flat-button class="button-action button-otros-3" i18n="@@other-actions-3">Otras acciones 3</button>
|
||||
</div>
|
||||
</mat-tab> -->
|
||||
<mat-tab label="Particiones">
|
||||
|
||||
<mat-tab label="{{ 'partitionsTab' | translate }}">
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close (click)="onNoClick()" i18n="@@close-button">Cerrar</button>
|
||||
<button mat-button mat-dialog-close (click)="onNoClick()">{{ 'closeButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<div class="create-client-container">
|
||||
<h1 mat-dialog-title i18n="@@add-client-dialog-title">Añadir Cliente</h1>
|
||||
<h1 mat-dialog-title>{{ 'addClientDialogTitle' | translate }}</h1>
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@organizational-unit-label">Padre</mat-label>
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
|
@ -14,12 +14,12 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@name-label">Nombre</mat-label>
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@oglive-label">OgLive</mat-label>
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
|
@ -28,12 +28,12 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@serial-number-label">Número de Serie</mat-label>
|
||||
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@netiface-label">Interfaz de red</mat-label>
|
||||
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
|
@ -42,7 +42,7 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@net-driver-label">Controlador de red</mat-label>
|
||||
<mat-label>{{ 'netDriverLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netDriver">
|
||||
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
|
@ -51,21 +51,21 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mac-label">MAC</mat-label>
|
||||
<mat-hint i18n="@@mac-hint">Ejemplo: 00:11:22:33:44:55</mat-hint>
|
||||
<mat-label>{{ 'macLabel' | translate }}</mat-label>
|
||||
<mat-hint>{{ 'macHint' | translate }}</mat-hint>
|
||||
<input matInput formControlName="mac">
|
||||
<mat-error i18n="@@mac-error">Formato de MAC inválido. Ejemplo válido: 00:11:22:33:44:55</mat-error>
|
||||
<mat-error>{{ 'macError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@ip-label">Dirección IP</mat-label>
|
||||
<mat-hint i18n="@@ip-hint">Ejemplo: 127.0.0.1</mat-hint>
|
||||
<mat-label>{{ 'ipLabel' | translate }}</mat-label>
|
||||
<mat-hint>{{ 'ipHint' | translate }}</mat-hint>
|
||||
<input matInput formControlName="ip">
|
||||
<mat-error i18n="@@ip-error">Formato de dirección IP inválido. Ejemplo válido: 127.0.0.1</mat-error>
|
||||
<mat-error>{{ 'ipError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@oglive-label">Plantilla PXE</mat-label>
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="template">
|
||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
|
@ -74,19 +74,19 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardware-profile-label">Perfil de Hardware</mat-label>
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
|
||||
{{ unit.description }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error i18n="@@hardware-profile-error">Formato de URL inválido.</mat-error>
|
||||
<mat-error>{{ 'hardwareProfileError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()" i18n="@@cancel-button">Cancelar</button>
|
||||
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()" i18n="@@add-button">Añadir</button>
|
||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()">{{ 'addButton' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -48,7 +48,7 @@ export class CreateClientComponent implements OnInit {
|
|||
this.clientForm = this.fb.group({
|
||||
organizationalUnit: [this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, Validators.required],
|
||||
name: ['', Validators.required],
|
||||
serialNumber: ['', Validators.required],
|
||||
serialNumber: [''],
|
||||
netiface: null,
|
||||
netDriver: null,
|
||||
mac: ['', Validators.required],
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<h1 mat-dialog-title i18n="@@edit-client-dialog-title">Editar Cliente</h1>
|
||||
<h1 mat-dialog-title>{{ 'editClientDialogTitle' | translate }}</h1>
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@organizational-unit-label">Padre</mat-label>
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
|
@ -13,12 +13,12 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@name-label">Nombre</mat-label>
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@oglive-label">OgLive</mat-label>
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive">
|
||||
<mat-option *ngFor="let ogLive of ogLives" [value]="ogLive['@id']">
|
||||
{{ ogLive.name }}
|
||||
|
@ -27,12 +27,12 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@serial-number-label">Número de Serie</mat-label>
|
||||
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@netiface-label">Interfaz de red</mat-label>
|
||||
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
|
@ -41,7 +41,7 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@net-driver-label">Controlador de red</mat-label>
|
||||
<mat-label>{{ 'netDriverLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netDriver">
|
||||
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
|
@ -50,19 +50,19 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mac-label">MAC</mat-label>
|
||||
<mat-label>{{ 'macLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mac">
|
||||
<mat-error i18n="@@mac-error">Formato de MAC inválido. Ejemplo válido: 00:11:22:33:44:55</mat-error>
|
||||
<mat-error>{{ 'macError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@ip-label">Dirección IP</mat-label>
|
||||
<mat-label>{{ 'ipLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="ip">
|
||||
<mat-error i18n="@@ip-error">Formato de dirección IP inválido. Ejemplo válido: 127.0.0.1</mat-error>
|
||||
<mat-error>{{ 'ipError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@oglive-label">Plantilla PXE</mat-label>
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="template">
|
||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
|
@ -71,21 +71,29 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardware-profile-label">Perfil de Hardware</mat-label>
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
|
||||
{{ unit.description }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error i18n="@@hardware-profile-error">Formato de URL inválido.</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardware-profile-label">Repositorio</mat-label>
|
||||
<mat-select formControlName="repository">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()" i18n="@@cancel-button">Cancelar</button>
|
||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()">
|
||||
{{ !isEditMode ? 'Añadir' : 'Guardar' }}
|
||||
{{ !isEditMode ? ('addButton' | translate) : ('saveButton' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -16,6 +16,7 @@ export class EditClientComponent {
|
|||
clientForm!: FormGroup;
|
||||
parentUnits: any[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
repositories: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
templates: any[] = [];
|
||||
isEditMode: boolean;
|
||||
|
@ -48,6 +49,7 @@ export class EditClientComponent {
|
|||
this.loadHardwareProfiles();
|
||||
this.loadOgLives();
|
||||
this.loadPxeTemplates()
|
||||
this.loadRepositories();
|
||||
this.clientForm = this.fb.group({
|
||||
organizationalUnit: [null, Validators.required],
|
||||
name: ['', Validators.required],
|
||||
|
@ -59,6 +61,7 @@ export class EditClientComponent {
|
|||
template: null,
|
||||
hardwareProfile: null,
|
||||
ogLive: null,
|
||||
repository: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -99,6 +102,19 @@ export class EditClientComponent {
|
|||
);
|
||||
}
|
||||
|
||||
loadRepositories(): void {
|
||||
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.repositories = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadPxeTemplates(): void {
|
||||
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
|
||||
|
||||
|
@ -127,6 +143,7 @@ export class EditClientComponent {
|
|||
serialNumber: data.serialNumber,
|
||||
hardwareProfile: data.hardwareProfile ? data.hardwareProfile['@id'] : null,
|
||||
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
|
||||
repository: data.repository ? data.repository['@id'] : null,
|
||||
ogLive: data.ogLive ? data.ogLive['@id'] : null,
|
||||
template: data.template ? data.template['@id'] : null,
|
||||
});
|
||||
|
@ -150,7 +167,6 @@ export class EditClientComponent {
|
|||
|
||||
this.http.patch<any>(putUrl, formData, { headers }).subscribe(
|
||||
response => {
|
||||
console.log('PUT successful:', response);
|
||||
this.dialogRef.close();
|
||||
this.openSnackBar(false, 'Cliente actualizado exitosamente');
|
||||
|
||||
|
@ -167,7 +183,6 @@ export class EditClientComponent {
|
|||
|
||||
this.http.post<any>(postUrl, formData, { headers }).subscribe(
|
||||
response => {
|
||||
console.log('POST successful:', response);
|
||||
this.dialogRef.close();
|
||||
this.openSnackBar(false, 'Cliente creado exitosamente');
|
||||
},
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.command-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mat-checkbox {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mat-dialog-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
button[mat-button] {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button[mat-button]:disabled {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<h2 mat-dialog-title>{{ 'executeCommandOrGroupTitle' | translate }}</h2>
|
||||
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="form" class="command-form">
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectCommandLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedCommand" (selectionChange)="form.get('selectedCommandGroup')?.reset()">
|
||||
<mat-option *ngFor="let command of commands" [value]="command.uuid">{{ command.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectCommandGroupLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedCommandGroup" (selectionChange)="form.get('selectedCommand')?.reset()">
|
||||
<mat-option *ngFor="let group of commandGroups" [value]="group.uuid">{{ group.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label>{{ 'clientsLabel' | translate }}</label>
|
||||
<div *ngIf="clients.length > 0">
|
||||
<mat-checkbox *ngFor="let client of clients"
|
||||
(change)="toggleClientSelection(client.uuid)"
|
||||
[checked]="form.get('clientSelection')?.value.includes(client.uuid)">
|
||||
{{ client.name }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div *ngIf="clients.length === 0">
|
||||
<p>{{ 'noClientsMessage' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="closeModal()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button
|
||||
(click)="executeCommand()"
|
||||
[disabled]="!form.get('clientSelection')?.value.length ||
|
||||
(!form.get('selectedCommand')?.value && !form.get('selectedCommandGroup')?.value)">
|
||||
{{ 'executeButton' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,120 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-command-ou',
|
||||
templateUrl: './execute-command-ou.component.html',
|
||||
styleUrls: ['./execute-command-ou.component.css']
|
||||
})
|
||||
export class ExecuteCommandOuComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
clients: any[] = [];
|
||||
commands: any[] = [];
|
||||
commandGroups: any[] = [];
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<ExecuteCommandOuComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private http: HttpClient,
|
||||
private fb: FormBuilder,
|
||||
private toastService: ToastrService,
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
selectedCommand: [null],
|
||||
selectedCommandGroup: [null],
|
||||
clientSelection: [[]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadClients();
|
||||
this.loadCommands();
|
||||
this.loadCommandGroups();
|
||||
}
|
||||
|
||||
loadClients(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units/${this.data.childUnitUuid}`).subscribe(
|
||||
response => {
|
||||
this.clients = this.getAllClients(response);
|
||||
const clientIds = this.clients.map(client => client.uuid);
|
||||
this.form.get('clientSelection')?.setValue(clientIds);
|
||||
},
|
||||
error => console.error('Error al cargar los clientes:', error)
|
||||
);
|
||||
}
|
||||
|
||||
getAllClients(unit: any): any[] {
|
||||
let allClients = unit.clients || [];
|
||||
if (unit.children && unit.children.length > 0) {
|
||||
unit.children.forEach((child: any) => {
|
||||
allClients = allClients.concat(this.getAllClients(child));
|
||||
});
|
||||
}
|
||||
return allClients;
|
||||
}
|
||||
|
||||
loadCommands(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/commands?page=1&itemsPerPage=30`).subscribe(
|
||||
response => {
|
||||
this.commands = response['hydra:member'] || [];
|
||||
},
|
||||
error => this.toastService.error('Error al cargar comandos:', error)
|
||||
);
|
||||
}
|
||||
|
||||
loadCommandGroups(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/command-groups?page=1&itemsPerPage=30`).subscribe(
|
||||
response => {
|
||||
this.commandGroups = response['hydra:member'] || [];
|
||||
},
|
||||
error => this.toastService.error('Error al cargar grupos de comandos:', error)
|
||||
);
|
||||
}
|
||||
|
||||
toggleClientSelection(clientId: string): void {
|
||||
const selectedClients = this.form.get('clientSelection')?.value || [];
|
||||
if (selectedClients.includes(clientId)) {
|
||||
this.form.get('clientSelection')?.setValue(selectedClients.filter((id: string) => id !== clientId));
|
||||
} else {
|
||||
this.form.get('clientSelection')?.setValue([...selectedClients, clientId]);
|
||||
}
|
||||
}
|
||||
|
||||
executeCommand(): void {
|
||||
const selectedCommandUuid = this.form.get('selectedCommand')?.value || this.form.get('selectedCommandGroup')?.value;
|
||||
const isCommandGroup = !!this.form.get('selectedCommandGroup')?.value;
|
||||
|
||||
if (!selectedCommandUuid) {
|
||||
console.warn('No se ha seleccionado ningún comando o grupo de comandos');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
clients: (this.form.get('clientSelection')?.value || []).map((clientId: string) => `/clients/${clientId}`)
|
||||
};
|
||||
|
||||
const url = isCommandGroup
|
||||
? `${this.baseUrl}/command-groups/${selectedCommandUuid}/execute`
|
||||
: `${this.baseUrl}/commands/${selectedCommandUuid}/execute`;
|
||||
|
||||
this.http.post(url, payload)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Comando ejecutado con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error('Error al ejecutar el comando:', error);
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeModal(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
|
@ -1,22 +1,22 @@
|
|||
<mat-list>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>apartment</mat-icon>
|
||||
<div matListItemTitle i18n="@@orgUnitTitle">Unidad organizativa</div>
|
||||
<div matListItemTitle>{{ 'orgUnitTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>meeting_room</mat-icon>
|
||||
<div matListItemTitle i18n="@@classroomGroupsTitle">Grupos de aula</div>
|
||||
<div matListItemTitle>{{ 'classroomGroupsTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>school</mat-icon>
|
||||
<div matListItemTitle i18n="@@classroomTitle">Aula</div>
|
||||
<div matListItemTitle>{{ 'classroomTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>lan</mat-icon>
|
||||
<div matListItemTitle i18n="@@clientGroupsTitle">Grupos de clientes</div>
|
||||
<div matListItemTitle>{{ 'clientGroupsTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>computer</mat-icon>
|
||||
<div matListItemTitle i18n="@@clientTitle">Cliente</div>
|
||||
<div matListItemTitle>{{ 'clientTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<h1 mat-dialog-title i18n="@@addOrgUnitTitle">Añadir Unidad Organizativa</h1>
|
||||
<h1 mat-dialog-title>{{ 'addOrgUnitTitle' | translate }}</h1>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<mat-stepper orientation="vertical" [linear]="isLinear">
|
||||
<!-- Step 1: General -->
|
||||
<!-- Paso 1: General -->
|
||||
<mat-step [stepControl]="generalFormGroup">
|
||||
<form [formGroup]="generalFormGroup">
|
||||
<ng-template matStepLabel i18n="@@generalStepLabel">General</ng-template>
|
||||
<ng-template matStepLabel>{{ 'generalStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@typeLabel">Tipo</mat-label>
|
||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of types" [value]="type">
|
||||
{{ typeTranslations[type] }}
|
||||
|
@ -15,42 +15,42 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@nameLabel">Nombre</mat-label>
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@createOrgUnitparentLabel">Unidad organizativa padre</mat-label>
|
||||
<mat-label>{{ 'createOrgUnitparentLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="parent">
|
||||
<mat-option i18n="@@noParentOption">--</mat-option>
|
||||
<mat-option>{{ 'noParentOption' | translate }}</mat-option>
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">{{ unit.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@descriptionLabel">Descripción</mat-label>
|
||||
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperNext i18n="@@nextButton">Siguiente</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 2: Classroom Info -->
|
||||
<!-- Paso 2: Información del Aula -->
|
||||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="classroomInfoFormGroup">
|
||||
<form [formGroup]="classroomInfoFormGroup">
|
||||
<ng-template matStepLabel i18n="@@classroomInfoStepLabel">Información del Aula</ng-template>
|
||||
<ng-template matStepLabel>{{ 'classroomInfoStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@locationLabel">Ubicación</mat-label>
|
||||
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="location">
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle formControlName="projector" i18n="@@projectorToggle">Proyector</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board" i18n="@@boardToggle">Pizarra</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="projector">{{ 'projectorToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@capacityLabel">Aforo</mat-label>
|
||||
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="capacity" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Calendario Asociado</mat-label>
|
||||
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
{{ calendar.name }}
|
||||
|
@ -59,62 +59,62 @@
|
|||
</mat-form-field>
|
||||
|
||||
<div>
|
||||
<button mat-button matStepperPrevious i18n="@@backButton">Atrás</button>
|
||||
<button mat-button matStepperNext i18n="@@nextButton">Siguiente</button>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 3: Información Adicional -->
|
||||
<!-- Paso 3: Información Adicional -->
|
||||
<mat-step [stepControl]="additionalInfoFormGroup">
|
||||
<form [formGroup]="additionalInfoFormGroup">
|
||||
<ng-template matStepLabel i18n="@@additionalInfoStepLabel">Información Adicional</ng-template>
|
||||
<ng-template matStepLabel>{{ 'additionalInfoStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@commentsLabel">Comentarios</mat-label>
|
||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="comments"></textarea>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperPrevious i18n="@@backButton">Atrás</button>
|
||||
<button mat-button matStepperNext i18n="@@nextButton">Siguiente</button>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 4: Configuración de Red -->
|
||||
<!-- Paso 4: Configuración de Red -->
|
||||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="networkSettingsFormGroup">
|
||||
<form [formGroup]="networkSettingsFormGroup">
|
||||
<ng-template matStepLabel i18n="@@networkSettingsStepLabel">Configuración de Red</ng-template>
|
||||
<ng-template matStepLabel>{{ 'networkSettingsStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@nextServerLabel">NextServer</mat-label>
|
||||
<mat-label>{{ 'nextServerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="nextServer">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@bootFileNameLabel">bootFileName</mat-label>
|
||||
<mat-label>{{ 'bootFileNameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="bootFileName">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@proxyUrlLabel">Url servidor Proxy</mat-label>
|
||||
<mat-label>{{ 'proxyUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="proxy">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@dnsIpLabel">IP servidor DNS</mat-label>
|
||||
<mat-label>{{ 'dnsIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="dns">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@netmaskLabel">Máscara de Red</mat-label>
|
||||
<mat-label>{{ 'netmaskLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="netmask">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@routerLabel">Router</mat-label>
|
||||
<mat-label>{{ 'routerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="router">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@ntpIpLabel">IP servidor NTP</mat-label>
|
||||
<mat-label>{{ 'ntpIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="ntp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
||||
<mat-label>{{ 'p2pModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
|
@ -122,23 +122,23 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@p2pTimeLabel">Tiempo P2P</mat-label>
|
||||
<mat-label>{{ 'p2pTimeLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mcastIpLabel">IP Multicast</mat-label>
|
||||
<mat-label>{{ 'mcastIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mcastSpeedLabel">Velocidad Multicast</mat-label>
|
||||
<mat-label>{{ 'mcastSpeedLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastSpeed" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mcastPortLabel">Puerto Multicast</mat-label>
|
||||
<mat-label>{{ 'mcastPortLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mcastModeLabel">Modo Multicast</mat-label>
|
||||
<mat-label>{{ 'mcastModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
|
@ -146,21 +146,21 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@menuUrlLabel">Menú URL</mat-label>
|
||||
<mat-label>{{ 'menuUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="menu" type="url">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardwareProfileLabel">Perfil de Hardware</mat-label>
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error i18n="@@urlFormatError">Formato de URL inválido.</mat-error>
|
||||
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()" i18n="@@cancelButton">Cancelar</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid" i18n="@@submitButton">Añadir</button>
|
||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'submitButton' | translate }}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,163 +1,154 @@
|
|||
<h1 mat-dialog-title i18n="@@editOrgUnitTitle">Editar Unidad Organizativa</h1>
|
||||
<h1 mat-dialog-title>{{ 'editOrgUnitTitle' | translate }}</h1>
|
||||
<div mat-dialog-content>
|
||||
<mat-stepper orientation="vertical" [linear]="isLinear">
|
||||
<!-- Step 1: General -->
|
||||
<!-- Paso 1: General -->
|
||||
<mat-step [stepControl]="generalFormGroup">
|
||||
<form [formGroup]="generalFormGroup">
|
||||
<ng-template matStepLabel i18n="@@generalStepLabel">General</ng-template>
|
||||
<ng-template matStepLabel>{{ 'generalStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@typeLabel">Tipo</mat-label>
|
||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of types" [value]="type">{{ type }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@nameLabel">Nombre</mat-label>
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@editOrgUnitParentLabel">Padre</mat-label>
|
||||
<mat-label>{{ 'editOrgUnitParentLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="parent">
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">{{ unit.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@descriptionLabel">Descripción</mat-label>
|
||||
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperNext i18n="@@nextButton">Siguiente</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 2: Classroom Info -->
|
||||
<!-- Paso 2: Información del Aula -->
|
||||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="classroomInfoFormGroup">
|
||||
<form [formGroup]="classroomInfoFormGroup">
|
||||
<ng-template matStepLabel i18n="@@classroomInfoStepLabel">Información del Aula</ng-template>
|
||||
<ng-template matStepLabel>{{ 'classroomInfoStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@locationLabel">Ubicación</mat-label>
|
||||
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="location">
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle formControlName="projector" i18n="@@projectorToggle">Proyector</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board" i18n="@@boardToggle">Pizarra</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="projector">{{ 'projectorToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@capacityLabel">Aforo</mat-label>
|
||||
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="capacity" type="number">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Calendario Asociado</mat-label>
|
||||
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
{{ calendar.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div>
|
||||
<button mat-button matStepperPrevious i18n="@@backButton">Atrás</button>
|
||||
<button mat-button matStepperNext i18n="@@nextButton">Siguiente</button>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
|
||||
<!-- Step 3: Información Adicional -->
|
||||
<!-- Paso 3: Información Adicional -->
|
||||
<mat-step [stepControl]="additionalInfoFormGroup">
|
||||
<form [formGroup]="additionalInfoFormGroup">
|
||||
<ng-template matStepLabel i18n="@@additionalInfoStepLabel">Información Adicional</ng-template>
|
||||
<ng-template matStepLabel>{{ 'additionalInfoStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@commentsLabel">Comentarios</mat-label>
|
||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="comments"></textarea>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperPrevious i18n="@@backButton">Atrás</button>
|
||||
<button mat-button matStepperNext i18n="@@nextButton">Siguiente</button>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<!-- Step 4: Configuración de Red -->
|
||||
<!-- Paso 4: Configuración de Red -->
|
||||
<mat-step [stepControl]="networkSettingsFormGroup">
|
||||
<form [formGroup]="networkSettingsFormGroup">
|
||||
<ng-template matStepLabel i18n="@@networkSettingsStepLabel">Configuración de Red</ng-template>
|
||||
<ng-template matStepLabel>{{ 'networkSettingsStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@proxyUrlLabel">Url servidor Proxy</mat-label>
|
||||
<mat-label>{{ 'proxyUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="proxy">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@dnsIpLabel">IP servidor DNS</mat-label>
|
||||
<mat-label>{{ 'dnsIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="dns">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@netmaskLabel">Máscara de Red</mat-label>
|
||||
<mat-label>{{ 'netmaskLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="netmask">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@routerLabel">Router</mat-label>
|
||||
<mat-label>{{ 'routerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="router">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@ntpIpLabel">IP servidor NTP</mat-label>
|
||||
<mat-label>{{ 'ntpIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="ntp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
||||
<mat-label>{{ 'p2pModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="p2pMode">
|
||||
<mat-option
|
||||
*ngFor="let option of p2pModeOptions"
|
||||
[value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">{{ option.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@p2pTimeLabel">Tiempo P2P</mat-label>
|
||||
<mat-label>{{ 'p2pTimeLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mcastIpLabel">IP Multicast</mat-label>
|
||||
<mat-label>{{ 'mcastIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mcastSpeedLabel">Velocidad Multicast</mat-label>
|
||||
<mat-label>{{ 'mcastSpeedLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastSpeed" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mcastPortLabel">Puerto Multicast</mat-label>
|
||||
<mat-label>{{ 'mcastPortLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mcastModeLabel">Modo Multicast</mat-label>
|
||||
<mat-label>{{ 'mcastModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="mcastMode">
|
||||
<mat-option
|
||||
*ngFor="let option of multicastModeOptions"
|
||||
[value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">{{ option.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@menuUrlLabel">Menú URL</mat-label>
|
||||
<mat-label>{{ 'menuUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="menu" type="url">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardwareProfileLabel">Perfil de Hardware</mat-label>
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }} </mat-option>
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error i18n="@@urlFormatError">Formato de URL inválido.</mat-error>
|
||||
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle formControlName="validation" i18n="@@validationToggle">Validación</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="validation">{{ 'validationToggle' | translate }}</mat-slide-toggle>
|
||||
<div>
|
||||
<button mat-button matStepperPrevious i18n="@@backButton">Atrás</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid" i18n="@@submitButton">Añadir</button>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'submitButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="onNoClick()" i18n="@@cancelButton">Cancelar</button>
|
||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
<h1 mat-dialog-title i18n="@@orgUnitPropertiesTitle">Propiedades unidad organizativa</h1>
|
||||
<h1 mat-dialog-title>{{ 'orgUnitPropertiesTitle' | translate }}</h1>
|
||||
<div mat-dialog-content>
|
||||
<mat-tab-group dynamicHeight>
|
||||
<mat-tab label="Datos generales" i18n-label="@@generalDataTab">
|
||||
<mat-tab label="{{ 'generalDataTab' | translate }}">
|
||||
<table mat-table [dataSource]="generalData" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="property">
|
||||
<th mat-header-cell *matHeaderCellDef i18n-header="@@propertyHeader"> Propiedad </th>
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'propertyHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.property }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell *matHeaderCellDef i18n-header="@@valueHeader"> Valor </th>
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'valueHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.value }} </td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</mat-tab>
|
||||
<mat-tab [disabled]="data.data.type !== 'classroom'" label="Propiedades aula y de red" i18n-label="@@classroomNetworkPropertiesTab">
|
||||
<mat-tab [disabled]="data.data.type !== 'classroom'" label="{{ 'classroomNetworkPropertiesTab' | translate }}">
|
||||
<table mat-table [dataSource]="networkData" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="property">
|
||||
<th mat-header-cell *matHeaderCellDef i18n-header="@@propertyHeader"> Propiedad </th>
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'propertyHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.property }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell *matHeaderCellDef i18n-header="@@valueHeader"> Valor </th>
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'valueHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.value }} </td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<h1 mat-dialog-title i18n="@@viewTreeTitle">Visualizar árbol unidad Organizativa</h1>
|
||||
<h1 mat-dialog-title>{{ 'viewTreeTitle' | translate }}</h1>
|
||||
|
||||
<mat-dialog-content>
|
||||
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="tree">
|
||||
|
@ -11,16 +11,15 @@
|
|||
<ng-container *ngSwitchCase="'clients-group'">lan</ng-container>
|
||||
<ng-container *ngSwitchDefault>help_outline</ng-container>
|
||||
</mat-icon>
|
||||
{{node.name}}
|
||||
{{ node.name }}
|
||||
</mat-tree-node>
|
||||
<!-- This is the tree node template for expandable nodes -->
|
||||
|
||||
<!-- Nodo expandible -->
|
||||
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
|
||||
<div class="mat-tree-node">
|
||||
<button mat-icon-button matTreeNodeToggle
|
||||
[attr.aria-label]="'Toggle ' + node.name"
|
||||
i18n-aria-label="@@toggleNodeAriaLabel">
|
||||
<button mat-icon-button matTreeNodeToggle [attr.aria-label]="'Toggle ' + node.name | translate">
|
||||
<mat-icon class="mat-icon-rtl-mirror">
|
||||
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
|
||||
{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
<div class="item-content">
|
||||
|
@ -32,13 +31,11 @@
|
|||
<ng-container *ngSwitchCase="'clients-group'">lan</ng-container>
|
||||
<ng-container *ngSwitchDefault>help_outline</ng-container>
|
||||
</mat-icon>
|
||||
{{node.name}}
|
||||
{{ node.name }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- There is inline padding applied to this div using styles.
|
||||
This padding value depends on the mat-icon-button width. -->
|
||||
<div [class.tree-invisible]="!treeControl.isExpanded(node)"
|
||||
role="group">
|
||||
|
||||
<div [class.tree-invisible]="!treeControl.isExpanded(node)" role="group">
|
||||
<ng-container matTreeNodeOutlet></ng-container>
|
||||
<mat-list *ngIf="node.clients">
|
||||
<mat-list-item *ngFor="let client of node.clients">
|
||||
|
@ -51,6 +48,7 @@
|
|||
</mat-nested-tree-node>
|
||||
</mat-tree>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()" i18n="@@closeButton">Cerrar</button>
|
||||
<button mat-button (click)="close()">{{ 'closeButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue