diff --git a/package-lock.json b/package-lock.json index 2ad6060..09b13db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { - "name": "coreui-free-angular-admin-template", - "version": "4.5.27", + "name": "MikroWizard", + "version": "1.0.7", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "coreui-free-angular-admin-template", - "version": "4.5.27", - "license": "MIT", + "name": "MikroWizard", + "version": "1.0.7", + "license": "AGPL", "dependencies": { "@angular/animations": "^17.3.5", "@angular/cdk": "^16.2.9", @@ -28,11 +28,7 @@ "@coreui/icons-angular": "~4.5.27", "@coreui/utils": "^2.0.2", "@easyfonts/font-awesome-v6": "^6.0.6", - "@fortawesome/angular-fontawesome": "^0.13.0", - "@fortawesome/fontawesome-svg-core": "^6.4.2", - "@fortawesome/free-brands-svg-icons": "^6.4.2", - "@fortawesome/free-regular-svg-icons": "^6.4.2", - "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/angular-fontawesome": "^0.15.0", "@generic-ui/fabric": "^0.19.0", "@generic-ui/hermes": "^0.19.0", "@generic-ui/ngx-grid": "^0.19.0", @@ -40,19 +36,25 @@ "date-fns": "^3.6.0", "date-fns-jalali": "^3.6.0-0", "date-fns-tz": "^3.1.3", + "diff-match-patch-ts": "^0.6.0", "font-awesome": "^4.7.0", "install": "^0.13.0", "lodash-es": "^4.17.21", "mat-progress-buttons": "^9.3.1", "ngx-cron-editor": "^0.8.1", "ngx-date-fns": "^11.0.0", + "ngx-diff": "^9.0.0", + "ngx-highlight-js": "^18.0.0", "ngx-highlightjs": "^12.0.0", + "ngx-infinite-scroll": "^18.0.0", "ngx-mat-select-search": "^7.0.6", "ngx-material-date-fns-adapter": "^18.0.0", "ngx-scrollbar": "^13.0.3", "ngx-super-select": "^3.17.0", "rxjs": "~7.8.1", "tslib": "^2.3.0", + "vis-data": "^7.1.9", + "vis-network": "^9.1.9", "zone.js": "~0.14.4" }, "devDependencies": { @@ -2603,6 +2605,18 @@ "resolved": "https://registry.npmjs.org/@easyfonts/font-awesome-v6/-/font-awesome-v6-6.0.6.tgz", "integrity": "sha512-I3mV9KQuD6jJgfX3bU3mdv/RtYSd+X/Y9Yo2RrAmIfQJeDVCQdTPXBgmodMtGxfAU8lVHKFwr/fh9zexavXdNQ==" }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "peer": true, + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", @@ -2972,69 +2986,31 @@ } }, "node_modules/@fortawesome/angular-fontawesome": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.13.0.tgz", - "integrity": "sha512-gzSPRdveOXNO7NIiMgTyB46aiHG0i98KinnAEqHXi8qzraM/kCcHn/0y3f4MhemX6kftwsFli0IU8RyHmtXlSQ==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.15.0.tgz", + "integrity": "sha512-oxmJDYGNSym5ycFR0LX4ZOPAU+wWmMAznYpkm5DNAtWWkhMLcrZl15eZQmVIEE+qruQ7JiVrg3tpo8bEkFlDgw==", "dependencies": { - "tslib": "^2.4.1" + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "tslib": "^2.6.2" }, "peerDependencies": { - "@angular/core": "^16.0.0", - "@fortawesome/fontawesome-svg-core": "~1.2.27 || ~1.3.0-beta2 || ^6.1.0" + "@angular/core": "^18.0.0" } }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", - "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", - "hasInstallScript": true, + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", - "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", - "hasInstallScript": true, + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz", - "integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==", - "hasInstallScript": true, - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz", - "integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==", - "hasInstallScript": true, - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", - "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", - "hasInstallScript": true, - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { "node": ">=6" @@ -4872,6 +4848,12 @@ "@types/send": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "peer": true + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -6111,6 +6093,15 @@ "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "dev": true }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -6735,6 +6726,11 @@ "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", "dev": true }, + "node_modules/diff-match-patch-ts": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/diff-match-patch-ts/-/diff-match-patch-ts-0.6.0.tgz", + "integrity": "sha512-U0uPIJ+wJqgaBoVw2MFSFpGIk7q3mJJ+/sehbxDZFv4Gx6a1GOmrsSLmxVDDrGtRL4Q9de084aa5lVpCHn+eUw==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -9021,6 +9017,12 @@ "node": ">=10" } }, + "node_modules/keycharm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", + "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", + "peer": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -9846,6 +9848,28 @@ "date-fns": ">=3" } }, + "node_modules/ngx-diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ngx-diff/-/ngx-diff-9.0.0.tgz", + "integrity": "sha512-eoF3D/9KjNqdnscIofkS81JWj87Ffq3eI3Gqim0SG/N/m0YWtOjximFxT1JI5bS8Gi1PVgoEQntJIZb/zaNULw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=18.0.0", + "@angular/core": ">=18.0.0", + "diff-match-patch-ts": ">=0.5.0" + } + }, + "node_modules/ngx-highlight-js": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ngx-highlight-js/-/ngx-highlight-js-18.0.0.tgz", + "integrity": "sha512-r/LSijb5Ju95ZGm89+4e905kFWkDt1XdGCg2prR1wleTE77uROscjZMht0D40WSRbxmOg+J2zw9OOZvsWSLeeg==", + "dependencies": { + "highlight.js": "^11.9.0", + "tslib": "^2.3.0" + } + }, "node_modules/ngx-highlightjs": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ngx-highlightjs/-/ngx-highlightjs-12.0.0.tgz", @@ -9859,6 +9883,18 @@ "@angular/core": ">=17.0.0" } }, + "node_modules/ngx-infinite-scroll": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-18.0.0.tgz", + "integrity": "sha512-D183TDwpsd9Zl56UmItsl3RzHdN25srAISfg6lc3A8mEKkEgOq0s7ZzRAYcx8DHsAkMgtZqjIPEvMifD3DOB/g==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=18.0.0 <19.0.0", + "@angular/core": ">=18.0.0 <19.0.0" + } + }, "node_modules/ngx-mat-select-search": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/ngx-mat-select-search/-/ngx-mat-select-search-7.0.6.tgz", @@ -12746,7 +12782,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "bin": { "uuid": "dist/bin/uuid" } @@ -12782,6 +12817,53 @@ "node": ">= 0.8" } }, + "node_modules/vis-data": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz", + "integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "vis-util": "^5.0.1" + } + }, + "node_modules/vis-network": { + "version": "9.1.9", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.9.tgz", + "integrity": "sha512-Ft+hLBVyiLstVYSb69Q1OIQeh3FeUxHJn0WdFcq+BFPqs+Vq1ibMi2sb//cxgq1CP7PH4yOXnHxEH/B2VzpZYA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0", + "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0", + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "vis-data": "^6.3.0 || ^7.0.0", + "vis-util": "^5.0.1" + } + }, + "node_modules/vis-util": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz", + "integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0 || ^2.0.0" + } + }, "node_modules/vite": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.7.tgz", @@ -15497,6 +15579,15 @@ "resolved": "https://registry.npmjs.org/@easyfonts/font-awesome-v6/-/font-awesome-v6-6.0.6.tgz", "integrity": "sha512-I3mV9KQuD6jJgfX3bU3mdv/RtYSd+X/Y9Yo2RrAmIfQJeDVCQdTPXBgmodMtGxfAU8lVHKFwr/fh9zexavXdNQ==" }, + "@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "peer": true, + "requires": { + "@types/hammerjs": "^2.0.36" + } + }, "@esbuild/aix-ppc64": { "version": "0.20.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", @@ -15659,48 +15750,25 @@ "optional": true }, "@fortawesome/angular-fontawesome": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.13.0.tgz", - "integrity": "sha512-gzSPRdveOXNO7NIiMgTyB46aiHG0i98KinnAEqHXi8qzraM/kCcHn/0y3f4MhemX6kftwsFli0IU8RyHmtXlSQ==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.15.0.tgz", + "integrity": "sha512-oxmJDYGNSym5ycFR0LX4ZOPAU+wWmMAznYpkm5DNAtWWkhMLcrZl15eZQmVIEE+qruQ7JiVrg3tpo8bEkFlDgw==", "requires": { - "tslib": "^2.4.1" + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "tslib": "^2.6.2" } }, "@fortawesome/fontawesome-common-types": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", - "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==" + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==" }, "@fortawesome/fontawesome-svg-core": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", - "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", "requires": { - "@fortawesome/fontawesome-common-types": "6.5.2" - } - }, - "@fortawesome/free-brands-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz", - "integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==", - "requires": { - "@fortawesome/fontawesome-common-types": "6.5.2" - } - }, - "@fortawesome/free-regular-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz", - "integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==", - "requires": { - "@fortawesome/fontawesome-common-types": "6.5.2" - } - }, - "@fortawesome/free-solid-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", - "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", - "requires": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.6.0" } }, "@generic-ui/fabric": { @@ -17244,6 +17312,12 @@ "@types/send": "*" } }, + "@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "peer": true + }, "@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -18210,6 +18284,12 @@ "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "dev": true }, + "component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "peer": true + }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -18673,6 +18753,11 @@ "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", "dev": true }, + "diff-match-patch-ts": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/diff-match-patch-ts/-/diff-match-patch-ts-0.6.0.tgz", + "integrity": "sha512-U0uPIJ+wJqgaBoVw2MFSFpGIk7q3mJJ+/sehbxDZFv4Gx6a1GOmrsSLmxVDDrGtRL4Q9de084aa5lVpCHn+eUw==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -20386,6 +20471,12 @@ "source-map-support": "^0.5.5" } }, + "keycharm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", + "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", + "peer": true + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -20992,6 +21083,23 @@ "tslib": "^2.3.0" } }, + "ngx-diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ngx-diff/-/ngx-diff-9.0.0.tgz", + "integrity": "sha512-eoF3D/9KjNqdnscIofkS81JWj87Ffq3eI3Gqim0SG/N/m0YWtOjximFxT1JI5bS8Gi1PVgoEQntJIZb/zaNULw==", + "requires": { + "tslib": "^2.0.0" + } + }, + "ngx-highlight-js": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ngx-highlight-js/-/ngx-highlight-js-18.0.0.tgz", + "integrity": "sha512-r/LSijb5Ju95ZGm89+4e905kFWkDt1XdGCg2prR1wleTE77uROscjZMht0D40WSRbxmOg+J2zw9OOZvsWSLeeg==", + "requires": { + "highlight.js": "^11.9.0", + "tslib": "^2.3.0" + } + }, "ngx-highlightjs": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ngx-highlightjs/-/ngx-highlightjs-12.0.0.tgz", @@ -21001,6 +21109,14 @@ "tslib": "^2.3.0" } }, + "ngx-infinite-scroll": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-18.0.0.tgz", + "integrity": "sha512-D183TDwpsd9Zl56UmItsl3RzHdN25srAISfg6lc3A8mEKkEgOq0s7ZzRAYcx8DHsAkMgtZqjIPEvMifD3DOB/g==", + "requires": { + "tslib": "^2.3.0" + } + }, "ngx-mat-select-search": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/ngx-mat-select-search/-/ngx-mat-select-search-7.0.6.tgz", @@ -23083,8 +23199,7 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "validate-npm-package-license": { "version": "3.0.4", @@ -23111,6 +23226,25 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true }, + "vis-data": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz", + "integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==", + "requires": {} + }, + "vis-network": { + "version": "9.1.9", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.9.tgz", + "integrity": "sha512-Ft+hLBVyiLstVYSb69Q1OIQeh3FeUxHJn0WdFcq+BFPqs+Vq1ibMi2sb//cxgq1CP7PH4yOXnHxEH/B2VzpZYA==", + "requires": {} + }, + "vis-util": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz", + "integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==", + "peer": true, + "requires": {} + }, "vite": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.7.tgz", diff --git a/package.json b/package.json index a390bf6..8c63917 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "MikroWizard", - "version": "1.0.1", + "version": "1.2.0", "copyright": "MikroWizard mikrowizard.com", "license": "AGPL", "author": "MikroWizard Team (https://github.com/MikroWizard)", @@ -63,6 +63,8 @@ "ngx-super-select": "^3.17.0", "rxjs": "~7.8.1", "tslib": "^2.3.0", + "vis-data": "^7.1.9", + "vis-network": "^9.1.9", "zone.js": "~0.14.4" }, "devDependencies": { diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 806ed24..2b30133 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -49,6 +49,11 @@ const routes: Routes = [ loadChildren: () => import('./views/devices_group/devgroup.module').then((m) => m.DevicesGroupModule) }, + { + path: 'maps', + loadChildren: () => + import('./views/maps/maps.module').then((m) => m.MapsModule) + }, { path: 'authlog', loadChildren: () => @@ -84,6 +89,11 @@ const routes: Routes = [ loadChildren: () => import('./views/user_tasks/user_tasks.module').then((m) => m.UserTasksModule) }, + { + path: 'cloner', + loadChildren: () => + import('./views/cloner/cloner.module').then((m) => m.ClonerModule) + }, { path: 'snippets', loadChildren: () => diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3160f40..bb74798 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -40,6 +40,7 @@ import { SidebarModule, TabsModule, UtilitiesModule, + TableModule, ModalModule } from '@coreui/angular'; @@ -85,7 +86,8 @@ export function loginStatusProviderFactory(provider: loginChecker) { CardModule, NgScrollbarModule, ModalModule, - FontAwesomeModule + FontAwesomeModule, + TableModule ], providers: [ { diff --git a/src/app/containers/default-layout/_nav.ts b/src/app/containers/default-layout/_nav.ts index 6339791..00c76ce 100644 --- a/src/app/containers/default-layout/_nav.ts +++ b/src/app/containers/default-layout/_nav.ts @@ -28,6 +28,12 @@ export const navItems: INavData[] = [ // linkProps: { fragment: 'someAnchor' }, icon: 'fa-solid fa-layer-group' }, + { + name: 'Network Maps', + url: '/maps', + icon:'fa-solid fa-map', + attributes: { 'pro':true } + }, // { // name: 'Tools', // url: '/login', @@ -60,10 +66,17 @@ export const navItems: INavData[] = [ icon: 'fa-solid fa-database' }, { - name: 'snippets', + name: 'Snippets', url: '/snippets', icon: 'fa-solid fa-code' }, + { + name: 'Sync and Cloner', + url: '/cloner', + icon: 'fa-solid fa-rotate', + attributes: { 'pro':true } + + }, { name: 'Password Vault', url: '/vault', diff --git a/src/app/containers/default-layout/default-header/default-header.component.html b/src/app/containers/default-layout/default-header/default-header.component.html index adefb87..b83982a 100644 --- a/src/app/containers/default-layout/default-header/default-header.component.html +++ b/src/app/containers/default-layout/default-header/default-header.component.html @@ -49,6 +49,7 @@ --> + | @@ -97,3 +98,45 @@ + + + + + + + + + diff --git a/src/app/containers/default-layout/default-header/default-header.component.ts b/src/app/containers/default-layout/default-header/default-header.component.ts index e957c15..cd6c204 100644 --- a/src/app/containers/default-layout/default-header/default-header.component.ts +++ b/src/app/containers/default-layout/default-header/default-header.component.ts @@ -15,6 +15,7 @@ export class DefaultHeaderComponent extends HeaderComponent { @Input() sidebarId: string = "sidebar"; @Output() UserModalEvent = new EventEmitter(); + @Output() ConfirmModalEvent = new EventEmitter(); public newMessages = new Array(4) public newTasks = new Array(5) @@ -25,7 +26,9 @@ export class DefaultHeaderComponent extends HeaderComponent { public uname: string; public fname: string; public lname: string; - public UserProfileModalVisible : boolean = false; + public ConfirmModalVisible : boolean = false; + public tasks : any = []; + public timer : any; constructor( private classToggler: ClassToggleService, @@ -53,7 +56,9 @@ export class DefaultHeaderComponent extends HeaderComponent { callParent(action:string): void { this.UserModalEvent.next(action); } - + callParentConfirm(action:string,data:any): void { + this.ConfirmModalEvent.next({action:action,data:data}); + } logout() { this.data_provider.logout().then(res => { this.router.navigate(['login']); @@ -62,6 +67,27 @@ export class DefaultHeaderComponent extends HeaderComponent { ngOnInit(): void { var _self = this; + console.log('DefaultHeaderComponent'); this.get_user_info(); + this.data_provider.get_running_tasks().then(res => { + _self.tasks = res['tasks'].filter((x:any) => x.status); + }) + // get running tasks every 5 seconds + this.timer=setInterval(function(){ + _self.get_running_tasks(); + }, 5000); } + + get_running_tasks(){ + var _self = this; + this.data_provider.get_running_tasks().then(res => { + _self.tasks = res['tasks'].filter((x:any) => x.status); + }) + } + ngOnDestroy(): void { + clearInterval(this.timer); + } + + + } diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html index ea53bfd..83af2f0 100644 --- a/src/app/containers/default-layout/default-layout.component.html +++ b/src/app/containers/default-layout/default-layout.component.html @@ -21,7 +21,7 @@
-
@@ -105,4 +105,34 @@ Close + + + + + +
Please Confirm Stopping Background Task
+
Please Confirm Stopping Background Task
+ +
+ +
+

Are you sure you want to stop the task {{ data['name'] }}?

+
+
+ Stopping this task will cause reload of other background tasks +
+
+ Clear browser cache and Reload the page or hit Ctrl+F5 to load latest Mikrofront version +
+
+ + + + + + +
\ No newline at end of file diff --git a/src/app/containers/default-layout/default-layout.component.ts b/src/app/containers/default-layout/default-layout.component.ts index 67c1d09..e59f4bd 100644 --- a/src/app/containers/default-layout/default-layout.component.ts +++ b/src/app/containers/default-layout/default-layout.component.ts @@ -6,6 +6,7 @@ import { navItems } from './_nav'; import { dataProvider } from '../../providers/mikrowizard/data'; import { arch } from 'os'; import { DomSanitizer } from '@angular/platform-browser'; +import { disconnect } from 'process'; @Component({ selector: 'app-dashboard', @@ -23,11 +24,14 @@ export class DefaultLayoutComponent implements OnInit { public ispro: boolean=false; public action: string="password"; public UserProfileModalVisible:boolean; + public ConfirmModalVisible:boolean; public error:any=false; public currentStep:number=1; public qrCode:any=false; public totpCode:string=''; public errorMessage:any=false; + public data:any={}; + public timer:any; public password:any={ 'cupass':'', 'pass1':'', @@ -130,7 +134,41 @@ export class DefaultLayoutComponent implements OnInit { else this.UserProfileModalVisible = true; } - + show_confirm_modal(data:any){ + this.data={}; + if (data.action=='CancelTask'){ + this.action=data.action; + this.data=data.data; + console.dir(this.data); + console.dir(this.action); + //disable submit button + this.data['SubmitDisable']=false; + this.ConfirmModalVisible = true; + } + if (data.action=='update'){ + this.action='update'; + this.data={}; + this.ConfirmModalVisible = true; + } + } + ConfirmAction(){ + var _self=this; + if(this.action=='CancelTask'){ + this.data_provider.stop_task(this.data['signal']).then(res => { + //disable submit button + if(res['status']=='success'){ + setTimeout(function () { + _self.ConfirmModalVisible = false; + }, 5000); + } + this.data['SubmitDisable']=true; + //wait 5 seconds before hiding the modal + }) + } + if(this.action=='update'){ + window.location.href = window.location.href.replace(/#.*$/, '') + } + } submit(){ var _self=this; if(!_self.passvalid['pass2']){ @@ -190,12 +228,31 @@ export class DefaultLayoutComponent implements OnInit { } }); }); - _self.data_provider.get_front_version().then((res:any) => { - if(res['version']!=this.version){ - console.dir("New version is available. Please refresh the page."); - window.location.href = window.location.href.replace(/#.*$/, ''); - } - }); + // check first time after 10 seconds + setTimeout(function(){ + _self.data_provider.get_front_version().then((res:any) => { + if(res['version']!=_self.version){ + console.log("New version is available. Please refresh the page."); + _self.show_confirm_modal({action:'update'}); + // window.location.href = window.location.href.replace(/#.*$/, ''); + } + }); + }, 10000); + // check for new version every 5 seconds + this.timer=setInterval(function(){ + _self.data_provider.get_front_version().then((res:any) => { + if(res['version']!=_self.version){ + console.log("New version is available. Please refresh the page."); + _self.show_confirm_modal({action:'update'}); + // window.location.href = window.location.href.replace(/#.*$/, ''); + } + }); + }, 60000); + } + clearTimer() { + clearInterval(this.timer); + this.ConfirmModalVisible = false; + } } diff --git a/src/app/providers/mikrowizard/data.ts b/src/app/providers/mikrowizard/data.ts index 8553378..6bc40f6 100644 --- a/src/app/providers/mikrowizard/data.ts +++ b/src/app/providers/mikrowizard/data.ts @@ -70,9 +70,10 @@ export class dataProvider { } return this.MikroWizardRPC.sendJsonRequest("/api/user/change_password", data); } - dashboard_stats(versioncheck:boolean){ + dashboard_stats(versioncheck:boolean,front_version:string){ var data={ - 'versioncheck':versioncheck + 'versioncheck':versioncheck, + 'front_version':front_version } return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/stats", data); } @@ -171,6 +172,12 @@ export class dataProvider { } return this.MikroWizardRPC.sendJsonRequest("/api/dev/radio/sensors", data); } + get_dev_dhcp_info(id: number){ + var data={ + 'devid':id, + } + return this.MikroWizardRPC.sendJsonRequest("/api/dev/dhcp-server/get", data); + } get_dev_ifstat(id: number,delta:string="5m",iface:string="ether1",type:string="bps") { var data={ 'devid':id, @@ -271,13 +278,14 @@ export class dataProvider { return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/update_save_group", data); } - get_snippets(name:string,desc:string,content:string,page:number=0,size:number=1000){ + get_snippets(name:string,desc:string,content:string,page:number=0,size:number=1000,limit:any=false){ var data={ 'name':name, 'description':desc, 'content':content, 'page':page, - 'size':size + 'size':size, + 'limit':limit } return this.MikroWizardRPC.sendJsonRequest("/api/snippet/list", data); } @@ -537,6 +545,90 @@ export class dataProvider { return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/save_all", data); } + get_running_tasks(){ + return this.MikroWizardRPC.sendJsonRequest("/api/tasks/list", {}); + } + stop_task(signal:number){ + var data={ + 'signal':signal + } + return this.MikroWizardRPC.sendJsonRequest("/api/tasks/stop", data); + } + apply_update(action:string){ + var data={ + 'action':action + } + return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/apply_update", data); + } + + + get_cloner_list() { + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/list", {}); + } + + Add_cloner(data:any,members:any) { + data['members']=members; + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/create", data); + } + + Delete_cloner(clonerid:number) { + var data={ + 'clonerid':clonerid, + } + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/delete", data); + } + + Edit_cloner(data:any,members:any) { + data['members']=members; + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/edit", data); + } + + get_cloner_members(clonerid:number) { + var data={ + 'clonerid':clonerid, + } + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/memberdetails", data); + } + killSession(devid:number,item:any){ + var data={ + 'devid':devid, + 'item':item + } + return this.MikroWizardRPC.sendJsonRequest("/api/dev/kill_session", data); + } + getDhcpHistory(item:any){ + var data={ + 'item':item + } + return this.MikroWizardRPC.sendJsonRequest("/api/dhcp-history/get", data); + } + + getNetworkMap(){ + return this.MikroWizardRPC.sendJsonRequest("/api/networkmap/get", {}); + } + + bulk_add_devices(devices: any[]){ + var data = { + 'devices': devices + } + return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add", data); + } + + bulk_add_status(taskId: string){ + var data = { + 'taskId': taskId + } + return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add_status", data); + } + + group_firmware_action(groupId: number, action: string){ + var data = { + 'groupId': groupId, + 'action': action + } + return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/firmware_action", data); + } + //// //// End api funcs diff --git a/src/app/views/acc_log/acc.component.html b/src/app/views/acc_log/acc.component.html index 159b290..3b1b9b8 100644 --- a/src/app/views/acc_log/acc.component.html +++ b/src/app/views/acc_log/acc.component.html @@ -1,10 +1,22 @@ - + - - Accunting Logs + +
Accunting Logs + + + +
+ Filtered Result For Device ID + {{devid}} + + Showing last 24 hours logs by default. Use filters to modify the date and time. +
- -
-
- - + + +
+
+ + + - -
- - - Start date - - - - - - End date - - - - - - Config search - - - -
-
+ +
+ + + + +
+ + + + + + +
+
+ +
+ + + + + + +
+
+ +
+ + + + +
+
+
+
+
+
@@ -87,13 +126,29 @@ {{value}} - + - - +
+ + + +
@@ -164,31 +219,80 @@ + - -
Please Confirm Action
+ +
Confirm Backup Restore
- - restore backup ? - - Are you sure that You want to Restore this configuration on - device?
-
-

- All Current device configuration will be reset:

- * All state data/history on router will be reset
- * All other local users on router will be deleted
- * After restore the password of the local user will be same as configured in MikroWizard
-

-
+ +
+ +
Restore Configuration Backup
+
+ +
+
Backup Details
+
+
Device: {{currentBackup?.devname}}
+
IP: {{currentBackup?.devip}}
+
Date: {{currentBackup?.createdC}}
+
Size: {{currentBackup?.filesize}}
+
+
+ + +
Critical Warning
+

This action will completely reset the device configuration:

+
    +
  • All current device configuration will be overwritten
  • +
  • All state data and history on router will be reset
  • +
  • All other local users on router will be deleted
  • +
  • Device will reboot and apply the restored configuration
  • +
+
- - - + +
+ + + + +
CRITICAL: Final Confirmation Required
+
+ +
+ +
DESTRUCTIVE ACTION
+

You are about to permanently overwrite device configuration

+
+ +
+

To proceed with this critical action, type CONFIRM in the box below:

+ +
+ + + + This confirmation ensures you understand the critical nature of this operation. + + +
+ + +
diff --git a/src/app/views/backups/backups.component.scss b/src/app/views/backups/backups.component.scss index ef17e02..16b7298 100644 --- a/src/app/views/backups/backups.component.scss +++ b/src/app/views/backups/backups.component.scss @@ -1,7 +1,72 @@ @import 'ngx-diff/styles/default-theme'; + ::ng-deep .modal-xl { --cui-modal-width: 90vw!important; } + ::ng-deep pre { display: block!important; +} + +// Enhanced UI Styles +.compare-panel { + background: rgba(13, 110, 253, 0.1); + border: 1px solid rgba(13, 110, 253, 0.2); + border-radius: 0.375rem; + padding: 0.5rem; +} + +.selected-item { + font-size: 0.75rem; + + .fa-times { + font-size: 0.7rem; + opacity: 0.7; + transition: opacity 0.2s; + + &:hover { + opacity: 1; + } + } +} + +.cursor-pointer { + cursor: pointer; +} + +.filter-group { + .form-label { + font-size: 0.875rem; + color: #495057; + } + + ::ng-deep .mat-mdc-form-field { + .mat-mdc-text-field-wrapper { + background-color: white; + } + } +} + +.confirmation-box { + input { + font-size: 1.1rem; + + &:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); + } + } +} + +// Action buttons spacing +.d-flex.gap-1 { + gap: 0.25rem !important; +} + +// Backup info styling +.backup-info { + .row > div { + margin-bottom: 0.5rem; + font-size: 0.9rem; + } } \ No newline at end of file diff --git a/src/app/views/backups/backups.component.ts b/src/app/views/backups/backups.component.ts index 24409fc..68a0b62 100644 --- a/src/app/views/backups/backups.component.ts +++ b/src/app/views/backups/backups.component.ts @@ -30,11 +30,13 @@ export class BackupsComponent implements OnInit { public codeForHighlightAuto: string = ""; public ispro: boolean = false; public ConfirmModalVisible: boolean = false; + public CriticalConfirmModalVisible: boolean = false; public CompareModalVisible: boolean = false; public compareitems:any=[]; public comparecontents:any=[]; public compare_type="unified"; public copy_msg:boolean=false; + public confirmationText: string = ''; constructor( private data_provider: dataProvider, @@ -186,31 +188,55 @@ export class BackupsComponent implements OnInit { this.filters_visible = !this.filters_visible; } - restore_backup(apply:boolean=false){ - var _slef=this; - if (!apply){ + restore_backup(apply: boolean = false, doubleConfirmed: boolean = false, backup?: any) { + var _self = this; + + // Set current backup if provided + if (backup) { + this.currentBackup = backup; + } + + if (!apply) { + // Step 1: Show initial confirmation this.ConfirmModalVisible = true; return; } - if (!this.currentBackup) + + if (!this.currentBackup) { return; - if(apply){ - _slef.ConfirmModalVisible = false; - _slef.BakcupModalVisible = true; - this.show_toast('Success', 'Backup restored successfully', 'success') - this.show_toast('Info', 'Wait for the router to reboot and apply config', 'info') + } + + if (apply && !doubleConfirmed) { + // Step 2: Show critical confirmation + this.ConfirmModalVisible = false; + this.CriticalConfirmModalVisible = true; + this.confirmationText = ''; + return; + } + + if (apply && doubleConfirmed) { + // Step 3: Execute restore + _self.CriticalConfirmModalVisible = false; + _self.BakcupModalVisible = false; + this.data_provider.restore_backup(this.currentBackup.id).then((res) => { - if ('status' in res){ - if(res['status']=='success'){ - this.show_toast('Success', 'Backup restored successfully', 'success') - this.show_toast('Info', 'Wait for the router to reboot and apply config', 'info') + if ('status' in res) { + if (res['status'] == 'success') { + this.show_toast('Success', 'Backup restored successfully', 'success'); + this.show_toast('Info', 'Wait for the router to reboot and apply config', 'info'); + } else { + this.show_toast('Error', 'Error restoring backup', 'danger'); } - else - this.show_toast('Error', 'Error restoring backup', 'danger') } }); } } + + cancelCriticalRestore() { + this.CriticalConfirmModalVisible = false; + this.confirmationText = ''; + this.currentBackup = null; + } start_compare(){ var _self=this; @@ -243,10 +269,20 @@ export class BackupsComponent implements OnInit { this.compareitems.push(item); } } - delete_compare(i:number){ - //delete item index i from compareitems - this.compareitems.splice(i,1); - + delete_compare(i: number) { + // Delete item index i from compareitems + this.compareitems.splice(i, 1); + } + + clearAllCompare() { + // Clear all compare items + this.compareitems = []; + this.comparecontents = []; + } + + isInCompareList(item: any): boolean { + // Check if item is already in compare list + return this.compareitems.some((compareItem: any) => compareItem.id === item.id); } reinitgrid(field: string, $event: any) { if (field == "start") this.filters["start_time"] = $event.target.value; diff --git a/src/app/views/backups/backups.module.ts b/src/app/views/backups/backups.module.ts index 9d6d18c..b24187a 100644 --- a/src/app/views/backups/backups.module.ts +++ b/src/app/views/backups/backups.module.ts @@ -12,6 +12,7 @@ import { ModalModule, FormModule, ToastModule, + AlertModule, } from "@coreui/angular"; import { BackupsRoutingModule } from "./backups-routing.module"; @@ -34,10 +35,10 @@ import { ClipboardModule } from "@angular/cdk/clipboard"; FormModule, FormsModule, ButtonModule, - ButtonModule, GuiGridModule, CollapseModule, BadgeModule, + AlertModule, Highlight, HighlightAuto, HighlightLineNumbers, diff --git a/src/app/views/cloner/cloner-routing.module.ts b/src/app/views/cloner/cloner-routing.module.ts new file mode 100644 index 0000000..b9fb90c --- /dev/null +++ b/src/app/views/cloner/cloner-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { ClonerComponent } from './cloner.component'; + +const routes: Routes = [ + { + path: '', + component: ClonerComponent, + data: { + title: $localize`synchronization and cloner` + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ClonerRoutingModule { +} diff --git a/src/app/views/cloner/cloner-styles.scss b/src/app/views/cloner/cloner-styles.scss new file mode 100644 index 0000000..dd8953e --- /dev/null +++ b/src/app/views/cloner/cloner-styles.scss @@ -0,0 +1,494 @@ +/* Modern Cloner Component Styles */ + +/* Form Sections */ +.cloner-form-section { + border-radius: 6px; + background: #f8f9fa; + padding: 0.75rem; + border: 1px solid #e9ecef; +} + +.section-header { + border-bottom: 1px solid #e9ecef; + padding-bottom: 0.5rem; +} + +.section-title { + color: #495057; + font-weight: 600; + font-size: 0.95rem; +} + +.form-input { + border-radius: 6px; + border: 1px solid #ced4da; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.form-input-sm { + border-radius: 4px; + border: 1px solid #ced4da; + padding: 0.375rem 0.5rem; + font-size: 0.85rem; + height: calc(1.5em + 0.75rem + 2px); +} + +.form-input:focus { + border-color: #0d6efd; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +.form-label-sm { + display: block; + font-size: 0.8rem; + color: #6c757d; + margin-bottom: 0.25rem; + font-weight: 500; +} + +.form-label-xs { + display: block; + font-size: 0.75rem; + color: #6c757d; + margin-bottom: 0.125rem; + font-weight: 500; +} + +.form-select-sm { + font-size: 0.85rem; + height: calc(1.8em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; +} + +.form-select-xs { + font-size: 0.8rem; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +/* Commands Configuration */ +.commands-container { + background: white; + border-radius: 8px; + border: 1px solid #dee2e6; + overflow: hidden; +} + +.commands-container-compact { + background: white; + border-radius: 6px; + border: 1px solid #dee2e6; + overflow: hidden; +} + +.commands-nav { + background: #f8f9fa; + border-bottom: 2px solid #e9ecef; + padding: 0.5rem 1rem; +} + +.commands-nav-compact { + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; + padding: 0.25rem 0.5rem; +} + +.commands-nav .nav-item { + margin-bottom: -2px; + cursor: pointer; +} + +.commands-nav .nav-link { + color: #6c757d; + border-style: none none solid; + border-width: 2px; + position: relative; + bottom: -1px; + cursor: pointer; + padding: 0.5rem 1rem; + font-size: 0.9rem; + font-weight: 500; +} + +.commands-nav .nav-link:hover, +.commands-nav .nav-link:focus { + border-color: #0d6efd; + color: #0d6efd; +} + +.commands-nav .nav-link.active { + color: #0d6efd; + background: transparent; + border-color: #0d6efd; +} + +.command-sections { + padding: 1rem; + min-height: 200px; +} + +.command-sections-compact { + padding: 0.5rem; + min-height: 120px; +} + +.command-category { + margin-bottom: 1.5rem; +} + +.command-category-compact { + margin-bottom: 0.75rem; +} + +.category-title { + color: #0d6efd; + margin-bottom: 0.75rem; + font-size: 0.9rem; + font-weight: 600; + border-bottom: 1px solid #e9ecef; + padding-bottom: 0.25rem; +} + +.category-title-compact { + color: #0d6efd; + margin-bottom: 0.375rem; + font-size: 0.8rem; + font-weight: 600; + border-bottom: 1px solid #e9ecef; + padding-bottom: 0.125rem; +} + +.commands-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 0.5rem; +} + +.commands-grid-compact { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 0.25rem; +} + +.command-item { + background: white; + border: 1px solid #e0e0e0; + border-radius: 6px; + transition: all 0.2s ease; +} + +.command-item-compact { + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; + transition: all 0.2s ease; +} + +.command-item:hover { + border-color: #0d6efd; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.command-content { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.75rem; +} + +.command-content-compact { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.25rem 0.5rem; +} + +.command-name { + font-size: 0.8rem; + font-weight: 600; + color: #495057; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + margin-right: 0.5rem; +} + +.command-name-compact { + font-size: 0.7rem; + font-weight: 600; + color: #495057; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + margin-right: 0.25rem; +} + +/* Custom Switch Styling */ +.custom-switch { + position: relative; + width: 40px; + height: 20px; + flex-shrink: 0; +} + +.custom-switch-compact { + position: relative; + width: 32px; + height: 16px; + flex-shrink: 0; +} + +.custom-switch-compact input { + display: none; +} + +.custom-switch-compact .custom-control-label { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 16px; + background: #ccc; + cursor: pointer; + transition: all 0.3s ease; +} + +.custom-switch-compact .custom-control-label::after { + content: ''; + position: absolute; + left: 2px; + top: 2px; + width: 12px; + height: 12px; + background: #fff; + border-radius: 50%; + transition: all 0.3s ease; +} + +.custom-switch-compact input:checked + .custom-control-label { + background: #0d6efd; +} + +.custom-switch-compact input:checked + .custom-control-label::after { + left: calc(100% - 14px); +} + +.custom-switch input { + display: none; +} + +.custom-control-label { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 20px; + background: #ccc; + cursor: pointer; + transition: all 0.3s ease; +} + +.custom-control-label::after { + content: ''; + position: absolute; + left: 2px; + top: 2px; + width: 16px; + height: 16px; + background: #fff; + border-radius: 50%; + transition: all 0.3s ease; +} + +.custom-switch input:checked + .custom-control-label { + background: #0d6efd; +} + +.custom-switch input:checked + .custom-control-label::after { + left: calc(100% - 18px); +} + +/* Master Device Selection */ +.master-selection { + background: white; + border-radius: 6px; + border: 1px solid #dee2e6; + padding: 0.75rem; +} + +.master-selection-compact { + background: white; + border-radius: 4px; + border: 1px solid #dee2e6; + padding: 0.5rem; +} + +.master-device { + display: flex; + align-items: center; + padding: 0.5rem; + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 6px; +} + +.master-device-compact { + display: flex; + align-items: center; + padding: 0.375rem; + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 4px; +} + +.master-info { + display: flex; + align-items: center; +} + +.no-master { + display: flex; + align-items: center; + padding: 0.5rem; + background: #f8d7da; + border: 1px solid #f5c6cb; + border-radius: 6px; +} + +.no-master-compact { + display: flex; + align-items: center; + padding: 0.375rem; + background: #f8d7da; + border: 1px solid #f5c6cb; + border-radius: 4px; +} + +/* Peers Container */ +.peers-container { + background: white; + border-radius: 8px; + border: 1px solid #dee2e6; + overflow: hidden; +} + +.empty-peers { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + text-align: center; +} + +.empty-icon { + margin-bottom: 1rem; +} + +.empty-text strong { + display: block; + margin-bottom: 0.5rem; + color: #495057; +} + +.add-members-section { + padding: 1rem; + background: #f8f9fa; + border-top: 1px solid #dee2e6; +} + +/* Modal Enhancements */ +.c-modal-header.bg-light { + background: #f8f9fa !important; + border-bottom: 1px solid #dee2e6; +} + +.c-modal-header.bg-success { + background: #198754 !important; + border-bottom: 1px solid #146c43; +} + +.c-modal-footer.bg-light { + background: #f8f9fa !important; + border-top: 1px solid #dee2e6; +} + +.c-modal-body { + max-height: 80vh; + overflow-y: auto; +} + +/* Button Groups */ +.btn-group .btn { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; +} + +.btn-group .btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .cloner-form-section { + padding: 0.75rem; + margin-bottom: 0.75rem; + } + + .commands-grid { + grid-template-columns: 1fr; + } + + .command-content { + padding: 0.5rem; + } + + .command-name { + font-size: 0.75rem; + } + + .c-modal-dialog { + margin: 0.25rem; + } + + .section-title { + font-size: 0.9rem; + } + + .commands-nav .nav-link { + padding: 0.375rem 0.75rem; + font-size: 0.8rem; + } + + .master-device, + .no-master { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } +} + +@media (max-width: 576px) { + .c-modal-footer { + flex-direction: column; + align-items: stretch; + } + + .c-modal-footer > div { + width: 100%; + text-align: center; + margin-bottom: 0.5rem; + } + + .c-modal-footer > div:last-child { + margin-bottom: 0; + } +} \ No newline at end of file diff --git a/src/app/views/cloner/cloner.component.html b/src/app/views/cloner/cloner.component.html new file mode 100644 index 0000000..67fd369 --- /dev/null +++ b/src/app/views/cloner/cloner.component.html @@ -0,0 +1,361 @@ + + + + + + + Config synchronization and cloners + + + + + + + + + + + {{value}} + + + + + {{value}} + + + + + {{value == 'twoway' ? 'Two Way' : 'Master Mode'}} + + + + + {{value ? 'Active' : 'Inactive'}} + + + + + {{value}} + + + + + + + + + + + + + + + + + +
Edit Cloner: {{SelectedCloner['name']}}
+
Add New Cloner
+ +
+ + +
+
+
Basic Information
+
+ + + + + + + + +
+ + +
+
+
Synchronization Settings
+
+ + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
Commands Configuration
+
+
+ + + +
+
{{ section.title }}
+
+
+
+ {{ command }} +
+ + +
+
+
+
+
+
+
+
+
+ + +
+
+
Master Device
+
+
+
+ + {{getMasterDeviceName()}} + Master +
+
+ + No master device selected +
+
+
+ + +
+
+
Device Management
+
+ {{SelectedMembers.length}} device(s) +
+
+ +
+
+
+ +
+
+ No peers added +

Click "Add Members" to start adding peers

+
+
+ +
+ + + +
+ + + {{value}} +
+
+
+ + + {{value}} + + + + +
+ + +
+
+
+
+
+ +
+ +
+
+
+
+ + + + + +
+ + + + +
Add Members to Cloner
+ +
+ + +
+ + + {{NewMemberRows.length}} {{SelectedCloner['pair_type']}} selected for addition + +
+ + + + +
+ + Available {{SelectedCloner['pair_type'] | titlecase}} ({{availbleMembers.length}} total) +
+
+ +
+ +
All {{SelectedCloner['pair_type']}} are already added
+

No available {{SelectedCloner['pair_type']}} to add to this cloner

+
+
+ + + +
+ + {{value}} +
+
+
+ + + {{value}} + + + + + {{value}} + + +
+
+
+
+
+ + +
+ Select {{SelectedCloner['pair_type']}} from the list above to add them to the cloner +
+
+ + +
+
+
+ + + + +
Confirm delete {{ SelectedCloner['name'] }}
+ +
+ + Are you sure that You want to delete following task ? +
+
+ + + + + + + + + + + + + +
Taks name : {{ SelectedCloner['name'] }}
Description : {{ SelectedCloner['description'] }}
Cron exec : {{ SelectedCloner['desc_cron'] }}
+
+ + + + +
+ + \ No newline at end of file diff --git a/src/app/views/cloner/cloner.component.ts b/src/app/views/cloner/cloner.component.ts new file mode 100644 index 0000000..7921d85 --- /dev/null +++ b/src/app/views/cloner/cloner.component.ts @@ -0,0 +1,483 @@ +import { Component, OnInit, OnDestroy, ViewChildren ,QueryList } from "@angular/core"; +import { dataProvider } from "../../providers/mikrowizard/data"; +import { Router } from "@angular/router"; +import { loginChecker } from "../../providers/login_checker"; +import { + GuiSelectedRow, + GuiSearching, + GuiInfoPanel, + GuiColumn, + GuiColumnMenu, + GuiPaging, + GuiPagingDisplay, + GuiRowSelectionMode, + GuiRowSelection, + GuiRowSelectionType, +} from "@generic-ui/ngx-grid"; +import { NgxSuperSelectOptions } from "ngx-super-select"; +import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform"; + + +import { ToasterComponent } from "@coreui/angular"; +import { AppToastComponent } from "../toast-simple/toast.component"; + +@Component({ + templateUrl: "cloner.component.html", + styleUrls: ["cloner.scss"], + +}) +export class ClonerComponent implements OnInit { + public uid: number; + public uname: string; + public ispro: boolean = false; + + constructor( + private data_provider: dataProvider, + private router: Router, + private login_checker: loginChecker + ) { + var _self = this; + if (!this.login_checker.isLoggedIn()) { + setTimeout(function () { + _self.router.navigate(["login"]); + }, 100); + } + this.data_provider.getSessionInfo().then((res) => { + _self.uid = res.uid; + _self.uname = res.name; + _self.ispro = res['ISPRO'] + const userId = _self.uid; + + if (res.role != "admin") { + setTimeout(function () { + _self.router.navigate(["/user/dashboard"]); + }, 100); + } + }); + //get datagrid data + function isNotEmpty(value: any): boolean { + return value !== undefined && value !== null && value !== ""; + } + } + @ViewChildren(ToasterComponent) viewChildren!: QueryList; + + public source: Array = []; + public columns: Array = []; + public loading: boolean = true; + public rows: any = []; + public SelectedCloner: any = {}; + public SelectedClonerItems: any = ""; + public EditClonerModalVisible: boolean = false; + public DeleteConfirmModalVisible: boolean = false; + public Members: any = ""; + public SelectedMembers: any = []; + public NewMemberModalVisible: boolean = false; + public availbleMembers: any = []; + public NewMemberRows: any = []; + public SelectedNewMemberRows: any; + public master: number = 0; + public active_commands:any=[]; + public tabs:any=[ + { + "name": "Network", + "sections": [ + { + "title": "IP Management", + "commands": [ + "/ip address", + "/ip cloud", + "/ip dhcp-server", + "/ip dns", + "/ip pool", + "/ip route", + "/ip vrf" + ] + }, + { + "title": "IP Services", + "commands": [ + "/ip service" + ] + } + ] + }, + { + "name": "Security", + "sections": [ + { + "title": "Firewall", + "commands": [ + "/ip firewall address-list", + "/ip firewall connection", + "/ip firewall layer7-protocol", + "/ip firewall nat", + "/ip firewall service-port", + "/ip firewall calea", + "/ip firewall filter", + "/ip firewall mangle", + "/ip firewall raw" + ] + }, + { + "title": "IPSec", + "commands": [ + "/ip ipsec identity", + "/ip ipsec key", + "/ip ipsec peer", + "/ip ipsec profile", + "/ip ipsec settings", + "/ip ipsec statistics", + "/ip ipsec proposal", + "/ip ipsec policy", + "/ip ipsec mode-config", + "/ip ipsec active-peers", + "/ip ipsec installed-sa" + ] + } + ] + }, + { + "name": "System", + "sections": [ + { + "title": "Scripts & Scheduling", + "commands": [ + "/system script", + "/system scheduler" + ] + }, + { + "title": "Time Management", + "commands": [ + "/system ntp client servers", + "/system ntp client", + "/system ntp server", + "/system clock" + ] + }, + { + "title": "RADIUS", + "commands": [ + "/radius" + ] + } + ] + }, + { + "name": "MPLS", + "sections": [ + { + "title": "MPLS Configuration", + "commands": [ + "/mpls forwarding-table", + "/mpls interface", + "/mpls ldp", + "/mpls settings", + "/mpls traffic-eng" + ] + }, + { + "title": "VPLS", + "commands": [ + "/interface vpls" + ] + } + ] + } + ]; + + public sorting = { + enabled: true, + multiSorting: true, + }; + searching: GuiSearching = { + enabled: true, + placeholder: "Search Devices", + }; + + options: Partial = { + selectionMode: "single", + actionsEnabled: false, + displayExpr: "name", + valueExpr: "id", + placeholder: "Snippet", + searchEnabled: true, + enableDarkMode: false, + }; + + public paging: GuiPaging = { + enabled: true, + page: 1, + pageSize: 10, + pageSizes: [5, 10, 25, 50], + display: GuiPagingDisplay.ADVANCED, + }; + + public columnMenu: GuiColumnMenu = { + enabled: true, + sort: true, + columnsManager: true, + }; + + toasterForm = { + autohide: true, + delay: 3000, + position: "fixed", + fade: true, + closeButton: true, + }; + + public infoPanel: GuiInfoPanel = { + enabled: true, + infoDialog: false, + columnsManager: true, + schemaManager: true, + }; + + public rowSelection: boolean | GuiRowSelection = { + enabled: true, + type: GuiRowSelectionType.CHECKBOX, + mode: GuiRowSelectionMode.MULTIPLE, + }; + activate_command(command:string){ + // add to active_commands if it not added before + if(!this.active_commands.includes(command)){ + this.active_commands.push(command); + } + else{ + this.active_commands=this.active_commands.filter((x:any)=>x!=command); + } + } + show_toast(title: string, body: string, color: string) { + const { ...props } = { ...this.toasterForm, color, title, body }; + const componentRef = this.viewChildren.first.addToast( + AppToastComponent, + props, + {} + ); + componentRef.instance["closeButton"] = props.closeButton; + } + show_new_member_form() { + this.NewMemberModalVisible = true; + var _self = this; + _self.availbleMembers = []; + this.SelectedNewMemberRows = []; + this.NewMemberRows = []; + + var data = { + group_id: false, + search: false, + page: false, + size: 10000, + }; + + if (this.SelectedCloner["pair_type"] == "devices") + _self.data_provider.get_dev_list(data).then((res) => { + _self.availbleMembers = res.filter( + (x: any) => !_self.SelectedClonerItems.includes(x.id) + ); + _self.NewMemberModalVisible = true; + }); + else + _self.data_provider.get_devgroup_list().then((res) => { + _self.availbleMembers = res.filter( + (x: any) => !_self.SelectedClonerItems.includes(x.id) + ); + _self.NewMemberModalVisible = true; + }); + } + + ngOnInit(): void { + this.initGridTable(); + } + + submit(action: string) { + var _self = this; + if(_self.master==0 && _self.SelectedCloner['direction']=='oneway'){ + _self.show_toast( + "Error", + "Master device is not selected", + "danger" + ); + return; + } + if(_self.SelectedCloner['direction']=='twoway' && _self.SelectedCloner['pair_type']=='groups'){ + _self.show_toast( + "Error", + "Using Groups is only allowed with Master Mode", + "danger" + ); + return; + } + if (_self.master!=0 && _self.SelectedCloner['direction']=='oneway'){ + _self.SelectedCloner["masterid"]=_self.master; + } + _self.SelectedCloner["active_commands"]=_self.active_commands; + if (action == "add") { + this.data_provider + .Add_cloner(_self.SelectedCloner, _self.SelectedClonerItems) + .then((res) => { + if (res.status === 'failed') { + _self.show_toast("Error", res.err, "danger"); + } else { + _self.show_toast("Success", "Cloner added successfully", "success"); + _self.initGridTable(); + _self.EditClonerModalVisible = false; + } + }); + } else { + this.data_provider + .Edit_cloner(_self.SelectedCloner, _self.SelectedClonerItems) + .then((res) => { + if (res.status === 'failed') { + _self.show_toast("Error", res.err, "danger"); + } else { + _self.show_toast("Success", "Cloner updated successfully", "success"); + _self.initGridTable(); + _self.EditClonerModalVisible = false; + } + }); + } + } + + onSelectedRowsNewMembers(rows: Array): void { + this.NewMemberRows = rows; + this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source); + } + + add_new_members() { + var _self = this; + _self.SelectedMembers = [ + ...new Set(_self.SelectedMembers.concat(_self.SelectedNewMemberRows)), + ]; + + _self.SelectedClonerItems = _self.SelectedMembers.map((x: any) => { + return x.id; + }); + + this.NewMemberModalVisible = false; + } + + set_master(id:number){ + var _self=this; + this.master=id; + // sort SelectedMembers and put master on top + this.SelectedMembers= + [ + ...new Set( + this.SelectedMembers.sort((a:any,b:any)=>{ + if(a.id==_self.master) return -1; + if(b.id==_self.master) return 1; + return 0; + }) + + ), + ]; + } + + + editAddCloner(item: any, action: string) { + if (action == "showadd") { + this.SelectedCloner = { + id: 0, + name: "", + description: "", + pair_type: "devices", + live_mode: false, + schedule: false, + cron: "", + desc_cron: "", + direction: "oneway", + members: "", + action: "add", + }; + this.master=0; + this.SelectedMembers = []; + this.SelectedClonerItems = []; + this.EditClonerModalVisible = true; + return; + } + + var _self = this; + this.SelectedCloner = { ...item }; + this.active_commands=item['active_commands']?JSON.parse(item['active_commands']):[]; + if (action != "select_change" ) { + this.SelectedCloner["action"] = "edit"; + this.data_provider.get_cloner_members(_self.SelectedCloner.id).then((res) => { + _self.SelectedMembers = res; + if(_self.SelectedCloner["master"] && _self.SelectedCloner['direction']=='oneway'){ + _self.set_master(_self.SelectedCloner["master"]); + } + _self.EditClonerModalVisible = true; + _self.SelectedClonerItems = res.map((x: any) => { + return x.id; + }); + }); + } else { + _self.SelectedMembers = []; + this.SelectedClonerItems = []; + } + } + + + in_active_commands(command:string){ + return this.active_commands.includes(command); + } + + getMasterDeviceName(): string { + const masterDevice = this.SelectedMembers.find((m: any) => m.id == this.master); + return masterDevice ? masterDevice.name : ''; + } + + onDirectionChange() { + if (this.SelectedCloner['direction'] == 'twoway') { + this.SelectedCloner['live_mode'] = true; + this.SelectedCloner['schedule'] = false; + } + this.form_changed(); + } + remove_member(item: any) { + var _self = this; + _self.SelectedMembers = _self.SelectedMembers.filter( + (x: any) => x.id != item.id + ); + _self.SelectedClonerItems = _self.SelectedMembers.map((x: any) => { + return x.id; + }); + } + + get_member_by_id(id: string) { + return this.Members.find((x: any) => x.id == id); + } + + confirm_delete(item: any = "", del: boolean = false) { + if (!del) { + this.SelectedCloner = { ...item }; + this.DeleteConfirmModalVisible = true; + } else { + var _self = this; + this.data_provider.Delete_cloner(_self.SelectedCloner["id"]).then((res) => { + _self.initGridTable(); + _self.DeleteConfirmModalVisible = false; + }); + } + } + + form_changed() { + this.editAddCloner(this.SelectedCloner, "select_change"); + } + + logger(item: any) { + console.dir(item); + } + + initGridTable(): void { + var _self = this; + this.data_provider.get_cloner_list().then((res) => { + _self.source = res.map((x: any) => { + return x; + }); + _self.loading = false; + }); + } +} diff --git a/src/app/views/cloner/cloner.module.ts b/src/app/views/cloner/cloner.module.ts new file mode 100644 index 0000000..6e78441 --- /dev/null +++ b/src/app/views/cloner/cloner.module.ts @@ -0,0 +1,49 @@ +import { NgModule } from "@angular/core"; +import { CommonModule, TitleCasePipe } from "@angular/common"; +import { FormsModule,ReactiveFormsModule } from "@angular/forms"; + +import { + ButtonModule, + CardModule, + FormModule, + GridModule, + ModalModule, + ButtonGroupModule, + ToastModule, + TooltipModule, + NavModule, + TabsModule, + BadgeModule, + AlertModule, +} from "@coreui/angular"; +import { ClonerRoutingModule } from "./cloner-routing.module"; +import { ClonerComponent } from "./cloner.component"; +import { GuiGridModule } from "@generic-ui/ngx-grid"; + +import { NgxSuperSelectModule} from "ngx-super-select"; + +@NgModule({ + imports: [ + ClonerRoutingModule, + CardModule, + CommonModule, + GridModule, + FormModule, + ButtonModule, + ButtonGroupModule, + GuiGridModule, + ModalModule, + ReactiveFormsModule, + FormsModule, + NgxSuperSelectModule, + ToastModule, + TooltipModule, + NavModule, + TabsModule, + BadgeModule, + AlertModule, + ], + declarations: [ClonerComponent], + providers: [TitleCasePipe], +}) +export class ClonerModule {} diff --git a/src/app/views/cloner/cloner.scss b/src/app/views/cloner/cloner.scss new file mode 100644 index 0000000..1daee8c --- /dev/null +++ b/src/app/views/cloner/cloner.scss @@ -0,0 +1,134 @@ +@import 'cloner-styles'; + +.nav-underline { + border-bottom: 2px solid var(--cui-nav-underline-border-color, #c4c9d0) +} + +.nav-underline .nav-item { + margin-bottom: -2px; + cursor: pointer; +} + +.nav-underline .nav-link { + color: var(--cui-nav-underline-link-color, #768192); + border-style: none none solid!important; + border-width:2px; + position:relative; + bottom:-1px; + cursor: pointer; + +} + +.nav-underline .nav-link:hover,.nav-underline .nav-link:focus { + border-color: var(--cui-nav-underline-link-active-border-color, #321fdb) +} + +.nav-underline .nav-link.active,.nav-underline .show>.nav-link { + color: var(--cui-nav-underline-link-active-color, #321fdb); + background: transparent; + border-color: var(--cui-nav-underline-link-active-border-color, #321fdb) +} + + + + + + +.custom-control-label::before, .custom-control-label::after { + top: 0.1rem; + width: 2rem; + height: 1rem; + } + + .custom-control-input:checked ~ .custom-control-label::before { + color: #fff; + border-color: #3498db; + background-color: #3498db; + } + + .custom-control-input:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); + } + + h5.cloner-sections { + color: #3498db; + margin-bottom: 5px; + font-size: 15px; + font-weight: 600; } + + .nav-link { + color: #333; + } + + .nav-link.active { + color: #3498db; + border-bottom: 2px solid #3498db; + } + + .command-sections c-card-body{ + display: flex!important; + justify-content: space-between!important; + padding:5px!important; + align-items: center!important; + justify-content: space-between!important; + border-radius: 4px; +} + +.command-sections c-card-body h6{ + margin: 0!important; + font-size: 12px!important; + color: var(--primary-color); + white-space: nowrap!important; + overflow: hidden!important; + text-overflow: ellipsis!important; + width: 80%!important; + font-weight: bold; + +} +.command-sections c-card{ + border: 1px solid #e0e0e0!important;; + border-radius: 4px!important;; +} + + /* Checkbox Styling */ + .custom-switch { + position: relative; + width: 40px; + height: 20px; + } + + .custom-switch input { + display: none; + } + + .custom-control-label { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 20px; + background: #ccc; + cursor: pointer; + transition: all 0.3s ease; + } + + .custom-control-label::after { + content: ''; + position: absolute; + left: 2px; + top: 2px; + width: 16px; + height: 16px; + background: #fff; + border-radius: 50%; + transition: all 0.3s ease; + } + + .custom-switch input:checked + .custom-control-label { + background: #3498db; + } + + .custom-switch input:checked + .custom-control-label::after { + left: calc(100% - 18px); + } \ No newline at end of file diff --git a/src/app/views/dashboard/dashboard.component.html b/src/app/views/dashboard/dashboard.component.html index 2dcc8a7..1fa301a 100644 --- a/src/app/views/dashboard/dashboard.component.html +++ b/src/app/views/dashboard/dashboard.component.html @@ -1,11 +1,23 @@ - + - + Past 24 Hour Statics - + + + + +
+
+
+
+
+
+
+
+ @@ -53,7 +65,17 @@ - + + +
+
+
+
+
+
+
+
+
Total users
@@ -120,35 +142,109 @@ - +
+
+
+
+
+ - - + +
- Serial: {{ stats['serial'] }} - Copy +

Version and Serial information

+
+
+
+
+
+
+
+
- Not Registred OR Not internet access - Learn how to register and get automatic - updates! +
+ Serial: {{ stats['serial'] }} + Copy +
+ Not Registred + License Validation failed
-
+
+
+ Serial: {{ stats['serial'] }} + Copy +
+ Unable connect to server/Check server internet connection +
+
+
+ Serial: {{ stats['serial'] }} + Copy +
Registred License Type : {{stats['license']}} + Manual update + Auto update
- - +
+ Your Mikroman version : {{stats['version']}} + + + + + Your Mikrofront version : {{front_version}} + + + + +
+

License User name is not set in settings read more!

+

Serial number not submitedread more!

- - - + + + + + +
+
+
+
+
+
+
+
+
+
+ @@ -165,4 +261,38 @@
- \ No newline at end of file + + + + +
Please Confirm Mikroman Update
+
Please Confirm MikroFront Update
+ +
+ +
+

Are you sure you want to apply latest Mikroman Update ver {{ stats['latest_version'] }}?

+

By updating Mikroman the MikroFront update is also get checked and applyed

+

If you made any special changes to configuration files or python files it will be removed automaticlaly

+
+
+ Applying the update will cause reload of the server couple of times +
+
+

Are you sure you want to apply latest MikroFront Update ver {{ stats['front_latest_version'] }}?

+
+
+ Applying the update will cause reload of the page,
+ Also please make sure you have the latest Mikroman before updating MikroFront. + Updating to latest MikroFront without getting latest Mikroman can cause problems
+
+
+ + + + +
\ No newline at end of file diff --git a/src/app/views/dashboard/dashboard.component.ts b/src/app/views/dashboard/dashboard.component.ts index 3d2099c..f5cc1a7 100644 --- a/src/app/views/dashboard/dashboard.component.ts +++ b/src/app/views/dashboard/dashboard.component.ts @@ -14,6 +14,9 @@ export class DashboardComponent implements OnInit { public uname: string; public tz: string; public copy_msg: any = false; + public ConfirmModalVisible: boolean = false; + public action: string = ""; + front_version=require('../../../../package.json').version; constructor( private data_provider: dataProvider, private router: Router, @@ -41,7 +44,9 @@ export class DashboardComponent implements OnInit { trafficRadio: new UntypedFormControl("5m"), }); public chart_data: any = {}; - Chartoptions = { + + public Chartoptions = { + responsive: true, plugins: { tooltip: { callbacks: { @@ -87,17 +92,17 @@ export class DashboardComponent implements OnInit { stacked: true, position: "left", type: "linear", - color: "#17522f", + color: "#4caf50", grid: { - color: "rgba(23, 82, 47, 0.3)", + color: "rgba(76, 175, 79, 0.3)", backgroundColor: "transparent", - borderColor: "#f86c6b", - pointHoverBackgroundColor: "#f86c6b", + borderColor: "#4caf50", + pointHoverBackgroundColor: "#4caf50", borderWidth: 1, borderDash: [8, 5], }, ticks: { - color: "#17522f", + color: "#000000", callback: function (value: any, index: any, ticks: any) { const units = ["bit", "Kib", "Mib", "Gib", "Tib"]; var res = value; @@ -119,10 +124,10 @@ export class DashboardComponent implements OnInit { position: "right", type: "linear", grid: { - color: "rgba(23, 82, 47, 0.3)", + color: "rgba(255, 152, 0, 0.4)", backgroundColor: "transparent", - borderColor: "#f86c6b", - pointHoverBackgroundColor: "#f86c6b", + borderColor: "#ff9800", + pointHoverBackgroundColor: "#ff9800", borderWidth: 1, borderDash: [8, 5], }, @@ -130,7 +135,7 @@ export class DashboardComponent implements OnInit { width: 2, }, ticks: { - color: "#171951", + color: "#000000", callback: function (value: any, index: any, ticks: any) { const units = ["bit", "Kib", "Mib", "Gib", "Tib"]; var res = value; @@ -147,7 +152,7 @@ export class DashboardComponent implements OnInit { elements: { line: { borderWidth: 1, - tension: 0.4, + tension: 0.1, }, point: { radius: 2, @@ -181,7 +186,7 @@ export class DashboardComponent implements OnInit { } initStats() { var _self = this; - this.data_provider.dashboard_stats(true).then((res) => { + this.data_provider.dashboard_stats(true,this.front_version).then((res) => { _self.stats = res; }); } @@ -200,4 +205,23 @@ export class DashboardComponent implements OnInit { this.delta = value; this.initTrafficChart(); } + showConfirmModal(action: string) { + this.action = action; + this.ConfirmModalVisible = true + } + ConfirmAction() { + var _self = this; + this.data_provider.apply_update(this.action).then((res) => { + if (res["status"]=='success') { + if (_self.action=='update_mikroman') { + _self.stats['update_inprogress']=true; + } + if (_self.action=='update_mikrofront') { + _self.stats['front_update_inprogress']=true; + } + _self.action=""; + _self.ConfirmModalVisible = false; + } + }); + } } diff --git a/src/app/views/dashboard/dashboard.module.ts b/src/app/views/dashboard/dashboard.module.ts index c605424..f1ff857 100644 --- a/src/app/views/dashboard/dashboard.module.ts +++ b/src/app/views/dashboard/dashboard.module.ts @@ -11,6 +11,7 @@ import { ProgressModule, TemplateIdDirective, BadgeModule, + ModalModule, CarouselModule, } from "@coreui/angular"; @@ -31,12 +32,12 @@ import { ClipboardModule } from "@angular/cdk/clipboard"; ReactiveFormsModule, ButtonModule, TemplateIdDirective, - ButtonModule, ButtonGroupModule, ChartjsModule, CarouselModule, BadgeModule, ClipboardModule, + ModalModule, ], declarations: [DashboardComponent], }) diff --git a/src/app/views/device_detail/active-users/active-users-routing.module.ts b/src/app/views/device_detail/active-users/active-users-routing.module.ts new file mode 100644 index 0000000..ab5a4a4 --- /dev/null +++ b/src/app/views/device_detail/active-users/active-users-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { ActiveUsersComponent } from './active-users.component'; + +const routes: Routes = [ + { + path: '', + component: ActiveUsersComponent, + data: { + title: 'Widgets' + } + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ActiveUsersRoutingModule { +} diff --git a/src/app/views/device_detail/active-users/active-users.component.html b/src/app/views/device_detail/active-users/active-users.component.html new file mode 100644 index 0000000..5dca6d7 --- /dev/null +++ b/src/app/views/device_detail/active-users/active-users.component.html @@ -0,0 +1,38 @@ + + + + +
Active Users
+
+ +
Current active users : + {{active_users.length}}
+ + + + + + + + + + + + + + + + + + + + + + + +
#AddressAttGroupNameViakill
{{i+1}}{{item['address']}}{{item['when']}}{{item['group']}}{{item['name']}}{{item['via']}}
+
+
+
+
\ No newline at end of file diff --git a/src/app/views/device_detail/active-users/active-users.component.scss b/src/app/views/device_detail/active-users/active-users.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/views/device_detail/active-users/active-users.component.ts b/src/app/views/device_detail/active-users/active-users.component.ts new file mode 100644 index 0000000..215c1d7 --- /dev/null +++ b/src/app/views/device_detail/active-users/active-users.component.ts @@ -0,0 +1,27 @@ +import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core'; +import { dataProvider } from "../../../providers/mikrowizard/data"; +@Component({ + selector: 'app-active-users', + templateUrl: './active-users.component.html', + styleUrls: ['./active-users.component.scss'], + changeDetection: ChangeDetectionStrategy.Default +}) +export class ActiveUsersComponent implements AfterContentInit { + @Input() active_users: any; + @Input() devid: number; + + constructor( + private changeDetectorRef: ChangeDetectorRef, + private data_provider: dataProvider, + ) {} + killsession(item:any){ + console.log(item); + this.data_provider.killSession(this.devid,item).then((res) => { + this.active_users = res; + }); + } + ngAfterContentInit(): void { + + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/app/views/device_detail/active-users/active-users.module.ts b/src/app/views/device_detail/active-users/active-users.module.ts new file mode 100644 index 0000000..65adf0b --- /dev/null +++ b/src/app/views/device_detail/active-users/active-users.module.ts @@ -0,0 +1,46 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ProgressModule, + NavbarModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + TooltipModule, +} from '@coreui/angular'; + +// import { WidgetsRoutingModule } from './widgets-routing.module'; +import { ActiveUsersComponent } from './active-users.component'; + +@NgModule({ + declarations: [ + ActiveUsersComponent, + ], + imports: [ + CardModule, + CommonModule, + GridModule, + ProgressModule, + FormModule, + ButtonModule, + ButtonGroupModule, + NavbarModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + TooltipModule, + ], + exports: [ + ActiveUsersComponent, + ] +}) +export class ActiveUsersModule { +} diff --git a/src/app/views/device_detail/device-info/device-info-routing.module.ts b/src/app/views/device_detail/device-info/device-info-routing.module.ts new file mode 100644 index 0000000..4d141ab --- /dev/null +++ b/src/app/views/device_detail/device-info/device-info-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { DeviceInfoComponent } from './device-info.component'; + +const routes: Routes = [ + { + path: '', + component: DeviceInfoComponent, + data: { + title: 'Widgets' + } + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class DeviceInfoRoutingModule { +} diff --git a/src/app/views/device_detail/device-info/device-info.component.html b/src/app/views/device_detail/device-info/device-info.component.html new file mode 100644 index 0000000..a20b979 --- /dev/null +++ b/src/app/views/device_detail/device-info/device-info.component.html @@ -0,0 +1,40 @@ + + + + + + + + Device offline or not accessible for MikroWizard + + + Firmware Upgrade Detected + + + Firmware Upgrade Detected + + + + {{item.key}} + {{ item.value }} + + + + + + + \ No newline at end of file diff --git a/src/app/views/device_detail/device-info/device-info.component.scss b/src/app/views/device_detail/device-info/device-info.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/views/device_detail/device-info/device-info.component.ts b/src/app/views/device_detail/device-info/device-info.component.ts new file mode 100644 index 0000000..a2d8521 --- /dev/null +++ b/src/app/views/device_detail/device-info/device-info.component.ts @@ -0,0 +1,25 @@ +import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core'; + +@Component({ + selector: 'app-device-info', + templateUrl: './device-info.component.html', + styleUrls: ['./device-info.component.scss'], + changeDetection: ChangeDetectionStrategy.Default +}) +export class DeviceInfoComponent implements AfterContentInit { + @Input() devdata: any; + constructor( + private changeDetectorRef: ChangeDetectorRef + ) {} + + checkitem(item: any) { + if (item.value && !item.key.match("sensors|id|_availble|interfaces|active_users|ping|status|created|online|syslog_configured|port")) { + return true; + } else { + return false; + } + } + ngAfterContentInit(): void { + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/app/views/device_detail/device-info/device-info.module.ts b/src/app/views/device_detail/device-info/device-info.module.ts new file mode 100644 index 0000000..3b05bf7 --- /dev/null +++ b/src/app/views/device_detail/device-info/device-info.module.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ProgressModule, + NavbarModule, + AlertModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, +} from '@coreui/angular'; +import { IconModule } from '@coreui/icons-angular'; +import { ChartjsModule } from '@coreui/angular-chartjs'; + +import { WidgetsModule } from "../../widgets/widgets.module"; + +// import { WidgetsRoutingModule } from './widgets-routing.module'; +import { DeviceInfoComponent } from './device-info.component'; + +@NgModule({ + declarations: [ + DeviceInfoComponent, + ], + imports: [ + CardModule, + AlertModule, + CommonModule, + GridModule, + ProgressModule, + FormModule, + ButtonModule, + ButtonGroupModule, + ChartjsModule, + WidgetsModule, + NavbarModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, + ], + exports: [ + DeviceInfoComponent, + ] +}) +export class DeviceInfoModule { +} diff --git a/src/app/views/device_detail/device.component.html b/src/app/views/device_detail/device.component.html index 2da3363..140e6d0 100644 --- a/src/app/views/device_detail/device.component.html +++ b/src/app/views/device_detail/device.component.html @@ -1,234 +1,321 @@ -Firmware - Update availble For This Device! -Device is updated but needs to upgrade firmware! - - - - - - - -

{{devdata['name'] }} ( {{devdata['ip'] }} )

-
- - - - - - - - - - - - -
- -
- - - - -
-
-
- - - - - - - - {{item.key}} - {{ item.value }} - - - - - - - - - - - - - - - - + + + + +

+ {{ devdata["name"] }} ( {{ devdata["ip"] }} ) +

+
+ +
+
+
- {{value}} - {{item['default-name']}} -
-
+ + - - - - {{value}} - - - - - -
{{convert_bw_human(value,'rx')}}
-
-
- - - - {{convert_bw_human(value,'tx')}} - - - - - - curr:{{value}}
- max : {{item['max-l2mtu']}} -
-
- - - - {{convert_bw_human(value,'rx')}} - - - - - - - {{convert_bw_human(value,'tx')}} - - - - - - {{value}} - - - - - - {{value}} - - - - - {{value}} - - - - - - - -
-
-
-
-
-
- - - - -
Radio data
- -
{{raddata.key}}
- + +
Loading...
+ + + + + + + + + + + + + + + + + + + + - - - - - - - - - - -
{{d.key}}{{d.value}}
+ + + + + + - + + + + + + + +
+ + + + + + + + + + {{ value }} - {{ item["default-name"] }} + + - - - - - - - - - -
{{d.key}}{{d.value}}
-
- - - - - - - - - - -
{{d.key}}{{d.value}}
-
- - - - - - - - - - -
{{d.key}}{{d.value}}
-
+ + + {{ value }} + + + + +
{{ convert_bw_human(value, "rx") }}
+
+
+ + + {{ convert_bw_human(value, "tx") }} + + + + + curr:{{ value }}
+ max : {{ item["max-l2mtu"] }} +
+
+ + + {{ convert_bw_human(value, "rx") }} + + + + + {{ convert_bw_human(value, "tx") }} + + + + + {{ value }} + + + + + {{ value }} + + + + + {{ value }} + + + + + + + + +
+
+
+
- - - - - - - - - -
Strength at rates - {{st}} -
-
+ + + + + + +
Radio data
+
+ +
{{ raddata.key }}
+ + + + + + + + + + + + +
+ {{ d.key }} + {{ d.value }}
+
+ + + + + + + + + + +
+ {{ d.key }} + {{ d.value }}
+
+ + + + + + + + + + +
+ {{ d.key }} + {{ d.value }}
+
+ + + + + + + + + + +
{{ d.key }}{{ d.value }}
+
+
+ + + + + + + + + +
+ Strength at rates + + {{ st }} +
+
+
+
+
+
- - +
+ + + + + + + + + + + + + + +
- -
{{interface_rate['name']}}
+
{{ interface_rate["name"] }}
- - + + + + + + + + + + + + + +
History for {{current_dhcp['mac-address']}}
+ +
+ + + + +
{{value}}
+
+
+ + + {{value}} + + + + + {{value}} + + + + + {{item.name}} ({{value}}) + + +
+
+ + + +
\ No newline at end of file diff --git a/src/app/views/device_detail/dhcp-info/dhcp-info.component.scss b/src/app/views/device_detail/dhcp-info/dhcp-info.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/views/device_detail/dhcp-info/dhcp-info.component.ts b/src/app/views/device_detail/dhcp-info/dhcp-info.component.ts new file mode 100644 index 0000000..31d21c9 --- /dev/null +++ b/src/app/views/device_detail/dhcp-info/dhcp-info.component.ts @@ -0,0 +1,47 @@ +import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core'; +import { dataProvider } from "../../../providers/mikrowizard/data"; +import { formatInTimeZone } from "date-fns-tz"; + +@Component({ + selector: 'app-dhcp-info', + templateUrl: './dhcp-info.component.html', + styleUrls: ['./dhcp-info.component.scss'], + changeDetection: ChangeDetectionStrategy.Default +}) +export class DhcpInfoComponent implements AfterContentInit { + @Input() dhcp_server_data: any; + @Input() small_screen: boolean=false; + @Input() columnMenu: any; + @Input() sorting: any; + @Input() searching: any; + @Input() infoPanel: any; + @Input() paging: any; + @Input() rowSelectionMode: any; + @Input() tz: any; + dhcp_history:any; + dhcp_history_modal: boolean = false; + current_dhcp:any; + constructor( + private changeDetectorRef: ChangeDetectorRef, + private data_provider: dataProvider, + ) {} + + show_history(item:any) { + var _self = this; + this.data_provider.getDhcpHistory(item).then((res) => { + _self.current_dhcp = item; + this.dhcp_history = res.map((d: any) => { + d.eventtime = formatInTimeZone( + d.eventtime.split(".")[0] + ".000Z", + _self.tz, + "yyyy-MM-dd HH:mm:ss XXX" + ); + return d; + }); + this.dhcp_history_modal=!this.dhcp_history_modal; + }); + } + ngAfterContentInit(): void { + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/app/views/device_detail/dhcp-info/dhcp-info.module.ts b/src/app/views/device_detail/dhcp-info/dhcp-info.module.ts new file mode 100644 index 0000000..d493f2a --- /dev/null +++ b/src/app/views/device_detail/dhcp-info/dhcp-info.module.ts @@ -0,0 +1,57 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ProgressModule, + NavbarModule, + AlertModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, +} from '@coreui/angular'; +import { ChartjsModule } from '@coreui/angular-chartjs'; + +import { WidgetsModule } from "../../widgets/widgets.module"; + +// import { WidgetsRoutingModule } from './widgets-routing.module'; +import { DhcpInfoComponent } from './dhcp-info.component'; +import { GuiGridModule } from "@generic-ui/ngx-grid"; + +@NgModule({ + declarations: [ + DhcpInfoComponent, + ], + imports: [ + CardModule, + AlertModule, + CommonModule, + GridModule, + ProgressModule, + FormModule, + ButtonModule, + ButtonGroupModule, + ChartjsModule, + WidgetsModule, + NavbarModule, + ModalModule, + GuiGridModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, + ], + exports: [ + DhcpInfoComponent, + ] +}) +export class DhcpInfoModule { +} diff --git a/src/app/views/device_detail/dhcp-info/dhcp-inforouting.module.ts b/src/app/views/device_detail/dhcp-info/dhcp-inforouting.module.ts new file mode 100644 index 0000000..7216364 --- /dev/null +++ b/src/app/views/device_detail/dhcp-info/dhcp-inforouting.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { DhcpInfoComponent } from './dhcp-info.component'; + +const routes: Routes = [ + { + path: '', + component: DhcpInfoComponent, + data: { + title: 'Widgets' + } + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class DhcpInfoRoutingModule { +} diff --git a/src/app/views/device_detail/ping-status/ping-status-routing.module.ts b/src/app/views/device_detail/ping-status/ping-status-routing.module.ts new file mode 100644 index 0000000..2aa3361 --- /dev/null +++ b/src/app/views/device_detail/ping-status/ping-status-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { PingStatsComponent } from './ping-status.component'; + +const routes: Routes = [ + { + path: '', + component: PingStatsComponent, + data: { + title: 'Widgets' + } + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class PingStatsRoutingModule { +} diff --git a/src/app/views/device_detail/ping-status/ping-status.component.html b/src/app/views/device_detail/ping-status/ping-status.component.html new file mode 100644 index 0000000..10a201c --- /dev/null +++ b/src/app/views/device_detail/ping-status/ping-status.component.html @@ -0,0 +1,42 @@ + + + + +
Ping status
+
+ +
Successful pings : + {{ping['successful_pings']}}
+
| Failed pings : {{ping['failed_pings']}}
+
| Avrage : + {{ping['average_ping_time']}} ms
+ + + + + + + + + + + + + + + + + + + + + +
#HostTimeStatusQualitydetails
{{i+1}}{{item['host']}}{{item['time']}} ms{{item['status']}}{{item['ping_quality']}}
+
+
+
+
\ No newline at end of file diff --git a/src/app/views/device_detail/ping-status/ping-status.component.scss b/src/app/views/device_detail/ping-status/ping-status.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/views/device_detail/ping-status/ping-status.component.ts b/src/app/views/device_detail/ping-status/ping-status.component.ts new file mode 100644 index 0000000..ec2a2b8 --- /dev/null +++ b/src/app/views/device_detail/ping-status/ping-status.component.ts @@ -0,0 +1,19 @@ +import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core'; + +@Component({ + selector: 'app-ping-stats', + templateUrl: './ping-status.component.html', + styleUrls: ['./ping-status.component.scss'], + changeDetection: ChangeDetectionStrategy.Default +}) +export class PingStatsComponent implements AfterContentInit { + @Input() ping: any; + constructor( + private changeDetectorRef: ChangeDetectorRef + ) {} + + ngAfterContentInit(): void { + + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/app/views/device_detail/ping-status/ping-status.module.ts b/src/app/views/device_detail/ping-status/ping-status.module.ts new file mode 100644 index 0000000..cc3b3a0 --- /dev/null +++ b/src/app/views/device_detail/ping-status/ping-status.module.ts @@ -0,0 +1,46 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ProgressModule, + NavbarModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + TooltipModule, +} from '@coreui/angular'; + +// import { WidgetsRoutingModule } from './widgets-routing.module'; +import { PingStatsComponent } from './ping-status.component'; + +@NgModule({ + declarations: [ + PingStatsComponent, + ], + imports: [ + CardModule, + CommonModule, + GridModule, + ProgressModule, + FormModule, + ButtonModule, + ButtonGroupModule, + NavbarModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + TooltipModule, + ], + exports: [ + PingStatsComponent, + ] +}) +export class PingStatsModule { +} diff --git a/src/app/views/device_detail/radio-data/interface-bw.component.html b/src/app/views/device_detail/radio-data/interface-bw.component.html new file mode 100644 index 0000000..250b2e4 --- /dev/null +++ b/src/app/views/device_detail/radio-data/interface-bw.component.html @@ -0,0 +1,77 @@ + + +
Radio data
+
+ +
{{raddata.key}}
+ + + + + + + + + + + + +
{{d.key}}{{d.value}}
+
+ + + + + + + + + + + +
{{d.key}}{{d.value}}
+
+ + + + + + + + + + +
{{d.key}}{{d.value}}
+
+ + + + + + + + + + +
{{d.key}}{{d.value}}
+
+
+ + + + + + + + + +
+ Strength at rates + {{st}} +
+
+
+
+
\ No newline at end of file diff --git a/src/app/views/device_detail/radio-data/radio-data-routing.module.ts b/src/app/views/device_detail/radio-data/radio-data-routing.module.ts new file mode 100644 index 0000000..53b7f99 --- /dev/null +++ b/src/app/views/device_detail/radio-data/radio-data-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { RadioDataComponent } from './radio-data.component'; + +const routes: Routes = [ + { + path: '', + component: RadioDataComponent, + data: { + title: 'Widgets' + } + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class RadioDataRoutingModule { +} diff --git a/src/app/views/device_detail/radio-data/radio-data.component.scss b/src/app/views/device_detail/radio-data/radio-data.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/views/device_detail/radio-data/radio-data.component.ts b/src/app/views/device_detail/radio-data/radio-data.component.ts new file mode 100644 index 0000000..ace8e53 --- /dev/null +++ b/src/app/views/device_detail/radio-data/radio-data.component.ts @@ -0,0 +1,24 @@ +import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core'; + +@Component({ + selector: 'app-iradio-data', + templateUrl: './radio-data.component.html', + styleUrls: ['./radio-data.component.scss'], + changeDetection: ChangeDetectionStrategy.Default +}) +export class RadioDataComponent implements AfterContentInit { + @Input() raddata: any; + constructor( + private changeDetectorRef: ChangeDetectorRef + ) {} + + objectlen(object:any){ + return Object.keys(object).length; + } + strangth_at_rate_extract(data:string){ + return data.split(','); + } + ngAfterContentInit(): void { + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/app/views/device_detail/radio-data/radio-data.module.ts b/src/app/views/device_detail/radio-data/radio-data.module.ts new file mode 100644 index 0000000..0b286bb --- /dev/null +++ b/src/app/views/device_detail/radio-data/radio-data.module.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ProgressModule, + NavbarModule, + AlertModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, +} from '@coreui/angular'; +import { IconModule } from '@coreui/icons-angular'; +import { ChartjsModule } from '@coreui/angular-chartjs'; + +import { WidgetsModule } from "../../widgets/widgets.module"; + +// import { WidgetsRoutingModule } from './widgets-routing.module'; +import { RadioDataComponent } from './radio-data.component'; + +@NgModule({ + declarations: [ + RadioDataComponent, + ], + imports: [ + CardModule, + AlertModule, + CommonModule, + GridModule, + ProgressModule, + FormModule, + ButtonModule, + ButtonGroupModule, + ChartjsModule, + WidgetsModule, + NavbarModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, + ], + exports: [ + RadioDataComponent, + ] +}) +export class RadioDataModule { +} diff --git a/src/app/views/device_logs/devlogs.component.html b/src/app/views/device_logs/devlogs.component.html index 89ffbf0..5d06c01 100644 --- a/src/app/views/device_logs/devlogs.component.html +++ b/src/app/views/device_logs/devlogs.component.html @@ -1,10 +1,22 @@ - + - - Device LOGS + +
Device LOGS + + + +
+ Filtered Result For Device ID + {{devid}} + + Showing last 24 hours logs by default. Use filters to modify the date and time. +
- - | + + size="sm" style="color: #fff;"> Scan +
- + + + + + + Group @@ -37,6 +43,10 @@ (selectedRows)="onSelectedRows($event)" [autoResizeWidth]=true> + @@ -55,7 +65,7 @@
{{value}}
-
@@ -115,9 +125,12 @@ - + Upgrade Firmware + @@ -151,8 +164,9 @@ Firmware
  • - +
  • + @@ -176,8 +190,8 @@
    Please select searching method
    - +
    @@ -256,8 +270,7 @@
    -
    - Empty username and password means system default + Empty username and password means system default configuration
    @@ -265,42 +278,68 @@ -
    Editing Group
    +
    Task History
    - - -
    Group Members :
    - - - -   {{value}} - - - -   {{value}} - - - -   {{value}} - - - - {{value}} - - - - - - - - -
    -
    -
    + +
    + + Filter by Type + + +
    + + + + + + {{getTaskTypeLabel(value)}} + + + + +
    +
    Start: {{item.started}}
    +
    End: {{item.ended}}
    +
    +
    +
    + + +
    +
    {{item.start_ip}} - {{item.end_ip}}
    +
    User: {{item.username}}
    +
    +
    +
    {{item.device_count}} devices
    +
    {{item.task_id}}
    +
    +
    +
    + + +
    + ✓ {{item.success_count}} + ✗ {{item.failed_count}} +
    +
    +
    + + + + + + +
    + + @@ -378,7 +427,8 @@ peer ip - @@ -394,4 +444,213 @@
    + + + +
    Add Devices from CSV
    + +
    + +
    +
    Upload CSV File
    +

    Please upload a CSV file containing device information with columns: IP Address, Username, Password, API Port

    + +
    +
    Preview (First 3 rows):
    + + + + + + + + + + + +
    {{header}}
    {{cell}}
    +
    Column Mapping:
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    + Loading... +
    +
    {{uploadStatus}}
    +
    +
    +
    Upload Complete
    +
    + Success: {{uploadResult.success}} devices added successfully
    + Failed: {{uploadResult.failed}} devices failed to add +
    + +
    +
    + + + + + +
    + + + + +
    Web Access Options
    + +
    + +

    Choose how to access the device:

    +
    + + +
    +
    + + + +
    + + + + +
    {{getTaskTypeLabel(selectedTaskDetails?.task_type)}} Results
    + +
    + +
    + + + Task Type: {{getTaskTypeLabel(selectedTaskDetails.task_type)}}
    + Started: {{selectedTaskDetails.started}}
    + Completed: {{selectedTaskDetails.ended}} +
    + +
    + IP Range: {{selectedTaskDetails.start_ip}} - {{selectedTaskDetails.end_ip}}
    + Username: {{selectedTaskDetails.username}} +
    +
    + Task ID: {{selectedTaskDetails.task_id}}
    + Total Devices: {{selectedTaskDetails.device_count}} +
    +
    +
    +
    +
    +
    +
    Detailed Results:
    +
    + +
    +
    + + + + + + + + + + + + + + + + + +
    IP AddressStatusError DetailsError Details
    {{result.ip}} + + {{result.added ? 'Success' : 'Failed'}} + + + {{result.failures || 'N/A'}} + + {{result.faileres || 'N/A'}} +
    + +
    + Success: {{selectedTaskDetails.success_count}} + Failed: {{selectedTaskDetails.failed_count}} +
    +
    + + + + +
    + \ No newline at end of file diff --git a/src/app/views/devices/devices.component.ts b/src/app/views/devices/devices.component.ts index 8ce0f7f..832760d 100644 --- a/src/app/views/devices/devices.component.ts +++ b/src/app/views/devices/devices.component.ts @@ -39,7 +39,7 @@ export class DevicesComponent implements OnInit, OnDestroy { public tz: string; public ispro:boolean=false; - constructor( + constructor( private data_provider: dataProvider, private route: ActivatedRoute, private router: Router, @@ -72,6 +72,7 @@ export class DevicesComponent implements OnInit, OnDestroy { @ViewChild("grid", { static: true }) gridComponent: GuiGridComponent; @ViewChildren(ToasterComponent) viewChildren!: QueryList; public source: Array = []; + public originalSource: Array = []; public columns: Array = []; public loading: boolean = true; public rows: any = []; @@ -94,6 +95,28 @@ export class DevicesComponent implements OnInit, OnDestroy { public show_pass: boolean = false; public ExecutedDataModalVisible: boolean = false; public ExecutedData: any = []; + public filteredExecutedData: any = []; + public selectedTaskType: string = 'all'; + public detailsModalVisible: boolean = false; + public selectedTaskDetails: any = null; + public detailsCurrentPage: number = 1; + public detailsPageSize: number = 10; + public detailsPaginatedResults: any[] = []; + public detailsSearchTerm: string = ''; + public filteredDetailsResults: any[] = []; + public showWebAccessModal: boolean = false; + public currentDeviceInfo: any = null; + public addDeviceModalVisible: boolean = false; + public addDeviceStep: number = 1; + public csvFile: File | null = null; + public csvData: any[] = []; + public csvHeaders: string[] = []; + public csvPreview: any[] = []; + public columnMapping = { ip: '', username: '', password: '', port: '' }; + public uploadStatus: string = 'Processing devices...'; + public uploadResult = { success: 0, failed: 0, resultFile: null }; + public currentTaskId: string = ''; + public statusCheckTimer: any; toasterForm = { autohide: true, @@ -165,14 +188,19 @@ export class DevicesComponent implements OnInit, OnDestroy { this.check_firmware(); break; case "update": - this.update_firmware(); + this.ConfirmAction = "update"; + this.ConfirmModalVisible = true; break; case "upgrade": - this.upgrade_firmware(); + this.ConfirmAction = "upgrade"; + this.ConfirmModalVisible = true; break; case "logauth": this.router.navigate(["/authlog", { devid: dev.id }]); break; + case "devlogs": + this.router.navigate(["/devlogs", { devid: dev.id }]); + break; case "logacc": this.router.navigate(["/accountlog", { devid: dev.id }]); break; @@ -180,7 +208,8 @@ export class DevicesComponent implements OnInit, OnDestroy { this.router.navigate(["/backups", { devid: dev.id }]); break; case "reboot": - this.reboot_devices(); + this.ConfirmAction = "reboot"; + this.ConfirmModalVisible = true; break; case "delete": this.ConfirmAction = "delete"; @@ -194,7 +223,7 @@ export class DevicesComponent implements OnInit, OnDestroy { this.selected_device = dev; this.data_provider.get_editform(dev.id).then((res) => { if ("error" in res) { - if (res.error.indexOf("Unauthorized")) { + if ("error" in res && res.error.indexOf("Unauthorized")) { _self.show_toast( "Error", "You are not authorized to perform this action", @@ -230,8 +259,17 @@ export class DevicesComponent implements OnInit, OnDestroy { var _self = this; this.ConfirmModalVisible = false; this.data_provider.delete_devices(this.Selectedrows).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.show_toast("Success", "Device Deleted", "success"); this.initGridTable(); + } }); } @@ -314,6 +352,14 @@ export class DevicesComponent implements OnInit, OnDestroy { }); } } + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else _self.scanwizard_step = step; }); } @@ -354,55 +400,106 @@ export class DevicesComponent implements OnInit, OnDestroy { } check_firmware() { var _self = this; + this.ConfirmModalVisible = false; this.data_provider .check_firmware(this.Selectedrows.toString()) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.show_toast("info", "Checking Firmwares", "light"); _self.ConfirmModalVisible = false; setTimeout(function () { if (_self.Selectedrows.length < 1) _self.initGridTable(); }, 1); + } }); } update_firmware() { var _self = this; + this.ConfirmModalVisible = false; this.data_provider .update_firmware(this.Selectedrows.toString()) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else if ("error" in res) { + _self.show_toast("Error", res.error, "danger"); + } + else{ _self.show_toast("info", "Updating Firmwares Sent", "light"); - _self.initGridTable(); + _self.initGridTable();} }); } upgrade_firmware() { var _self = this; + this.ConfirmModalVisible = false; this.data_provider .upgrade_firmware(this.Selectedrows.toString()) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.show_toast("info", "Upgrading Firmwares", "light"); _self.initGridTable(); + } }); } reboot_devices() { var _self = this; + this.ConfirmModalVisible = false; this.data_provider .reboot_devices(this.Selectedrows.toString()) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.show_toast("info", "Reboot sent", "light"); _self.ConfirmModalVisible = !_self.ConfirmModalVisible; _self.initGridTable(); + } }); } get_groups() { var _self = this; this.data_provider.get_devgroup_list().then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if( "status" in res && res.status == 'failed' ) _self.groups = false else _self.groups = res; + } }); } @@ -417,13 +514,23 @@ export class DevicesComponent implements OnInit, OnDestroy { }; _self.data_provider.get_dev_list(data).then((res) => { - _self.source = res.map((x: any) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ + _self.originalSource = res.map((x: any) => { if (x.upgrade_availble) _self.upgrades.push(x); if (x.update_availble) _self.updates.push(x); return x; }); + _self.source = [..._self.originalSource]; _self.device_interval(); _self.loading = false; + } }); } @@ -516,8 +623,17 @@ export class DevicesComponent implements OnInit, OnDestroy { _self.selected_device['editform']['password']="Loading ..."; if (_self.ispro && !this.show_pass){ _self.data_provider.get_device_pass(this.selected_device['id']).then((res) => { + if ("error" in res && "error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.selected_device['editform']['password']=res['password']; this.show_pass=!this.show_pass; + } }); } else{ @@ -530,7 +646,14 @@ export class DevicesComponent implements OnInit, OnDestroy { this.data_provider .scan_results() .then((res) => { - console.dir(res); + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ let index = 1; _self.ExecutedData= res.data.map((d: any) => { d.index = index; @@ -545,25 +668,273 @@ export class DevicesComponent implements OnInit, OnDestroy { _self.tz, "yyyy-MM-dd HH:mm:ss XXX" ); - d.start_ip=d.info.start_ip; - d.end_ip=d.info.end_ip; + d.start_ip=d.info.start_ip || 'N/A'; + d.end_ip=d.info.end_ip || 'N/A'; + d.task_id=d.info.task_id || 'N/A'; + d.device_count=d.info.device_count || 0; + d.username=d.info.username || 'N/A'; d.result=JSON.parse(d.result); + d.success_count = d.result.filter((r: any) => r.added === true).length; + d.failed_count = d.result.filter((r: any) => r.added === false).length; index += 1; return d; }); + _self.filteredExecutedData = [..._self.ExecutedData]; + } }); } + filterByTaskType() { + if (this.selectedTaskType === 'all') { + this.filteredExecutedData = [...this.ExecutedData]; + } else { + this.filteredExecutedData = this.ExecutedData.filter((d: any) => d.task_type === this.selectedTaskType); + } + } + showTaskDetails(task: any) { + this.selectedTaskDetails = task; + this.detailsCurrentPage = 1; + this.updateDetailsPagination(); + this.detailsModalVisible = true; + } + updateDetailsPagination() { + if (!this.selectedTaskDetails?.result) return; + + // Filter results based on search term + this.filteredDetailsResults = this.selectedTaskDetails.result.filter((result: any) => + result.ip.toLowerCase().includes(this.detailsSearchTerm.toLowerCase()) || + (result.failures && result.failures.toLowerCase().includes(this.detailsSearchTerm.toLowerCase())) || + (result.faileres && result.faileres.toLowerCase().includes(this.detailsSearchTerm.toLowerCase())) + ); + + const startIndex = (this.detailsCurrentPage - 1) * this.detailsPageSize; + const endIndex = startIndex + this.detailsPageSize; + this.detailsPaginatedResults = this.filteredDetailsResults.slice(startIndex, endIndex); + } + onDetailsPageChange(page: number) { + this.detailsCurrentPage = page; + this.updateDetailsPagination(); + } + getTotalDetailsPages(): number { + if (!this.filteredDetailsResults) return 0; + return Math.ceil(this.filteredDetailsResults.length / this.detailsPageSize); + } + onDetailsSearch() { + this.detailsCurrentPage = 1; + this.updateDetailsPagination(); + } + closeDetailsModal() { + this.detailsModalVisible = false; + this.selectedTaskDetails = null; + this.detailsPaginatedResults = []; + this.filteredDetailsResults = []; + this.detailsCurrentPage = 1; + this.detailsSearchTerm = ''; + } + filterUpdatable() { + this.source = this.originalSource.filter(device => device.update_availble); + } + filterUpgradable() { + this.source = this.originalSource.filter(device => device.upgrade_availble); + } + + clearFilter() { + this.source = [...this.originalSource]; + } + + webAccess(device: any) { + this.currentDeviceInfo = device; + if (this.ispro) { + this.showWebAccessModal = true; + } else { + this.openDirectAccess(); + } + } + + openProxyAccess() { + if (this.currentDeviceInfo?.id) { + window.open(`/api/proxy/init?devid=${this.currentDeviceInfo.id}`, '_blank'); + } else { + const ip = this.currentDeviceInfo?.ip; + if (ip) { + window.open(`/api/proxy/init?dev_ip=${ip}`, '_blank'); + } + } + this.showWebAccessModal = false; + } + + openDirectAccess() { + const ip = this.currentDeviceInfo?.ip; + if (ip) { + window.open(`http://${ip}`, '_blank'); + } + this.showWebAccessModal = false; + } + + closeWebAccessModal() { + this.showWebAccessModal = false; + } + + openAddDeviceModal() { + this.addDeviceModalVisible = true; + this.resetAddDeviceForm(); + } + + closeAddDeviceModal() { + this.addDeviceModalVisible = false; + this.resetAddDeviceForm(); + } + + resetAddDeviceForm() { + this.addDeviceStep = 1; + this.csvFile = null; + this.csvData = []; + this.csvHeaders = []; + this.csvPreview = []; + this.columnMapping = { ip: '', username: '', password: '', port: '' }; + this.uploadStatus = 'Processing devices...'; + this.uploadResult = { success: 0, failed: 0, resultFile: null }; + this.currentTaskId = ''; + clearTimeout(this.statusCheckTimer); + } + + onFileSelected(event: any) { + const file = event.target.files[0]; + if (file && file.type === 'text/csv') { + this.csvFile = file; + this.parseCSV(file); + } + } + + parseCSV(file: File) { + const reader = new FileReader(); + reader.onload = (e: any) => { + const csv = e.target.result; + const lines = csv.split('\n').filter((line: string) => line.trim()); + + if (lines.length > 0) { + this.csvHeaders = lines[0].split(',').map((header: string) => header.trim()); + this.csvData = lines.slice(1).map((line: string) => + line.split(',').map((cell: string) => cell.trim()) + ); + this.csvPreview = this.csvData.slice(0, 3); + } + }; + reader.readAsText(file); + } + + isValidMapping(): boolean { + return this.columnMapping.ip !== '' && + this.columnMapping.username !== '' && + this.columnMapping.password !== '' && + this.columnMapping.port !== '' && + this.csvData.length > 0; + } + + uploadDevices() { + if (!this.isValidMapping()) return; + + this.addDeviceStep = 2; + + const devices = this.csvData.map(row => ({ + ip: row[parseInt(this.columnMapping.ip)], + username: row[parseInt(this.columnMapping.username)], + password: row[parseInt(this.columnMapping.password)], + port: row[parseInt(this.columnMapping.port)] + })); + + this.data_provider.bulk_add_devices(devices).then((res) => { + if ('error' in res) { + this.addDeviceStep = 3; + this.show_toast('Error', 'Failed to start device upload', 'danger'); + this.uploadResult = { success: 0, failed: devices.length, resultFile: null }; + } else if ('taskId' in res) { + this.currentTaskId = res.taskId; + this.uploadStatus = 'Processing devices...'; + this.checkUploadStatus(); + } else { + this.addDeviceStep = 3; + this.show_toast('Error', 'Invalid response from server', 'danger'); + this.uploadResult = { success: 0, failed: devices.length, resultFile: null }; + } + }).catch(() => { + this.addDeviceStep = 3; + this.uploadResult = { success: 0, failed: devices.length, resultFile: null }; + this.show_toast('Error', 'Failed to upload devices', 'danger'); + }); + } + + checkUploadStatus() { + clearTimeout(this.statusCheckTimer); + + this.data_provider.bulk_add_status(this.currentTaskId).then((res) => { + if ('error' in res) { + this.addDeviceStep = 3; + this.show_toast('Error', 'Failed to check upload status', 'danger'); + this.uploadResult = { success: 0, failed: 0, resultFile: null }; + return; + } + + if (res.status === 'completed') { + this.addDeviceStep = 3; + this.uploadResult = { + success: res.success || 0, + failed: res.failed || 0, + resultFile: res.resultFile || null + }; + this.show_toast('Success', `${res.success} devices added successfully`, 'success'); + this.initGridTable(); + } else if (res.status === 'failed') { + this.addDeviceStep = 3; + this.show_toast('Error', res.message || 'Upload failed', 'danger'); + this.uploadResult = { success: 0, failed: 0, resultFile: null }; + } else { + // Still processing + this.uploadStatus = res.message || 'Processing devices...'; + this.statusCheckTimer = setTimeout(() => { + this.checkUploadStatus(); + }, 3000); + } + }).catch(() => { + this.addDeviceStep = 3; + this.show_toast('Error', 'Failed to check upload status', 'danger'); + this.uploadResult = { success: 0, failed: 0, resultFile: null }; + }); + } + + downloadResults() { + if (this.uploadResult.resultFile) { + const link = document.createElement('a'); + link.href = this.uploadResult.resultFile; + link.download = 'device_upload_results.csv'; + link.click(); + } + } + + getTaskTypeLabel(taskType: string): string { + switch(taskType) { + case 'ip-scan': return 'IP Scan'; + case 'bulk-add': return 'Bulk Add'; + default: return taskType; + } + } + + getStatusColor(success: number, failed: number): string { + if (failed === 0) return 'success'; + if (success === 0) return 'danger'; + return 'warning'; + } ngOnDestroy(): void { clearTimeout(this.scan_timer); + clearTimeout(this.statusCheckTimer); } } diff --git a/src/app/views/devices/devices.module.ts b/src/app/views/devices/devices.module.ts index f16b801..53cd568 100644 --- a/src/app/views/devices/devices.module.ts +++ b/src/app/views/devices/devices.module.ts @@ -17,6 +17,7 @@ import { ModalModule, ListGroupModule, TooltipModule, + TableModule, } from "@coreui/angular"; import { MatMenuModule } from "@angular/material/menu"; import { DevicesRoutingModule } from "./devices-routing.module"; @@ -44,6 +45,7 @@ import { GuiGridModule } from "@generic-ui/ngx-grid"; ListGroupModule, MatMenuModule, TooltipModule, + TableModule, ], declarations: [DevicesComponent], }) diff --git a/src/app/views/devices_group/devgroup.component.html b/src/app/views/devices_group/devgroup.component.html index fb48f5f..57f80a0 100644 --- a/src/app/views/devices_group/devgroup.component.html +++ b/src/app/views/devices_group/devgroup.component.html @@ -25,29 +25,67 @@ - All Devices + All Devices - 0 Members - {{value.length}} Members + + 0 + + + {{value.length}} + - - {{value}} + {{formatCreateTime(value)}} - - - - - + + + + {{value.length}} + + + + + + + + + +
    +
  • Actions Menu
  • + + + + + +
    +
    @@ -59,92 +97,146 @@ - - -
    Group Edit
    + + +
    Edit Device Group
    - - -
    - - + + +
    +
    + +
    - - -
    Group Members :
    - - - -   {{value}} - - - - {{value}} - - - - - - - - -
    - -
    -
    - + + {{groupMembers.length}} Devices + +
    + + + + +
    Current Group Members
    +
    + + +
    +
    + +
    + +
    No devices in this group
    +

    Click "Add Devices" to start adding devices to this group

    +
    +
    + + + +
    + + {{value}} +
    +
    +
    + + + {{value}} + + + + + + + +
    +
    +
    +
    - - + + - - -
    Members not in Group
    + + +
    Add Devices to Group
    - - -
    Members Availble to add:
    - - - -   {{value}} - - - - {{value}} - - - - - {{value}} - - - -
    -
    -
    + + +
    + + + {{NewMemberRows.length}} device(s) selected for addition + +
    + + + + +
    Available Devices ({{availbleMembers.length}} total)
    +
    + +
    + +
    All devices are already in groups
    +

    No available devices to add to this group

    +
    +
    + + + +
    + + {{value}} +
    +
    +
    + + + {{value}} + + + + + {{value}} + + +
    +
    +
    +
    - - - + +
    + Select devices from the list above to add them to the group +
    +
    + + +
    @@ -175,4 +267,200 @@ Close
    + + + +
    User Permissions - {{selectedGroup?.name}}
    + +
    + + + +
    Add User Permission
    +
    + + + +
    + + +
    +
    + {{user.username}} ({{user.first_name}} {{user.last_name}}) +
    +
    +
    + No users found +
    +
    +
    + +
    + + +
    +
    + {{perm.name}} +
    +
    +
    + No permissions found +
    +
    +
    + + + +
    +
    +
    + + +
    Current Permissions ({{selectedGroup?.assigned_users?.length || 0}} users)
    +
    + +
    + +

    No users have permissions for this group

    +
    +
    + + + + + + + + + + + + + + + + + + + +
    #UserNamePermissionActions
    {{i + 1}} +
    + + {{user.username}} +
    +
    {{user.first_name}} {{user.last_name}} + {{user.perm_name}} + +
    + + +
    +
    +
    +
    +
    +
    + + + +
    + + + +
    Change Permission for {{editingUser?.username}}
    + +
    + +
    + +
    +
    + + +
    +
    + + + + +
    + + + +
    Remove Permission
    + +
    + +
    + +

    Are you sure you want to remove {{removingUser?.username}}'s permission from group {{selectedGroup?.name}}?

    +

    This action cannot be undone.

    +
    +
    + + + + +
    + + +
    Confirm Firmware {{firmwareAction | titlecase}}
    + +
    + +
    + +

    Are you sure you want to {{firmwareAction}} firmware for all devices in group {{selectedGroupForFirmware?.name}}?

    +

    This action will affect all devices in this group and may take some time to complete.

    +
    +
    + + + +
    \ No newline at end of file diff --git a/src/app/views/devices_group/devgroup.component.scss b/src/app/views/devices_group/devgroup.component.scss new file mode 100644 index 0000000..c845baa --- /dev/null +++ b/src/app/views/devices_group/devgroup.component.scss @@ -0,0 +1,108 @@ +.users-summary { + min-height: 60px; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.user-badges { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.user-badges c-badge { + font-size: 0.7rem; + max-width: 80px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 768px) { + .users-summary { + min-height: auto; + } + + .user-badges c-badge { + max-width: 60px; + font-size: 0.65rem; + } +} + +/* Search Select Components */ +.search-select-wrapper { + position: relative; +} + +.search-label { + display: block; + font-size: 0.8rem; + color: #6c757d; + margin-bottom: 0.25rem; + font-weight: 500; +} + +.search-input { + width: 100%; +} + +.search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 0.375rem 0.375rem; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.search-option { + padding: 0.5rem 0.75rem; + cursor: pointer; + border-bottom: 1px solid #f1f3f4; + font-size: 0.85rem; + transition: background-color 0.2s ease; +} + +.search-option:hover { + background-color: #f8f9fa; +} + +.search-option:last-child { + border-bottom: none; +} + +.search-no-results { + padding: 0.75rem; + text-align: center; + color: #6c757d; + font-size: 0.8rem; + font-style: italic; + background: white; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 0.375rem 0.375rem; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; +} + +.compact-select { + font-size: 0.85rem; + height: calc(1.8em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; +} + +.compact-select:focus { + border-color: #0d6efd; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} \ No newline at end of file diff --git a/src/app/views/devices_group/devgroup.component.ts b/src/app/views/devices_group/devgroup.component.ts index 5ee1248..5614bfa 100644 --- a/src/app/views/devices_group/devgroup.component.ts +++ b/src/app/views/devices_group/devgroup.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { dataProvider } from "../../providers/mikrowizard/data"; import { Router } from "@angular/router"; import { loginChecker } from "../../providers/login_checker"; +import { formatInTimeZone } from "date-fns-tz"; import { GuiSearching, GuiSelectedRow, @@ -31,10 +32,12 @@ interface IUser { @Component({ templateUrl: "devgroup.component.html", + styleUrls: ["devgroup.component.scss"] }) export class DevicesGroupComponent implements OnInit { public uid: number; public uname: string; + public tz: string; constructor( private data_provider: dataProvider, @@ -50,6 +53,7 @@ export class DevicesGroupComponent implements OnInit { this.data_provider.getSessionInfo().then((res) => { _self.uid = res.uid; _self.uname = res.name; + _self.tz = res.tz; const userId = _self.uid; if (res.role != "admin") { @@ -83,6 +87,30 @@ export class DevicesGroupComponent implements OnInit { id: 0, name: "", }; + public selectedGroup: any = null; + public UserManagementModalVisible: boolean = false; + public EditPermissionModalVisible: boolean = false; + public RemovePermissionModalVisible: boolean = false; + public availableUsers: any[] = []; + public availablePermissions: any[] = []; + public selectedUserId: string = ""; + public selectedPermId: string = ""; + public selectedUser: any = null; + public selectedPermission: any = null; + public userSearch: string = ''; + public permissionSearch: string = ''; + public filteredUsers: any[] = []; + public filteredPermissions: any[] = []; + public showUserDropdown: boolean = false; + public showPermissionDropdown: boolean = false; + public editingUser: any = null; + public removingUser: any = null; + public newPermissionId: string = ""; + private deviceCache: { [key: number]: any[] } = {}; + private loadingDevices: { [key: number]: boolean } = {}; + public FirmwareConfirmModalVisible: boolean = false; + public firmwareAction: string = ""; + public selectedGroupForFirmware: any = null; public DefaultCurrentGroup: any = { array_agg: [], created: "", @@ -182,6 +210,9 @@ export class DevicesGroupComponent implements OnInit { save_group() { var _self = this; this.data_provider.update_save_group(this.currentGroup).then((res) => { + // Clear device cache for this group + delete this.deviceCache[this.currentGroup.id]; + delete this.loadingDevices[this.currentGroup.id]; _self.initGridTable(); _self.EditGroupModalVisible = false; }); @@ -237,4 +268,225 @@ export class DevicesGroupComponent implements OnInit { this.loading = false; }); } + + manageUsers(group: any): void { + this.selectedGroup = { ...group }; + this.loadAvailableUsers(); + this.loadAvailablePermissions(); + this.UserManagementModalVisible = true; + } + + loadAvailableUsers(): void { + this.data_provider.get_users(1, 1000, "").then((res) => { + this.availableUsers = res.filter((user: any) => + !this.selectedGroup.assigned_users.some((assignedUser: any) => assignedUser.user_id === user.id) + ); + this.filteredUsers = [...this.availableUsers]; + }); + } + + loadAvailablePermissions(): void { + this.data_provider.get_perms(1, 1000, "").then((res) => { + this.availablePermissions = res; + this.filteredPermissions = [...this.availablePermissions]; + }); + } + + filterUsers(event: any): void { + const query = event.target.value.toLowerCase(); + this.filteredUsers = this.availableUsers.filter((user: any) => + user.username.toLowerCase().includes(query) || + (user.first_name + ' ' + user.last_name).toLowerCase().includes(query) + ); + } + + filterPermissions(event: any): void { + const query = event.target.value.toLowerCase(); + this.filteredPermissions = this.availablePermissions.filter((perm: any) => + perm.name.toLowerCase().includes(query) + ); + } + + selectUser(user: any): void { + this.selectedUser = user; + this.selectedUserId = user.id; + this.userSearch = user.username + ' (' + user.first_name + ' ' + user.last_name + ')'; + this.showUserDropdown = false; + } + + selectPermission(perm: any): void { + this.selectedPermission = perm; + this.selectedPermId = perm.id; + this.permissionSearch = perm.name; + this.showPermissionDropdown = false; + } + + hideUserDropdown(): void { + setTimeout(() => this.showUserDropdown = false, 200); + } + + hidePermissionDropdown(): void { + setTimeout(() => this.showPermissionDropdown = false, 200); + } + + addUserPermission(): void { + if (!this.selectedUser || !this.selectedPermission) return; + + console.log('Adding user permission:', { + userId: this.selectedUser.id, + permissionId: this.selectedPermission.id, + groupId: this.selectedGroup.id, + selectedUser: this.selectedUser, + selectedPermission: this.selectedPermission + }); + + this.data_provider.Add_user_perm(this.selectedUser.id, +this.selectedPermission.id, this.selectedGroup.id) + .then((res) => { + console.log('Add user permission response:', res); + this.initGridTable(); + this.selectedUserId = ""; + this.selectedPermId = ""; + this.selectedUser = null; + this.selectedPermission = null; + this.userSearch = ""; + this.permissionSearch = ""; + // Refresh the selected group data + this.data_provider.get_devgroup_list().then((groups) => { + this.selectedGroup = groups.find((g: any) => g.id === this.selectedGroup.id); + this.loadAvailableUsers(); + }); + }); + } + + editUserPermission(user: any): void { + this.editingUser = { ...user }; + this.newPermissionId = user.perm_id.toString(); + this.EditPermissionModalVisible = true; + } + + updateUserPermission(): void { + if (!this.newPermissionId) return; + + // Remove old permission and add new one + this.data_provider.Delete_user_perm(this.editingUser.id).then(() => { + this.data_provider.Add_user_perm(this.editingUser.user_id, +this.newPermissionId, this.selectedGroup.id) + .then(() => { + this.EditPermissionModalVisible = false; + this.initGridTable(); + // Refresh the selected group data + this.data_provider.get_devgroup_list().then((groups) => { + this.selectedGroup = groups.find((g: any) => g.id === this.selectedGroup.id); + }); + }); + }); + } + + removeUserPermission(user: any): void { + this.removingUser = { ...user }; + this.RemovePermissionModalVisible = true; + } + + confirmRemovePermission(): void { + this.data_provider.Delete_user_perm(this.removingUser.id).then(() => { + this.RemovePermissionModalVisible = false; + this.initGridTable(); + // Refresh the selected group data + this.data_provider.get_devgroup_list().then((groups) => { + this.selectedGroup = groups.find((g: any) => g.id === this.selectedGroup.id); + this.loadAvailableUsers(); + }); + }); + } + + getPermissionColor(permName: string): string { + const colorMap: { [key: string]: string } = { + 'full': 'success', + 'read': 'info', + 'write': 'warning', + 'admin': 'danger', + 'test': 'secondary' + }; + return colorMap[permName] || 'primary'; + } + + getUsersTooltip(users: any[]): string { + if (users.length === 0) return 'No users assigned'; + + const maxShow = 10; + const userList = users.slice(0, maxShow).map((user, index) => + `• ${user.username} (${user.perm_name})` + ).join('\n'); + + return users.length > maxShow + ? `${userList}\n━━━━━━━━━━━━━━━━\n+${users.length - maxShow} more users` + : userList; + } + + getDevicesTooltip(group: any): string { + if (group.id === 1) return 'All devices in the system'; + if (!group.array_agg || group.array_agg[0] === null) return 'No devices assigned'; + + // Check if data is cached + if (this.deviceCache[group.id]) { + const devices = this.deviceCache[group.id]; + const maxShow = 10; + const deviceList = devices.slice(0, maxShow).map(device => + `• ${device.name} (${device.ip})` + ).join('\n'); + + return devices.length > maxShow + ? `${deviceList}\n━━━━━━━━━━━━━━━━\n+${devices.length - maxShow} more devices` + : deviceList; + } + + // Check if already loading + if (this.loadingDevices[group.id]) { + return 'Loading devices...'; + } + + // Start loading + this.loadingDevices[group.id] = true; + this.data_provider.get_devgroup_members(group.id).then((devices) => { + this.deviceCache[group.id] = devices; + this.loadingDevices[group.id] = false; + }).catch(() => { + this.loadingDevices[group.id] = false; + }); + + return 'Loading devices...'; + } + + formatCreateTime(dateString: string): string { + if (!dateString || !this.tz) return dateString; + return formatInTimeZone( + dateString.split(".")[0] + ".000Z", + this.tz, + "yyyy-MM-dd HH:mm:ss XXX" + ); + } + + groupFirmwareAction(group: any, action: string): void { + this.selectedGroupForFirmware = group; + this.firmwareAction = action; + this.FirmwareConfirmModalVisible = true; + } + + confirmGroupFirmwareAction(): void { + if (!this.selectedGroupForFirmware) return; + + this.data_provider.group_firmware_action(this.selectedGroupForFirmware.id, this.firmwareAction) + .then((res) => { + if ("error" in res) { + console.error('Firmware action failed:', res.error); + } else { + const actionText = this.firmwareAction === 'update' ? 'Update' : 'Upgrade'; + console.log(`${actionText} firmware initiated for group: ${this.selectedGroupForFirmware.name}`); + } + this.FirmwareConfirmModalVisible = false; + }) + .catch((error) => { + console.error('Firmware action error:', error); + this.FirmwareConfirmModalVisible = false; + }); + } } diff --git a/src/app/views/devices_group/devgroup.module.ts b/src/app/views/devices_group/devgroup.module.ts index 0fe8c12..1dc3e06 100644 --- a/src/app/views/devices_group/devgroup.module.ts +++ b/src/app/views/devices_group/devgroup.module.ts @@ -2,6 +2,7 @@ import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { + AlertModule, ButtonGroupModule, ButtonModule, CardModule, @@ -9,15 +10,19 @@ import { GridModule, CollapseModule, ModalModule, + TooltipModule, + ListGroupModule, } from "@coreui/angular"; import { DevicesGroupRoutingModule } from "./devgroup-routing.module"; import { DevicesGroupComponent } from "./devgroup.component"; import { GuiGridModule } from "@generic-ui/ngx-grid"; import { BadgeModule } from "@coreui/angular"; import { FormsModule } from "@angular/forms"; +import { MatMenuModule } from "@angular/material/menu"; @NgModule({ imports: [ DevicesGroupRoutingModule, + AlertModule, CardModule, CommonModule, GridModule, @@ -29,6 +34,9 @@ import { FormsModule } from "@angular/forms"; CollapseModule, ModalModule, BadgeModule, + TooltipModule, + MatMenuModule, + ListGroupModule, ], declarations: [DevicesGroupComponent], }) diff --git a/src/app/views/maps/code-typescript.txt b/src/app/views/maps/code-typescript.txt new file mode 100644 index 0000000..64f7093 --- /dev/null +++ b/src/app/views/maps/code-typescript.txt @@ -0,0 +1,377 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core"; +import { dataProvider } from "../../providers/mikrowizard/data"; +import { loginChecker } from "../../providers/login_checker"; +import { Router } from "@angular/router"; +import { formatInTimeZone } from "date-fns-tz"; +import { Network } from 'vis-network/peer'; +import { DataSet } from 'vis-data'; + +@Component({ + templateUrl: "maps.component.html", + styleUrls: ["maps.component.scss"], +}) +export class MapsComponent implements OnInit { + public uid: number; + public uname: string; + public ispro: boolean = false; + public tz: string; + public savedPositions: any = {}; + public savedPositionsKey = "network-layout"; + public selectedDevice: any = null; + constructor( + private data_provider: dataProvider, + private router: Router, + private login_checker: loginChecker + ) { + var _self = this; + if (!this.login_checker.isLoggedIn()) { + setTimeout(function () { + _self.router.navigate(["login"]); + }, 100); + } + this.data_provider.getSessionInfo().then((res) => { + _self.uid = res.uid; + _self.uname = res.name; + _self.ispro = res.ISPRO; + if (!_self.ispro) + setTimeout(function () { + _self.router.navigate(["dashboard"]); + }, 100); + _self.tz = res.tz; + }); + } + + @ViewChild('network', { static: true }) networkContainer: ElementRef | undefined; + + mikrotikData: any[] = []; + + + + + ngOnInit(): void { + this.loadFontAwesome(); + this.savedPositions = JSON.parse(localStorage.getItem(this.savedPositionsKey) || "{}"); + this.loadNetworkData(); + } + + loadNetworkData(): void { + this.data_provider.getNetworkMap().then((res) => { + this.mikrotikData = res; + console.dir(res); + setTimeout(() => { + this.createNetworkMap(); + }, 100); + }); + } + + loadFontAwesome() { + if (!document.querySelector('link[href*="font-awesome"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'; + document.head.appendChild(link); + } + } + +createNetworkMap() { + const container = this.networkContainer?.nativeElement; + if (!container) return; + + let nodes = new DataSet([]); + let edges = new DataSet([]); + let deviceMap: { [key: string]: string } = {}; // uniqueId to nodeId mapping + let allDevices: { [key: string]: any } = {}; // uniqueId to device info mapping + let macToDevice: { [key: string]: string } = {}; // MAC -> uniqueId mapping + const hasSavedPositions = Object.keys(this.savedPositions).length > 0; + let nodeIdCounter = 1; + const getUniqueId = (obj: any): string => { + if (obj.device_id) return `dev_${obj.device_id}`; + if (obj.mac) return `mac_${obj.mac}`; + if (obj.software_id) return `sw_${obj.software_id}`; + if (obj.hostname) return `host_${obj.hostname}`; + return `unknown_${obj.address || Math.random().toString(36).slice(2)}`; + }; + // Collect all devices + this.mikrotikData.forEach((device) => { + const deviceId = device.device_id || `${device.name}_${Date.now()}`; + + if (!allDevices[deviceId]) { + allDevices[deviceId] = { + name: device.name, + type: 'Router', + brand: 'MikroTik' + }; + } + + Object.entries(device.interfaces).forEach(([_, iface]: [string, any]) => { + if (iface.mac) { + macToDevice[iface.mac] = deviceId; + } + + iface.neighbors.forEach((neighbor: any) => { + const neighborId = + neighbor.device_id || + neighbor.software_id || + `${neighbor.hostname}_${neighbor.mac}_${neighbor.address || 'unknown'}`; + + if (!allDevices[neighborId]) { + allDevices[neighborId] = { + name: neighbor.hostname || neighbor.mac || 'Unknown', + type: neighbor.type || 'Router', + brand: neighbor.brand || 'MikroTik' + }; + } + + if (neighbor.mac) { + macToDevice[neighbor.mac] = neighborId; + } + }); + }); + }); + + // Create nodes + Object.entries(allDevices).forEach(([uniqueId, device]: [string, any]) => { + const nodeId = `node_${nodeIdCounter++}`; + deviceMap[uniqueId] = nodeId; + + nodes.add({ + id: nodeId, + label: device.name, + shape: 'image', + image: this.getDeviceIcon(device.type || 'Unknown', device.brand || 'Unknown'), + size: 15, + font: { size: 11, color: '#333', face: 'Arial, sans-serif' }, + ...(hasSavedPositions && this.savedPositions[nodeId] + ? { x: this.savedPositions[nodeId].x, y: this.savedPositions[nodeId].y } + : {}) + } as any); + }); + + // Create edges + this.mikrotikData.forEach((device) => { + Object.entries(device.interfaces).forEach(([ifaceName, iface]: [string, any]) => { + const sourceDeviceId = macToDevice[iface.mac]; + iface.neighbors.forEach((neighbor: any) => { + const targetDeviceId = macToDevice[neighbor.mac]; + + if (deviceMap[sourceDeviceId] && deviceMap[targetDeviceId]) { + const edgeId = `${sourceDeviceId}_${targetDeviceId}`; + const reverseId = `${targetDeviceId}_${sourceDeviceId}`; + + if (!edges.get().find(e => e.id === edgeId || e.id === reverseId)) { + edges.add({ + id: edgeId, + from: deviceMap[sourceDeviceId], + to: deviceMap[targetDeviceId], + label: ifaceName, + color: { color: '#34495e', highlight: '#3498db' }, + width: 3, + smooth: { type: 'continuous', roundness: 0.1 }, + font: { + size: 12, + color: '#2c3e50', + face: 'Arial, sans-serif', + strokeWidth: 1, + strokeColor: '#ffffff', + align: 'horizontal' + } + } as any); + } + } + }); + }); + }); + + const data = { nodes, edges }; + const options = { physics: { enabled: true, stabilization: { iterations: 100 }, barnesHut: { gravitationalConstant: -8000, centralGravity: 0.3, springLength: 200, springConstant: 0.04, damping: 0.09 } }, interaction: { hover: true, dragNodes: true, dragView: true, zoomView: true, hoverConnectedEdges: false, selectConnectedEdges: false, navigationButtons: false, keyboard: false }, nodes: { borderWidth: 3, shadow: true }, edges: { shadow: true, smooth: true, length: 150 }, manipulation: { enabled: false } }; + const network = new Network(container, data, options); + + // Keep your existing events (dragEnd, click, stabilization, etc.) + // No changes needed below + network.on('dragEnd', () => { + const positions = network.getPositions(); + this.savedPositions = positions; + localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions)); + }); + + network.on('click', (event: any) => { + if (event.nodes[0]) { + const clickedNode = nodes.get(event.nodes[0]); + const canvasPosition = network.canvasToDOM(event.pointer.canvas); + const containerRect = container.getBoundingClientRect(); + const mainContainer = document.querySelector('.main-container') as HTMLElement; + const mainRect = mainContainer?.getBoundingClientRect() || containerRect; + + let adjustedX = canvasPosition.x + containerRect.left - mainRect.left + 20; + let adjustedY = canvasPosition.y + containerRect.top - mainRect.top - 50; + + const popupWidth = 280; + const popupHeight = 200; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + if (adjustedX + popupWidth > viewportWidth) adjustedX -= popupWidth + 40; + if (adjustedY + popupHeight > viewportHeight) adjustedY -= popupHeight + 40; + if (adjustedX < 20) adjustedX = 20; + if (adjustedY < 20) adjustedY = 20; + + this.handleNodeClick(clickedNode, { x: adjustedX, y: adjustedY }); + } + }); + + network.on('stabilizationIterationsDone', () => { + network.fit(); + if (!hasSavedPositions) { + const positions = network.getPositions(); + this.savedPositions = positions; + localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions)); + } + }); + + if (hasSavedPositions) { + setTimeout(() => network.fit(), 500); + } +} + + handleNodeClick(node: any, position?: { x: number, y: number }) { + this.selectedDevice = node; + if (position) { + this.selectedDevice.popupPosition = position; + } + } + + closeDeviceDetails() { + this.selectedDevice = null; + } + + getDeviceInterfaces() { + if (!this.selectedDevice) return []; + + const device = this.mikrotikData.find(d => d.name === this.selectedDevice.label); + if (!device) return []; + + return Object.entries(device.interfaces).map(([name, data]: [string, any]) => ({ + name, + address: data.address, + mac: data.mac + })); + } + + getNodeColor(deviceType: string): string { + const colors = { + 'gateway': '#dc3545', + 'router': '#fd7e14', + 'switch': '#6f42c1', + 'ap': '#20c997', + 'cpe': '#0dcaf0' + }; + return (colors as any)[deviceType] || '#6c757d'; + } + + getNodeBorderColor(deviceType: string): string { + const borderColors = { + 'gateway': '#b02a37', + 'router': '#e8681a', + 'switch': '#59359a', + 'ap': '#1aa179', + 'cpe': '#0baccc' + }; + return (borderColors as any)[deviceType] || '#495057'; + } + + getDeviceIcon(deviceType: string, brand: string): string { + const basePath = './assets/Network-Icons-SVG/'; + const type = deviceType.toLowerCase(); + const brandName = brand.toLowerCase(); + + // MikroTik devices + if (brandName === 'mikrotik') { + if (type === 'switch') { + return `${basePath}cumulus-switch-v2.svg`; + } + return `${basePath}cumulus-router-v2.svg`; + } + + // Cisco devices + if (brandName === 'cisco') { + if (type === 'switch') { + return `${basePath}cisco-switch-l2.svg`; + } + return `${basePath}cisco-router.svg`; + } + + // Juniper devices + if (brandName === 'juniper') { + if (type === 'switch') { + return `${basePath}juniper-switch-l2.svg`; + } + return `${basePath}juniper-router.svg`; + } + + // HPE/Aruba devices + if (brandName === 'hpe/aruba' || brandName === 'aruba' || brandName === 'hpe') { + if (type === 'server') { + return `${basePath}generic-server-1.svg`; + } + return `${basePath}arista-switch.svg`; + } + + // Ubiquiti devices + if (brandName === 'ubiquiti' || brandName === 'ubnt') { + if (type === 'switch') { + return `${basePath}generic-switch-l2-v1-colour.svg`; + } + return `${basePath}generic-router-colour.svg`; + } + + // Default icons by type + const defaultIcons = { + 'switch': `${basePath}generic-switch-l2-v1-colour.svg`, + 'router': `${basePath}generic-router-colour.svg`, + 'router/switch': `${basePath}generic-router-colour.svg`, + 'server': `${basePath}generic-server-1.svg`, + 'unknown': `${basePath}generic-router-colour.svg` + }; + + return (defaultIcons as any)[type] || `${basePath}generic-router-colour.svg`; + } + + getDefaultPosition(deviceName: string, index: number): { x: number, y: number } { + const positions = { + 'Core Router': { x: 0, y: 0 }, + 'Edge Router': { x: -200, y: -100 }, + 'Distribution Switch': { x: 200, y: -100 }, + 'Access Point 1': { x: 100, y: 100 }, + 'Access Point 2': { x: 300, y: 100 }, + 'Customer Router 1': { x: 0, y: 200 }, + 'Customer Router 2': { x: 200, y: 200 } + }; + return (positions as any)[deviceName] || { x: index * 100, y: index * 50 }; + } + + webAccess() { + if (!this.selectedDevice) return; + const device = this.mikrotikData.find(d => d.name === this.selectedDevice.label); + if (device) { + const firstInterface = Object.values(device.interfaces)[0] as any; + const ip = firstInterface.address.split('/')[0]; + window.open(`http://${ip}`, '_blank'); + } + } + + showMoreInfo() { + console.log('More info for:', this.selectedDevice); + // Implement modal or detailed view + } + + pingDevice() { + console.log('Ping device:', this.selectedDevice); + // Implement ping functionality + } + + configureDevice() { + console.log('Configure device:', this.selectedDevice); + // Implement configuration interface + } + +} diff --git a/src/app/views/maps/maps-routing.module.ts b/src/app/views/maps/maps-routing.module.ts new file mode 100644 index 0000000..db9975a --- /dev/null +++ b/src/app/views/maps/maps-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { MapsComponent } from './maps.component'; + +const routes: Routes = [ + { + path: '', + component: MapsComponent, + data: { + title: $localize`Maps Wall` + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MapsRoutingModule { +} diff --git a/src/app/views/maps/maps.component.html b/src/app/views/maps/maps.component.html new file mode 100644 index 0000000..3319749 --- /dev/null +++ b/src/app/views/maps/maps.component.html @@ -0,0 +1,82 @@ + + + + +
    +
    +
    +
    + +
    +
    +
    + {{ selectedDevice.label }} + +
    + +
    +
    +
    Type: {{getDeviceInfo()?.type}} ({{getDeviceInfo()?.brand}})
    +
    Board: {{getDeviceInfo()?.board}}
    +
    Version: {{getDeviceInfo()?.version}}
    +
    System: {{getDeviceInfo()?.systemDescription}}
    +
    IP: {{getPrimaryIP()}}
    +
    Neighbors: {{getNeighborCount()}}
    +
    + +
    +
    +
    + {{ interface.name }} + ({{ interface.neighbors }} neighbors) +
    + {{ interface.address }} +
    +
    + +
    + + + +
    +
    +
    +
    + + + + +
    Web Access Options
    + +
    + +

    Choose how to access the device:

    +
    + + +
    +
    + + + +
    diff --git a/src/app/views/maps/maps.component.scss b/src/app/views/maps/maps.component.scss new file mode 100644 index 0000000..26682f1 --- /dev/null +++ b/src/app/views/maps/maps.component.scss @@ -0,0 +1,256 @@ +:host { + .network-container { + height: calc(100vh - 165px); + margin: 0; + position: relative; + } + + .network-col { + padding: 0; + } + + .network-card { + height: 100%; + border: none; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + border-radius: 12px; + overflow: hidden; + position: relative; + } + + .refresh-btn { + position: absolute; + top: 10px; + right: 10px; + z-index: 1000; + padding: 6px 12px; + font-size: 12px; + } + + .network-canvas { + width: 100%; + height: calc(100vh - 200px); + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 8px; + border: 1px solid #dee2e6; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + + ::ng-deep .vis-network { + cursor: default; + + .vis-item { + cursor: pointer; + + &:hover { + cursor: pointer; + } + + &:active { + cursor: grabbing; + } + } + } + } + + .floating-sidebar { + position: fixed; + z-index: 10000; + animation: slideIn 0.3s ease; + } + + .device-panel { + background: rgba(44, 62, 80, 0.95); + backdrop-filter: blur(10px); + border-radius: 8px; + border: 1px solid rgba(52, 73, 94, 0.8); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + width: 280px; + max-height: calc(100vh - 40px); + color: white; + display: flex; + flex-direction: column; + } + + .panel-header { + padding: 12px 16px; + border-bottom: 1px solid rgba(52, 73, 94, 0.5); + display: flex; + justify-content: space-between; + align-items: center; + } + + .device-name { + font-weight: 600; + font-size: 14px; + color: #ecf0f1; + } + + .close-btn { + color: #bdc3c7; + border: none; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: rgba(255, 255, 255, 0.2); + color: white; + } + } + + .panel-content { + padding: 16px; + overflow-y: auto; + flex: 1; + + /* Override global scrollbar styles */ + scrollbar-width: thin !important; + scrollbar-color: #bdc3c7 rgba(52, 73, 94, 0.3) !important; + + /* Custom scrollbar styling for webkit */ + &::-webkit-scrollbar { + width: 8px !important; + } + + &::-webkit-scrollbar-track { + background: rgba(52, 73, 94, 0.3) !important; + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb { + background: #bdc3c7 !important; + border-radius: 4px; + + &:hover { + background: #ecf0f1 !important; + } + } + } + + .interfaces-section { + margin-bottom: 16px; + margin-top: 3px; + + .interface-row { + max-height: none; + } + } + + .section-title { + font-size: 12px; + font-weight: 600; + color: #95a5a6; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .interface-row { + display: flex; + flex-direction: column; + padding: 1px 0; + border-bottom: 1px solid rgba(52, 73, 94, 0.3); + font-size: 12px; + gap: 2px; + + &:last-child { + border-bottom: none; + } + } + + .if-header { + display: flex; + justify-content: space-between; + align-items: center; + } + + .if-name { + color: #3498db; + font-weight: 500; + } + + .if-ip { + color: #ecf0f1; + font-family: monospace; + word-break: break-all; + } + + .if-neighbors { + color: #95a5a6; + font-size: 11px; + } + + .actions-section { + display: flex; + gap: 8px; + + &:has(button:only-child) { + button { + flex: 1; + } + } + + &:not(:has(button:only-child)) { + display: grid; + grid-template-columns: 1fr 1fr; + } + } + + .compact-btn { + padding: 6px 8px; + font-size: 11px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 4px; + justify-content: center; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + } + } + + @keyframes slideIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } + } +} + +.mdc-line-ripple.mdc-line-ripple--deactivating.ng-star-inserted { + display: none!important; +} + +::ng-deep .main-container{ + padding:0!important; + margin-top:-10px; +} + +::ng-deep .header{ + margin-bottom: 0.9rem!important; +} + +@media only screen and (max-width: 768px) { + :host .floating-sidebar { + position: fixed; + top: 10px; + right: 10px; + left: 10px; + width: auto; + } + + :host .device-panel { + width: 100%; + } +} \ No newline at end of file diff --git a/src/app/views/maps/maps.component.ts b/src/app/views/maps/maps.component.ts new file mode 100644 index 0000000..d545e8a --- /dev/null +++ b/src/app/views/maps/maps.component.ts @@ -0,0 +1,479 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core"; +import { dataProvider } from "../../providers/mikrowizard/data"; +import { loginChecker } from "../../providers/login_checker"; +import { Router } from "@angular/router"; +import { formatInTimeZone } from "date-fns-tz"; +import { Network } from 'vis-network/peer'; +import { DataSet } from 'vis-data'; + +@Component({ + templateUrl: "maps.component.html", + styleUrls: ["maps.component.scss"], +}) +export class MapsComponent implements OnInit { + public uid: number; + public uname: string; + public ispro: boolean = false; + public tz: string; + public savedPositions: any = {}; + public savedPositionsKey = "network-layout"; + public selectedDevice: any = null; + public showWebAccessModal: boolean = false; + public showMoreInfoModal: boolean = false; + public currentDeviceInfo: any = null; + constructor( + private data_provider: dataProvider, + private router: Router, + private login_checker: loginChecker + ) { + var _self = this; + if (!this.login_checker.isLoggedIn()) { + setTimeout(function () { + _self.router.navigate(["login"]); + }, 100); + } + this.data_provider.getSessionInfo().then((res) => { + _self.uid = res.uid; + _self.uname = res.name; + _self.ispro = res.ISPRO; + if (!_self.ispro) + setTimeout(function () { + _self.router.navigate(["dashboard"]); + }, 100); + _self.tz = res.tz; + }); + } + + @ViewChild('network', { static: true }) networkContainer: ElementRef | undefined; + + mikrotikData: any[] = []; + + ngOnInit(): void { + this.loadFontAwesome(); + this.savedPositions = JSON.parse(localStorage.getItem(this.savedPositionsKey) || "{}"); + this.loadNetworkData(); + } + + loadNetworkData(): void { + this.data_provider.getNetworkMap().then((res) => { + this.mikrotikData = res; + console.dir(res); + setTimeout(() => { + this.createNetworkMap(); + }, 100); + }); + } + + refreshData(): void { + this.selectedDevice = null; + this.loadNetworkData(); + } + + + + loadFontAwesome() { + if (!document.querySelector('link[href*="font-awesome"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'; + document.head.appendChild(link); + } + } + + createNetworkMap() { + const container = this.networkContainer?.nativeElement; + if (!container) return; + + let nodes = new DataSet([]); + let edges = new DataSet([]); + let deviceMap: { [key: string]: string } = {}; // uniqueId (hostname) to nodeId mapping + let allDevices: { [key: string]: any } = {}; // uniqueId to device info mapping + let macToDevice: { [key: string]: string } = {}; // MAC -> uniqueId (hostname) mapping + const hasSavedPositions = Object.keys(this.savedPositions).length > 0; + let nodeIdCounter = 1; + + // Collect all devices using hostname as consistent unique ID + this.mikrotikData.forEach((device) => { + const deviceId = device.name; // Use name (hostname) as unique ID + + if (!allDevices[deviceId]) { + allDevices[deviceId] = { + name: device.name, + type: 'Router', + brand: 'MikroTik' + }; + } + + Object.entries(device.interfaces).forEach(([_, iface]: [string, any]) => { + if (iface.mac) { + macToDevice[iface.mac] = deviceId; // Map to hostname + } + + if (iface.neighbors && Array.isArray(iface.neighbors)) { + iface.neighbors.forEach((neighbor: any) => { + const neighborId = neighbor.hostname || 'Unknown'; // Use hostname + + if (!allDevices[neighborId]) { + allDevices[neighborId] = { + name: neighbor.hostname || neighbor.mac || 'Unknown', + type: neighbor.type || 'Router', + brand: neighbor.brand || 'MikroTik' + }; + } + + if (neighbor.mac) { + macToDevice[neighbor.mac] = neighborId; // Map to hostname + } + }); + } + }); + }); + + // Create nodes + Object.entries(allDevices).forEach(([uniqueId, device]: [string, any]) => { + const nodeId = `node_${nodeIdCounter++}`; + deviceMap[uniqueId] = nodeId; + + nodes.add({ + id: nodeId, + label: device.name, + shape: 'image', + image: this.getDeviceIcon(device.type || 'Unknown', device.brand || 'Unknown'), + size: 15, + font: { size: 11, color: '#333', face: 'Arial, sans-serif' }, + ...(hasSavedPositions && this.savedPositions[nodeId] + ? { x: this.savedPositions[nodeId].x, y: this.savedPositions[nodeId].y } + : {}) + } as any); + }); + +// Create edges - collect all connections first +let connectionMap: { [key: string]: string[] } = {}; + +this.mikrotikData.forEach((device) => { + const deviceName = device.name; + Object.entries(device.interfaces).forEach(([ifaceName, iface]: [string, any]) => { + const sourceDeviceId = iface.mac ? macToDevice[iface.mac] : deviceName; + + if (iface.neighbors && Array.isArray(iface.neighbors)) { + iface.neighbors.forEach((neighbor: any) => { + const targetDeviceId = neighbor.mac ? macToDevice[neighbor.mac] : null; + + if (deviceMap[sourceDeviceId] && targetDeviceId && deviceMap[targetDeviceId]) { + const connectionKey = [sourceDeviceId, targetDeviceId].sort().join('_'); + const interfacePair = neighbor.interface ? `${ifaceName}↔${neighbor.interface}` : ifaceName; + + if (!connectionMap[connectionKey]) { + connectionMap[connectionKey] = []; + } + + if (!connectionMap[connectionKey].includes(interfacePair)) { + connectionMap[connectionKey].push(interfacePair); + } + } + }); + } + }); +}); + +// Create edges with combined labels +Object.entries(connectionMap).forEach(([connectionKey, interfacePairs]) => { + const [sourceDeviceId, targetDeviceId] = connectionKey.split('_'); + let edgeLabel = interfacePairs.join('\n'); + + // Limit to max 2 interface pairs to avoid overcrowding + if (interfacePairs.length > 2) { + edgeLabel = interfacePairs.slice(0, 2).join('\n') + '\n+' + (interfacePairs.length - 2); + } + + edges.add({ + id: connectionKey, + from: deviceMap[sourceDeviceId], + to: deviceMap[targetDeviceId], + label: edgeLabel, + color: { color: '#34495e', highlight: '#3498db' }, + width: 3, + smooth: { type: 'continuous', roundness: 0.1 }, + font: { + size: 9, + color: '#2c3e50', + face: 'Arial, sans-serif', + strokeWidth: 2, + strokeColor: '#ffffff', + align: 'horizontal' + } + } as any); +}); + const data = { nodes, edges }; + const options = { physics: { enabled: true, stabilization: { iterations: 100 }, barnesHut: { gravitationalConstant: -8000, centralGravity: 0.3, springLength: 200, springConstant: 0.04, damping: 0.09 } }, interaction: { hover: true, dragNodes: true, dragView: true, zoomView: true, hoverConnectedEdges: false, selectConnectedEdges: false, navigationButtons: false, keyboard: false }, nodes: { borderWidth: 3, shadow: true }, edges: { shadow: true, smooth: true, length: 150 }, manipulation: { enabled: false } }; + const network = new Network(container, data, options); + + // Keep your existing events (dragEnd, click, stabilization, etc.) + // No changes needed below + network.on('dragEnd', () => { + const positions = network.getPositions(); + this.savedPositions = positions; + localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions)); + }); + + network.on('click', (event: any) => { + if (event.nodes[0]) { + const clickedNode = nodes.get(event.nodes[0]); + const canvasPosition = network.canvasToDOM(event.pointer.canvas); + const containerRect = container.getBoundingClientRect(); + const mainContainer = document.querySelector('.main-container') as HTMLElement; + const mainRect = mainContainer?.getBoundingClientRect() || containerRect; + + let adjustedX = canvasPosition.x + containerRect.left - mainRect.left + 20; + let adjustedY = canvasPosition.y + containerRect.top - mainRect.top - 50; + + const popupWidth = 280; + const maxPopupHeight = window.innerHeight - 40; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + if (adjustedX + popupWidth > viewportWidth) adjustedX -= popupWidth + 40; + if (adjustedY + maxPopupHeight > viewportHeight) adjustedY = viewportHeight - maxPopupHeight - 20; + if (adjustedX < 20) adjustedX = 20; + if (adjustedY < 20) adjustedY = 20; + + this.handleNodeClick(clickedNode, { x: adjustedX, y: adjustedY }); + } + }); + + network.on('stabilizationIterationsDone', () => { + network.fit(); + if (!hasSavedPositions) { + const positions = network.getPositions(); + this.savedPositions = positions; + localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions)); + } + }); + + if (hasSavedPositions) { + setTimeout(() => network.fit(), 500); + } + } + + handleNodeClick(node: any, position?: { x: number, y: number }) { + this.selectedDevice = node; + if (position) { + this.selectedDevice.popupPosition = position; + } + } + + closeDeviceDetails() { + this.selectedDevice = null; + } + + getDeviceInfo() { + if (!this.selectedDevice) return null; + + const device = this.mikrotikData.find(d => d.name === this.selectedDevice.label); + + if (device) { + // Main device found + const interfaces = Object.entries(device.interfaces).map(([name, data]: [string, any]) => ({ + name, + address: data.address || 'N/A', + mac: data.mac || 'N/A', + neighbors: data.neighbors?.length || 0 + })); + + const interfaceWithNeighbors = Object.values(device.interfaces) + .find((iface: any) => iface.neighbors?.length > 0) as any; + const firstNeighbor = interfaceWithNeighbors?.neighbors?.[0]; + + return { + name: device.name, + deviceId: device.device_id, + type: firstNeighbor?.type || 'Router', + brand: firstNeighbor?.brand || 'MikroTik', + board: firstNeighbor?.board || 'Unknown', + version: firstNeighbor?.version || 'Unknown', + systemDescription: firstNeighbor?.['system-description'] || null, + softwareId: firstNeighbor?.['software-id'] || 'N/A', + interfaces + }; + } else { + // Search in neighbor data + for (const mainDevice of this.mikrotikData) { + for (const iface of Object.values(mainDevice.interfaces)) { + const neighbor = (iface as any).neighbors?.find((n: any) => n.hostname === this.selectedDevice.label); + if (neighbor) { + return { + name: neighbor.hostname, + deviceId: null, + type: neighbor.type || 'Router', + brand: neighbor.brand || 'MikroTik', + board: neighbor.board || 'Unknown', + version: neighbor.version || 'Unknown', + systemDescription: neighbor['system-description'] || null, + softwareId: neighbor['software-id'] || 'N/A', + interfaces: [{ name: neighbor.interface || 'Unknown', address: neighbor.address || 'N/A', mac: neighbor.mac || 'N/A', neighbors: 0 }] + }; + } + } + } + } + + return null; + } + + getNodeColor(deviceType: string): string { + const colors = { + 'gateway': '#dc3545', + 'router': '#fd7e14', + 'switch': '#6f42c1', + 'ap': '#20c997', + 'cpe': '#0dcaf0' + }; + return (colors as any)[deviceType] || '#6c757d'; + } + + getNodeBorderColor(deviceType: string): string { + const borderColors = { + 'gateway': '#b02a37', + 'router': '#e8681a', + 'switch': '#59359a', + 'ap': '#1aa179', + 'cpe': '#0baccc' + }; + return (borderColors as any)[deviceType] || '#495057'; + } + + getDeviceIcon(deviceType: string, brand: string): string { + const basePath = './assets/Network-Icons-SVG/'; + const type = deviceType.toLowerCase(); + const brandName = brand.toLowerCase(); + + // MikroTik devices + if (brandName === 'mikrotik') { + if (type === 'switch') { + return `${basePath}cumulus-switch-v2.svg`; + } + return `${basePath}cumulus-router-v2.svg`; + } + + // Cisco devices + if (brandName === 'cisco') { + if (type === 'switch') { + return `${basePath}cisco-switch-l2.svg`; + } + return `${basePath}cisco-router.svg`; + } + + // Juniper devices + if (brandName === 'juniper') { + if (type === 'switch') { + return `${basePath}juniper-switch-l2.svg`; + } + return `${basePath}juniper-router.svg`; + } + + // HPE/Aruba devices + if (brandName === 'hpe/aruba' || brandName === 'aruba' || brandName === 'hpe') { + if (type === 'server') { + return `${basePath}generic-server-1.svg`; + } + return `${basePath}arista-switch.svg`; + } + + // Ubiquiti devices + if (brandName === 'ubiquiti' || brandName === 'ubnt') { + if (type === 'switch') { + return `${basePath}generic-switch-l2-v1-colour.svg`; + } + return `${basePath}generic-router-colour.svg`; + } + + // Default icons by type + const defaultIcons = { + 'switch': `${basePath}generic-switch-l2-v1-colour.svg`, + 'router': `${basePath}generic-router-colour.svg`, + 'router/switch': `${basePath}generic-router-colour.svg`, + 'server': `${basePath}generic-server-1.svg`, + 'unknown': `${basePath}generic-router-colour.svg` + }; + + return (defaultIcons as any)[type] || `${basePath}generic-router-colour.svg`; + } + + getDefaultPosition(deviceName: string, index: number): { x: number, y: number } { + const positions = { + 'Core Router': { x: 0, y: 0 }, + 'Edge Router': { x: -200, y: -100 }, + 'Distribution Switch': { x: 200, y: -100 }, + 'Access Point 1': { x: 100, y: 100 }, + 'Access Point 2': { x: 300, y: 100 }, + 'Customer Router 1': { x: 0, y: 200 }, + 'Customer Router 2': { x: 200, y: 200 } + }; + return (positions as any)[deviceName] || { x: index * 100, y: index * 50 }; + } + + webAccess() { + if (!this.selectedDevice) return; + this.currentDeviceInfo = this.getDeviceInfo(); + this.showWebAccessModal = true; + this.closeDeviceDetails(); + } + + openProxyAccess() { + if (this.currentDeviceInfo?.deviceId) { + window.open(`/api/proxy/init?devid=${this.currentDeviceInfo.deviceId}`, '_blank'); + } else { + const ip = this.currentDeviceInfo?.interfaces.find((iface: any) => iface.address !== 'N/A')?.address?.split('/')[0]; + if (ip) { + window.open(`/api/proxy/init?dev_ip=${ip}`, '_blank'); + } + } + this.showWebAccessModal = false; + } + + openDirectAccess() { + const ip = this.currentDeviceInfo?.interfaces.find((iface: any) => iface.address !== 'N/A')?.address?.split('/')[0]; + if (ip) { + window.open(`http://${ip}`, '_blank'); + } + this.showWebAccessModal = false; + } + + closeWebAccessModal() { + this.showWebAccessModal = false; + } + + getNeighborCount() { + const deviceInfo = this.getDeviceInfo(); + return deviceInfo?.interfaces.reduce((total, iface) => total + iface.neighbors, 0) || 0; + } + + getPrimaryIP() { + const deviceInfo = this.getDeviceInfo(); + const primaryInterface = deviceInfo?.interfaces.find(iface => iface.address !== 'N/A'); + return primaryInterface?.address?.split('/')[0] || 'N/A'; + } + + getDeviceInterfaces() { + const deviceInfo = this.getDeviceInfo(); + return deviceInfo?.interfaces || []; + } + + showMoreInfo() { + const deviceInfo = this.getDeviceInfo(); + if (deviceInfo?.deviceId) { + window.open(`/#/device-stats;id=${deviceInfo.deviceId}`, '_blank'); + } + } + + pingDevice(devid: number) { + console.log('Ping device:', this.selectedDevice); + // Implement ping functionality + } + + configureDevice(devid: number) { + console.log('Configure device:', this.selectedDevice); + // Implement configuration interface + } + +} \ No newline at end of file diff --git a/src/app/views/maps/maps.module.ts b/src/app/views/maps/maps.module.ts new file mode 100644 index 0000000..beb35e3 --- /dev/null +++ b/src/app/views/maps/maps.module.ts @@ -0,0 +1,60 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { ReactiveFormsModule } from "@angular/forms"; +import { FormsModule } from "@angular/forms"; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + GridModule, + WidgetModule, + ProgressModule, + TemplateIdDirective, + TooltipModule, + BadgeModule, + CarouselModule, + ListGroupModule, + ModalModule, + TableModule, + UtilitiesModule +} from "@coreui/angular"; +import { IconModule } from "@coreui/icons-angular"; + +import { ChartjsModule } from "@coreui/angular-chartjs"; +import { NgScrollbarModule } from 'ngx-scrollbar'; +import { MapsRoutingModule } from "./maps-routing.module"; +import { MapsComponent } from "./maps.component"; +import { ClipboardModule } from "@angular/cdk/clipboard"; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; + +@NgModule({ + imports: [ + MapsRoutingModule, + CardModule, + WidgetModule, + CommonModule, + GridModule, + ProgressModule, + ReactiveFormsModule, + ButtonModule, + ModalModule, + FormsModule, + TemplateIdDirective, + ButtonModule, + ButtonGroupModule, + ChartjsModule, + CarouselModule, + BadgeModule, + ClipboardModule, + ListGroupModule, + NgScrollbarModule, + TableModule, + TooltipModule, + UtilitiesModule, + InfiniteScrollModule, + IconModule + ], + declarations: [MapsComponent], +}) +export class MapsModule {} diff --git a/src/app/views/permissions/permissions.component.ts b/src/app/views/permissions/permissions.component.ts index d8fac73..1ce6ac0 100644 --- a/src/app/views/permissions/permissions.component.ts +++ b/src/app/views/permissions/permissions.component.ts @@ -225,17 +225,26 @@ export class PermissionsComponent implements OnInit { } else { var _self = this; this.data_provider.delete_perm(_self.SelectedPerm["id"]).then((res) => { - if (res["status"] == "failed") { + if ("error" in res && res.error.indexOf("Unauthorized")) { _self.show_toast( "Error", - res.err, + "You are not authorized to perform this action", "danger" ); - return; } else{ - _self.initGridTable(); - _self.DeleteConfirmModalVisible = false; + if (res["status"] == "failed") { + _self.show_toast( + "Error", + res.err, + "danger" + ); + return; + } + else{ + _self.initGridTable(); + _self.DeleteConfirmModalVisible = false; + } } }); } diff --git a/src/app/views/settings/settings.component.html b/src/app/views/settings/settings.component.html index b0cc3a5..8b66fb9 100644 --- a/src/app/views/settings/settings.component.html +++ b/src/app/views/settings/settings.component.html @@ -1,216 +1,510 @@ - - - - Firmware Manager - - -
    Firmware in repository:
    - - - -   {{value}} - - - - {{value}} - - - - - {{value}} - - - - - - - - -
    +
    + + +
    +
    + +

    System Settings

    +
    + Admin Panel +
    +
    + +
    + + +
    -
    - - - - - -
    - Add new Permission - - - - - - - - - - {{firm}} - - - - - - -
    + + + +
    +
    +
    + Firmware Repository + +
    + + {{source.length}} Versions Available + +
    +
    + + Tip: Keep multiple firmware versions for compatibility. Always test updates on non-critical devices first. +
    + + + + + {{value}} + + + + + {{value}} + + + + + {{value.substring(0, 16)}}... + + + + + + + + +
    -
    - - - - - * Choose how Mikrowizard should update old v6 firmwares - + +
    +
    +
    + + Download New Firmware + +
    +
    + + Internet Required: Downloads from mikrotik.com. Ensure stable connection and access to mikrotik.com domain. +
    + + +
    + + +
    +
    + {{firm}} +
    +
    +
    + No versions found +
    +
    +
    + + + +
    +
    + + Fetching available versions from MikroTik... +
    +
    +
    - - - - - * The version of firmware to install routers - - - - - - - * The version of firmware to install on V6 routers - - -
    - -
    - - System Settings - - - - Rad Secret - - - * Radius Secret of Mikrowizard Radius Server - - - - - System URL - - - * Default system access URl - - - - Default IP - - - * Default Mikrowizard Access IP - - - System Time Zone - - Select event type - - - - - - {{tz.text}} - - - - - * Default TimeZone for the system - - - - - Default User - - Default password - - - * Default username and Password for searching new devices - - - License Username - - - * The username that you registred in Mikrowizard.com,Required for License Activation - + +
    +
    + Update Configuration + +
    +
    + + Best Practice: Use "Keep v6" for legacy devices. Test new firmware versions in lab environment first. +
    + + + +
    + + + 📋 Keep v6: v6 devices use V6 Firmware Version, v7 devices use Default Firmware Version +
    +
    - - - - - * Force User Groups under user>groups configuration of each router to match Mikrowizard Permissions and - monitor for any change to prevent/fix the configuration. - + +
    + + + 💡 Tip: Choose stable releases for production. LTS versions are only available for v6 firmware +
    +
    + + +
    + + + Firmware version for RouterOS v6 devices +
    +
    +
    + +
    + +
    +
    +
    - - - - - * Force Radius config under radius>client and user>aaa setting of each router that added to Mikrowizard and - monitor for any change to prevent/fix the configuration. - + + + +
    +
    + Network Configuration + +
    +
    + + Network Setup: Configure these settings to match your network infrastructure for proper device communication. +
    + + + +
    + + + 🌐 Examples: https://mikrowizard.company.com or 192.168.1.100 +
    +
    + + +
    + + + 🔧 Tip: Use static IP outside DHCP range (e.g., 192.168.1.100-200) +
    +
    + + +
    + + + 🔐 Internal RADIUS: Secret for MikroWizard's built-in RADIUS server (for MikroTik admin authentication) +
    +
    + + +
    + +
    + +
    +
    + {{tz.text}} +
    +
    +
    + No timezones found +
    +
    + Default system timezone +
    +
    +
    +
    - - - - - * Force Syslog config under system>logs setting of each router that added to Mikrowizard and monitor syslog - setting for any change to prevent/fix the configuration. - + +
    +
    + Device Discovery Credentials + +
    +
    + + Security Warning: Change default passwords immediately after device setup. Use strong, unique credentials. +
    + + + +
    + + + 🔍 Device Discovery: Username for finding MikroTik devices with default credentials in your network +
    +
    + + +
    + + + 🔍 Device Discovery: Password for finding MikroTik devices with default credentials (often empty on new devices) +
    +
    +
    +
    - - - - PRO - * Download and install reqired firmware before installing the target firmware . for example it will install - latest 7.12 then upgrade to newer version >7.13 or install Required packages before update - + +
    +
    + License Configuration + +
    + + + +
    + + + 🔑 Same username you use to login to mikrowizard.com website +
    +
    +
    +
    - - - - PRO - * Force login to devices using otp for all users.(you can make exceptions for each user) - - + +
    +
    + System Behavior + +
    + + + +
    + + + 📊 Manual Mode: Updates require confirmation and actions in dashboard view +
    +
    +
    +
    -
    -
    - - + +
    +
    + Security & Enforcement + +
    +
    + + Important: These settings will override device configurations. Test in lab environment before enabling in production. +
    + +
    +
    +
    +
    +
    Force Permissions + +
    +

    🔄 Sync MikroTik user groups with MikroWizard permissions +
    💡 Ensures consistent user access control across all devices +

    +
    + + + + +
    +
    + +
    +
    +
    +
    Force RADIUS + +
    +

    🔐 Configure RADIUS on MikroTik devices for admin user authentication +
    👥 For MikroTik system users authentication (not end users) +

    +
    + + + + +
    +
    + +
    +
    +
    +
    Force Syslog + +
    +

    📊 Automatically configure syslog settings on managed devices +
    ⚠️ Will OVERWRITE existing syslog configurations on devices +

    +
    + + + + +
    +
    + +
    +
    +
    +
    Safe Updates PRO + +
    +

    🛡️ Install required intermediate firmware versions before target version +
    ✅ Highly recommended for production environments +

    +
    + + + + +
    +
    + +
    +
    +
    +
    Force Device OTP PRO + +
    +

    🔐 Require OTP authentication for all device access +
    ⚠️ Ensure users have OTP configured before enabling +

    +
    + + + + +
    +
    + +
    +
    +
    +
    WebFig Auto Login PRO + +
    +

    🌐 Create temporary OTP for automatic WebFig login +
    🔑 Users don't need to input passwords to access WebFig admin interface +

    +
    + + + + +
    +
    +
    +
    + +
    + +
    + + + + + +
    Loading system configuration...
    +
    + +
    diff --git a/src/app/views/settings/settings.component.scss b/src/app/views/settings/settings.component.scss index 0185fbf..364f8c3 100644 --- a/src/app/views/settings/settings.component.scss +++ b/src/app/views/settings/settings.component.scss @@ -5,6 +5,629 @@ } } } + .mdc-line-ripple.mdc-line-ripple--deactivating.ng-star-inserted { display: none!important; +} + +/* Settings Container */ +.settings-container { + max-width: 1200px; + margin: 0 auto; +} + +.settings-card { + border: none; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + border-radius: 12px; + overflow: hidden; +} + +.settings-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-bottom: 1px solid #dee2e6; + padding: 1.25rem 1.5rem; +} + +/* Tabs */ +.settings-tabs { + background: #f8f9fa; + border-bottom: 1px solid #dee2e6; + padding: 0 1.5rem; + display: flex; +} + +.tab-link { + border: none; + background: transparent; + border-radius: 0; + padding: 1rem 1.5rem; + font-weight: 500; + color: #6c757d; + transition: all 0.2s ease; + border-bottom: 3px solid transparent; + cursor: pointer; +} + +.tab-link:hover { + color: #495057; + background: rgba(13, 110, 253, 0.05); +} + +.tab-link.active { + color: #0d6efd; + background: white; + border-bottom-color: #0d6efd; +} + +/* Tab Content */ +.tab-content { + padding: 2rem; +} + +/* Section Titles */ +.section-title { + color: #495057; + font-weight: 600; + font-size: 1.1rem; + margin-bottom: 1rem; + display: flex; + align-items: center; +} + +/* Status Section */ +.status-section { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 1.5rem; +} + +/* Download Section */ +.download-section { + .section-card { + background: linear-gradient(135deg, #e7f3ff 0%, #f8f9fa 100%); + border: 2px dashed #0d6efd; + border-radius: 8px; + padding: 1.5rem; + transition: all 0.2s ease; + } + + .section-card:hover { + border-color: #0a58ca; + background: linear-gradient(135deg, #cfe2ff 0%, #e7f3ff 100%); + } + + .section-header { + display: flex; + align-items: center; + font-size: 1rem; + font-weight: 600; + color: #495057; + } +} + +/* Firmware Settings */ +.firmware-settings { + background: #f8f9fa; + border-radius: 8px; + padding: 1.5rem; + border: 1px solid #e9ecef; +} + +/* Config Sections */ +.config-section { + background: #f8f9fa; + border-radius: 8px; + padding: 1.5rem; + border: 1px solid #e9ecef; +} + +/* Setting Groups */ +.setting-group { + margin-bottom: 1rem; +} + +.setting-label { + display: block; + font-weight: 600; + color: #495057; + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +.setting-help { + color: #6c757d; + font-size: 0.8rem; + margin-top: 0.25rem; + display: block; +} + +/* Security Switches */ +.security-switches { + display: grid; + gap: 1rem; +} + +.switch-item { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 1.25rem; + transition: all 0.2s ease; +} + +.switch-item:hover { + border-color: #0d6efd; + box-shadow: 0 2px 4px rgba(13, 110, 253, 0.1); +} + +.switch-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.switch-info { + flex: 1; + margin-right: 1rem; +} + +.switch-title { + font-size: 1rem; + font-weight: 600; + color: #495057; + margin-bottom: 0.25rem; + display: flex; + align-items: center; +} + +.switch-description { + color: #6c757d; + font-size: 0.85rem; + margin: 0; + line-height: 1.4; +} + +/* Compact Grid */ +.compact-grid { + border-radius: 6px; + overflow: hidden; + border: 1px solid #e9ecef; +} + +/* Version Badge */ +.version-badge { + font-family: 'Courier New', monospace; + font-weight: 600; +} + + + +/* Form Controls */ +.form-control, .form-select { + border-radius: 6px; + border: 1px solid #ced4da; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.form-control:focus, .form-select:focus { + border-color: #0d6efd; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +/* Save Section */ +.save-section { + text-align: center; + padding-top: 1rem; + border-top: 1px solid #e9ecef; +} + +/* Form Check Labels */ +.form-check-label { + font-weight: 500; +} + +/* Button Enhancements */ +.btn { + border-radius: 6px; + font-weight: 500; + transition: all 0.2s ease; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Badge Enhancements */ +.badge { + font-weight: 500; + padding: 0.4em 0.6em; +} + +/* Mobile Responsive for Informative Elements */ +@media (max-width: 768px) { + .info-banner, + .warning-banner, + .success-banner { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + .help-icon { + font-size: 0.75rem; + } + + .setting-help { + font-size: 0.75rem; + } + + .switch-description { + font-size: 0.8rem; + } + + .switch-description small { + font-size: 0.7rem; + } + + ::ng-deep .tooltip-inner { + max-width: 250px; + font-size: 0.75rem; + } +} + +@media (max-width: 576px) { + .info-banner, + .warning-banner, + .success-banner { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .setting-label { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } +} + +/* Original Responsive Design */ +@media (max-width: 768px) { + .settings-header { + padding: 1rem; + } + + .settings-tabs { + padding: 0 1rem; + } + + .tab-content { + padding: 1rem; + } + + .tab-link { + padding: 0.75rem 1rem; + font-size: 0.9rem; + } + + .status-section, + .config-section, + .firmware-settings { + padding: 1rem; + } + + .download-section .section-card { + padding: 1rem; + } + + .switch-content { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .switch-info { + margin-right: 0; + } + + .section-title { + font-size: 1rem; + } +} + +@media (max-width: 576px) { + .settings-container { + margin: 0 0.5rem; + } + + .settings-card { + border-radius: 8px; + } + + .tab-content { + padding: 0.75rem; + } + + .status-section, + .config-section, + .firmware-settings { + padding: 0.75rem; + } +} + +/* Search Select Components */ +.search-select-wrapper { + position: relative; + width: 100%; +} + +.search-input { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid #ced4da; + border-radius: 6px; + font-size: 0.9rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.search-input:focus { + border-color: #0d6efd; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); + outline: 0; +} + +.search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 6px 6px; + max-height: 300px; + overflow-y: auto; + z-index: 1000; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.search-option { + padding: 0.5rem 0.75rem; + cursor: pointer; + border-bottom: 1px solid #f8f9fa; + transition: background-color 0.15s ease-in-out; + font-size: 0.85rem; +} + +.search-option:hover { + background-color: #f8f9fa; +} + +.search-option:last-child { + border-bottom: none; +} + +.search-no-results { + padding: 0.75rem; + text-align: center; + color: #6c757d; + font-style: italic; + font-size: 0.8rem; +} + +/* Animation for dropdown */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +.search-dropdown { + animation: fadeIn 0.2s ease-out; +} + +/* Informative Design Elements */ +.help-icon { + font-size: 0.8rem; + cursor: help; + opacity: 0.7; + transition: opacity 0.2s ease; +} + +.help-icon:hover { + opacity: 1; +} + +.help-icon.text-danger { + color: #dc3545 !important; + opacity: 1; +} + +.help-icon.text-warning { + color: #fd7e14 !important; + opacity: 0.9; +} + +.info-banner { + background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); + border: 1px solid #2196f3; + border-radius: 6px; + padding: 0.75rem 1rem; + color: #0d47a1; + font-size: 0.85rem; + display: flex; + align-items: center; +} + +.warning-banner { + background: linear-gradient(135deg, #fff3e0 0%, #ffcc80 100%); + border: 1px solid #ff9800; + border-radius: 6px; + padding: 0.75rem 1rem; + color: #e65100; + font-size: 0.85rem; + display: flex; + align-items: center; +} + +.success-banner { + background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%); + border: 1px solid #4caf50; + border-radius: 6px; + padding: 0.75rem 1rem; + color: #2e7d32; + font-size: 0.85rem; + display: flex; + align-items: center; +} + +/* Enhanced Setting Help */ +.setting-help { + color: #6c757d; + font-size: 0.8rem; + margin-top: 0.25rem; + display: block; + line-height: 1.4; +} + +.setting-help strong { + color: #495057; +} + +/* Enhanced Form Labels */ +.setting-label { + display: flex; + align-items: center; + font-weight: 600; + color: #495057; + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +/* Enhanced Switch Descriptions */ +.switch-description { + color: #6c757d; + font-size: 0.85rem; + margin: 0; + line-height: 1.4; +} + +.switch-description small { + display: block; + margin-top: 0.25rem; + font-size: 0.75rem; +} + +.switch-description .text-warning { + color: #856404 !important; +} + +.switch-description .text-info { + color: #0c5460 !important; +} + +.switch-description .text-success { + color: #155724 !important; +} + +.switch-description .text-danger { + color: #721c24 !important; +} + +.setting-help.text-danger { + color: #721c24 !important; + font-weight: 600; +} + +.setting-help.text-warning { + color: #856404 !important; + font-weight: 600; +} + +/* Enhanced Section Titles */ +.section-title { + color: #495057; + font-weight: 600; + font-size: 1.1rem; + margin-bottom: 1rem; + display: flex; + align-items: center; +} + +/* Tooltip Enhancements */ +::ng-deep .tooltip { + font-size: 0.8rem; +} + +::ng-deep .tooltip-inner { + max-width: 300px; + text-align: left; + background-color: #2c3e50; + border-radius: 6px; + padding: 0.5rem 0.75rem; +} + +/* Enhanced Form Controls with Status */ +.form-control.has-warning { + border-color: #ffc107; + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); +} + +.form-control.has-success { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.form-control.has-error { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +/* Enhanced Select Options */ +.form-select option { + padding: 0.5rem; +} + +/* Status Indicators */ +.status-indicator { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + margin-left: 0.5rem; +} + +.status-indicator.recommended { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.status-indicator.warning { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; +} + +.status-indicator.critical { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +/* Grid Enhancements */ +::ng-deep .gui-grid { + .gui-grid-header { + background: #f8f9fa; + font-weight: 600; + } + + .gui-grid-cell { + padding: 0.75rem 0.5rem; + } } \ No newline at end of file diff --git a/src/app/views/settings/settings.component.ts b/src/app/views/settings/settings.component.ts index 394a771..53a387a 100644 --- a/src/app/views/settings/settings.component.ts +++ b/src/app/views/settings/settings.component.ts @@ -22,7 +22,6 @@ import { import { ToasterComponent } from "@coreui/angular"; import { AppToastComponent } from "../toast-simple/toast.component"; import { TimeZones } from "./timezones-data"; -import { error } from "console"; @Component({ templateUrl: "settings.component.html", @@ -38,6 +37,17 @@ export class SettingsComponent implements OnInit { public filters: any = {}; public firms: any = {}; public firmtodownload: any = {}; + public activeTab: string = 'firmware'; + + // Search functionality properties + public firmwareSearch: string = ''; + public showFirmwareDropdown: boolean = false; + public filteredFirmwares: any[] = []; + + public timezoneSearch: string = ''; + public showTimezoneDropdown: boolean = false; + public filteredTimezones: any[] = []; + constructor( private data_provider: dataProvider, private router: Router, @@ -137,6 +147,14 @@ export class SettingsComponent implements OnInit { _self.currentFirm=firm; if(del){ this.data_provider.delete_firm(this.currentFirm.id).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if (res.status == true){ _self.DeleteConfirmModalVisible=false; _self.initFirmsTable(); @@ -148,6 +166,7 @@ export class SettingsComponent implements OnInit { "danger" ); } + } }); } else @@ -160,6 +179,14 @@ export class SettingsComponent implements OnInit { this.data_provider .download_firmware_to_repository(this.firmtodownload) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if (res.status == true) { // show toast that we are already downloading _self.show_toast( @@ -177,6 +204,7 @@ export class SettingsComponent implements OnInit { } _self.ConfirmModalVisible = !_self.ConfirmModalVisible; _self.loading = false; + } }); } @@ -204,14 +232,34 @@ export class SettingsComponent implements OnInit { this.firmwaretoinstallv6 ) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.initFirmsTable(); + } }); } saveSysSetting() { var _self = this; this.data_provider.save_sys_setting(this.sysconfigs).then((res) => { - _self.initsettings(); + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ + _self.show_toast("Settings", "Settings saved", "success"); + _self.initsettings(); + + } }); } @@ -244,10 +292,24 @@ export class SettingsComponent implements OnInit { initsettings(): void { var _self = this; this.data_provider.get_settings().then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.sysconfigs = res.sysconfigs; _self.sysconfigs["default_user"]["value"] = ""; _self.sysconfigs["default_password"]["value"] = ""; _self.timezones = _self.TimeZones.timezones; + _self.filteredTimezones = _self.TimeZones.timezones; + // Set initial timezone search display + const currentTz = _self.timezones.find((tz: any) => tz.utc[0] === _self.sysconfigs['timezone']['value']); + if (currentTz) { + _self.timezoneSearch = currentTz.text; + } _self.sysconfigs["force_syslog"]["value"] = /true/i.test( _self.sysconfigs["force_syslog"]["value"] ); @@ -265,7 +327,33 @@ export class SettingsComponent implements OnInit { _self.sysconfigs["otp_force"]["value"] ); } + if(_self.ispro && "proxy_auto_login" in _self.sysconfigs){ + _self.sysconfigs["proxy_auto_login"]["value"] = /true/i.test( + _self.sysconfigs["proxy_auto_login"]["value"] + ); + } + else if(_self.ispro){ + _self.sysconfigs["proxy_auto_login"] = { + "value": true + } + } + //check if update_mode is in the sysconfigs + if ("update_mode" in _self.sysconfigs){ + //convert string to json + _self.sysconfigs["update_mode"]["value"] = JSON.parse(_self.sysconfigs["update_mode"]["value"]); + } + else{ + //create default update_mode and set mode to auto + _self.sysconfigs["update_mode"] = { + "value": { + "mode": "auto", + "update_back" : false, + "update_front" : false + } + } + } _self.SysConfigloading = false; + } }); } @@ -274,7 +362,50 @@ export class SettingsComponent implements OnInit { this.data_provider.get_downloadable_firms().then((res) => { let index = 1; _self.firms = res.versions; + _self.filteredFirmwares = res.versions; _self.loading = false; }); } + + // Firmware search methods + filterFirmwares(event: any): void { + const searchTerm = event.target.value.toLowerCase(); + this.firmwareSearch = searchTerm; + this.filteredFirmwares = this.firms.filter((firm: string) => + firm.toLowerCase().includes(searchTerm) + ); + } + + selectFirmware(firmware: string): void { + this.firmtodownload = firmware; + this.firmwareSearch = firmware; + this.showFirmwareDropdown = false; + } + + hideFirmwareDropdown(): void { + setTimeout(() => { + this.showFirmwareDropdown = false; + }, 200); + } + + // Timezone search methods + filterTimezones(event: any): void { + const searchTerm = event.target.value.toLowerCase(); + this.timezoneSearch = searchTerm; + this.filteredTimezones = this.timezones.filter((tz: any) => + tz.text.toLowerCase().includes(searchTerm) + ); + } + + selectTimezone(timezone: any): void { + this.sysconfigs['timezone']['value'] = timezone.utc[0]; + this.timezoneSearch = timezone.text; + this.showTimezoneDropdown = false; + } + + hideTimezoneDropdown(): void { + setTimeout(() => { + this.showTimezoneDropdown = false; + }, 200); + } } diff --git a/src/app/views/settings/settings.module.ts b/src/app/views/settings/settings.module.ts index 116d06d..b482be7 100644 --- a/src/app/views/settings/settings.module.ts +++ b/src/app/views/settings/settings.module.ts @@ -10,14 +10,14 @@ import { SpinnerModule, ToastModule, ModalModule, + BadgeModule, + TooltipModule, } from "@coreui/angular"; import { SettingsRoutingModule } from "./settings-routing.module"; import { SettingsComponent } from "./settings.component"; import { GuiGridModule } from "@generic-ui/ngx-grid"; import { FormsModule } from "@angular/forms"; -import { MatSelectModule } from "@angular/material/select"; -import { NgxMatSelectSearchModule } from "ngx-mat-select-search"; @NgModule({ imports: [ @@ -30,11 +30,11 @@ import { NgxMatSelectSearchModule } from "ngx-mat-select-search"; ButtonModule, ButtonGroupModule, GuiGridModule, - MatSelectModule, - NgxMatSelectSearchModule, SpinnerModule, ToastModule, ModalModule, + BadgeModule, + TooltipModule, ], declarations: [SettingsComponent], }) diff --git a/src/app/views/snippets/snippets.component.html b/src/app/views/snippets/snippets.component.html index 2b68c32..4b3d199 100644 --- a/src/app/views/snippets/snippets.component.html +++ b/src/app/views/snippets/snippets.component.html @@ -159,12 +159,11 @@ -
    Editing Group
    +
    Exec history
    -
    Group Members :
    { + _self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => { _self.source = res.map((x: any) => { x.created = [ x.created.split("T")[0], diff --git a/src/app/views/syslog/syslog.component.html b/src/app/views/syslog/syslog.component.html index 6be0bf3..0f804b1 100644 --- a/src/app/views/syslog/syslog.component.html +++ b/src/app/views/syslog/syslog.component.html @@ -3,8 +3,11 @@ - - Devices + +
    Devices
    + + Showing last 24 hours logs by default. Use filters to modify the date and time. +
    - -
    - - -
    - - - First Name - - - Last Name - - - -
    - - -
    - -
    - - -
    - -
    MikroWizard permisssions :
    - - - - - - - - - - - - - - + + +
    +
    +
    Basic Information
    + Enter the user's personal details and login credentials +
    + + + + + + + + + + + + + + + - - - -
    Mikrotik permisssions :
    - - - -   {{value}} - - - - {{value}} - - - - - - - - -
    -
    - - - - - -
    - Add new Permission - - - - - - - - {{group.name}} - - - - - - - - - - - {{perm.name}} - - - - - - - -
    - - -
    - -
    + + +
    +
    +
    System Access Levels
    + Control what system features this user can access and modify +
    +
    + R = Read Only + W = Read & Write + F = Full Control + × = No Access +
    +
    +
    + {{ perm.key.replace('_', ' ') }} +
    + + + + +
    +
    +
    +
    + + +
    +
    +
    Device Access Control
    + Grant access to specific device groups with defined permission levels +
    + + +
    +
    + + Add Device Permission +
    + + +
    + + +
    +
    + {{group.name}} +
    +
    +
    + No groups found +
    +
    +
    + +
    + + +
    +
    + {{perm.name}} +
    +
    +
    + No permissions found +
    +
    +
    + + + + +
    +
    + + +
    +
    +
    Current Device Access
    + {{userperms.length}} permission(s) +
    + +
    +
    +
    {{i + 1}}
    +
    +
    + + {{perm.group_name}} +
    +
    + + {{perm.perm_name}} +
    +
    + +
    +
    + +
    +
    + +
    +
    + No device access granted +

    Add permissions above to grant access to device groups

    +
    +
    +
    +
    + +
    - - - + +
    +
    + + +
    diff --git a/src/app/views/user_manager/user_manager.component.ts b/src/app/views/user_manager/user_manager.component.ts index 6bdc57f..60b6595 100644 --- a/src/app/views/user_manager/user_manager.component.ts +++ b/src/app/views/user_manager/user_manager.component.ts @@ -78,6 +78,12 @@ export class UserManagerComponent implements OnInit { public permission: any = {}; public allDevGroups: any = []; public allPerms: any = []; + public devgroupSearch: string = ''; + public permissionSearch: string = ''; + public filteredDevGroups: any = []; + public filteredPermissions: any = []; + public showDevGroupDropdown: boolean = false; + public showPermissionDropdown: boolean = false; public DeletePermConfirmModalVisible: boolean = false; public userperms: any = {}; public userresttrictions: any = false; @@ -129,6 +135,40 @@ export class UserManagerComponent implements OnInit { this.adminperms[key] = value; } + filterDevGroups(event: any): void { + const query = event.target.value.toLowerCase(); + this.filteredDevGroups = this.allDevGroups.filter((group: any) => + group.name.toLowerCase().includes(query) + ); + } + + filterPermissions(event: any): void { + const query = event.target.value.toLowerCase(); + this.filteredPermissions = this.allPerms.filter((perm: any) => + perm.name.toLowerCase().includes(query) + ); + } + + selectDevGroup(group: any): void { + this.devgroup = group; + this.devgroupSearch = group.name; + this.showDevGroupDropdown = false; + } + + selectPermission(perm: any): void { + this.permission = perm; + this.permissionSearch = perm.name; + this.showPermissionDropdown = false; + } + + hideDevGroupDropdown(): void { + setTimeout(() => this.showDevGroupDropdown = false, 200); + } + + hidePermissionDropdown(): void { + setTimeout(() => this.showPermissionDropdown = false, 200); + } + public rowSelection: boolean | GuiRowSelection = { enabled: true, type: GuiRowSelectionType.CHECKBOX, @@ -171,6 +211,14 @@ export class UserManagerComponent implements OnInit { } _self.SelectedUser["adminperms"] = _self.adminperms; this.data_provider.create_user(_self.SelectedUser).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if ("id" in res && !("status" in res)) { _self.initGridTable(); this.EditTaskModalVisible = false; @@ -178,6 +226,7 @@ export class UserManagerComponent implements OnInit { //show error _self.show_toast("Error", res.err, "danger"); } + } }); } else { if (_self.userperms.length > 0) { @@ -187,8 +236,17 @@ export class UserManagerComponent implements OnInit { } _self.SelectedUser["adminperms"] = _self.adminperms; this.data_provider.edit_user(_self.SelectedUser).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.initGridTable(); _self.EditTaskModalVisible = false; + } }); } // @@ -197,14 +255,34 @@ export class UserManagerComponent implements OnInit { editAddUser(item: any, action: string) { var _self = this; this.data_provider.get_perms(1, 1000, "").then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.allPerms = res.map((x: any) => { return { id: x["id"], name: x.name }; }); + _self.filteredPermissions = [..._self.allPerms]; _self.data_provider.get_devgroup_list().then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.allDevGroups = res.map((x: any) => { return { id: x["id"], name: x.name }; }); + _self.filteredDevGroups = [..._self.allDevGroups]; + } }); + } }); if (action == "showadd") { this.userperms = []; @@ -218,6 +296,10 @@ export class UserManagerComponent implements OnInit { action: "add", }; this.adminperms = { ...this.defadminperms }; + this.devgroup = {}; + this.permission = {}; + this.devgroupSearch = ''; + this.permissionSearch = ''; this.EditTaskModalVisible = true; return; } @@ -227,6 +309,10 @@ export class UserManagerComponent implements OnInit { } else this.adminperms = { ...this.defadminperms }; _self.SelectedUser["action"] = "edit"; _self.get_user_perms(_self.SelectedUser["id"]); + _self.devgroup = {}; + _self.permission = {}; + _self.devgroupSearch = ''; + _self.permissionSearch = ''; _self.EditTaskModalVisible = true; } @@ -241,7 +327,6 @@ export class UserManagerComponent implements OnInit { this.data_provider.get_user_restrictions(this.SelectedUser["id"]).then((res) => { _self.userresttrictions = res; - console.log(_self.userresttrictions); _self.RestrictionsTaskModalVisible = true; }); } @@ -270,13 +355,23 @@ export class UserManagerComponent implements OnInit { } save_sec(){ + var _self=this; this.data_provider.save_user_restrictions(this.SelectedUser.id,this.userresttrictions).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if('status' in res && res['status']=='success') this.RestrictionsTaskModalVisible = false; else if('status' in res && res['status']=='failed') this.show_toast("Error", res.err, "danger"); else this.show_toast("Error", "Somthing went wrong", "danger"); + } }); } @@ -289,9 +384,18 @@ export class UserManagerComponent implements OnInit { this.devgroup["id"] ) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.get_user_perms(_self.SelectedUser["id"]); _self.permission = 0; _self.devgroup = 0; + } }); } @@ -314,8 +418,17 @@ export class UserManagerComponent implements OnInit { } else { var _self = this; this.data_provider.delete_user(_self.SelectedUser["id"]).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.initGridTable(); _self.DeleteConfirmModalVisible = false; + } }); } } @@ -329,8 +442,18 @@ export class UserManagerComponent implements OnInit { } confirm_delete_perm(item: any) { + var _self = this; this.data_provider.Delete_user_perm(item.id).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ this.get_user_perms(this.SelectedUser["id"]); + } }); } @@ -344,11 +467,20 @@ export class UserManagerComponent implements OnInit { var pageSize = 10; var searchstr = ""; this.data_provider.get_users(page, pageSize, searchstr).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.source = res.map((x: any) => { return x; }); _self.SelectedUser = {}; _self.loading = false; + } }); } } diff --git a/src/app/views/user_manager/user_manager.scss b/src/app/views/user_manager/user_manager.scss index 37a3dcd..e08d0da 100644 --- a/src/app/views/user_manager/user_manager.scss +++ b/src/app/views/user_manager/user_manager.scss @@ -1,4 +1,462 @@ table tr td{ padding-bottom:20px; vertical-align:top +} + +/* Enhanced Modal Styles */ +.permission-item { + transition: all 0.2s ease; + background: #fafafa; + min-height: 60px; +} + +.permission-item:hover { + background: #f0f0f0; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +/* Modern Form Sections */ +.user-form-section, .permissions-section, .device-permissions-section { + border-radius: 8px; + background: #f8f9fa; + padding: 1rem; + border: 1px solid #e9ecef; +} + +.section-title { + color: #495057; + font-weight: 600; + margin-bottom: 0.75rem; + font-size: 0.95rem; +} + +.form-input { + border-radius: 6px; + border: 1px solid #ced4da; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.form-input:focus { + border-color: #0d6efd; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +/* Compact Permissions */ +.permissions-compact { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 0.5rem; +} + +.perm-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.4rem 0.6rem; + background: white; + border: 1px solid #dee2e6; + border-radius: 6px; + transition: all 0.2s ease; +} + +.perm-row:hover { + background: #f1f3f4; + border-color: #adb5bd; +} + +.perm-label { + font-size: 0.85rem; + font-weight: 500; + color: #495057; + text-transform: capitalize; + flex: 1; +} + +.perm-controls { + display: flex; + gap: 2px; +} + +.perm-controls .btn { + padding: 0.2rem 0.4rem; + font-size: 0.7rem; + font-weight: 600; + min-width: 24px; + border-radius: 4px; +} + +/* Section Headers */ +.section-header { + border-bottom: 1px solid #e9ecef; + padding-bottom: 0.5rem; +} + +/* Permission Legend */ +.permission-legend { + display: flex; + flex-wrap: wrap; + gap: 1rem; + padding: 0.5rem; + background: #f8f9fa; + border-radius: 4px; + border: 1px solid #e9ecef; +} + +.legend-item { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.75rem; + color: #6c757d; +} + +.legend-color { + width: 12px; + height: 12px; + border-radius: 2px; + display: inline-block; +} + +/* Add Permission Card */ +.add-permission-card { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border: 2px dashed #0d6efd; + border-radius: 8px; + padding: 1rem; + transition: all 0.2s ease; +} + +.add-permission-card:hover { + border-color: #0a58ca; + background: linear-gradient(135deg, #e7f3ff 0%, #cfe2ff 100%); +} + +.add-header { + display: flex; + align-items: center; + font-size: 0.9rem; + color: #495057; + margin-bottom: 0.5rem; +} + +.add-btn { + font-weight: 600; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.add-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Current Permissions */ +.current-permissions { + background: white; + border-radius: 8px; + border: 1px solid #dee2e6; + overflow: hidden; +} + +.permissions-header { + background: #f8f9fa; + padding: 0.75rem 1rem; + border-bottom: 1px solid #dee2e6; + display: flex; + justify-content: space-between; + align-items: center; +} + +.permissions-list { + max-height: 250px; + overflow-y: auto; +} + +.permission-item { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + border-bottom: 1px solid #f1f3f4; + transition: all 0.2s ease; +} + +.permission-item:last-child { + border-bottom: none; +} + +.permission-item:hover { + background: #f8f9fa; +} + +.perm-number { + background: #0d6efd; + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; + margin-right: 0.75rem; + flex-shrink: 0; +} + +.perm-info { + flex: 1; +} + +.perm-main { + display: flex; + align-items: center; + margin-bottom: 0.25rem; +} + +.perm-detail { + display: flex; + align-items: center; +} + +.group-name { + font-size: 0.85rem; + font-weight: 600; + color: #495057; +} + +.perm-name { + font-size: 0.75rem; + color: #6c757d; +} + +.remove-btn { + margin-left: 0.5rem; + flex-shrink: 0; +} + +/* Empty State */ +.empty-permissions { + display: flex; + align-items: center; + padding: 2rem 1rem; + text-align: center; +} + +.empty-icon { + font-size: 2rem; + margin-right: 1rem; + opacity: 0.5; +} + +.empty-text strong { + display: block; + margin-bottom: 0.25rem; + color: #495057; +} + +.compact-select { + font-size: 0.85rem; + height: calc(1.8em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; +} + +.compact-select:focus { + border-color: #0d6efd; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +/* Search Select Components */ +.search-select-wrapper { + position: relative; +} + +.search-label { + display: block; + font-size: 0.8rem; + color: #6c757d; + margin-bottom: 0.25rem; + font-weight: 500; +} + +.search-input { + width: 100%; +} + +.search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 0.375rem 0.375rem; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.search-option { + padding: 0.5rem 0.75rem; + cursor: pointer; + border-bottom: 1px solid #f1f3f4; + font-size: 0.85rem; + transition: background-color 0.2s ease; +} + +.search-option:hover { + background-color: #f8f9fa; +} + +.search-option:last-child { + border-bottom: none; +} + +.search-no-results { + padding: 0.75rem; + text-align: center; + color: #6c757d; + font-size: 0.8rem; + font-style: italic; + background: white; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 0.375rem 0.375rem; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; +} + +.badge { + font-size: 0.7rem; +} + +.add-permission-form { + border: 1px dashed #dee2e6; +} + +.c-modal-body { + max-height: 85vh; + overflow-y: auto; +} + +.c-card-header { + border-bottom: 1px solid #e9ecef; +} + +.permission-item small { + color: #6c757d; + font-weight: 500; +} + +.c-button-group .btn { + font-size: 0.75rem; + font-weight: 500; + min-width: 28px; +} + +.compact-field { + font-size: 0.85rem; +} + +.compact-field .mat-form-field-wrapper { + padding-bottom: 0.5em; +} + +/* Mobile Responsive */ +@media (max-width: 576px) { + .c-modal-dialog { + margin: 0.25rem; + } + + .user-form-section, .permissions-section, .device-permissions-section { + padding: 0.75rem; + margin-bottom: 0.75rem; + } + + .permissions-compact { + grid-template-columns: 1fr; + } + + .perm-row { + flex-direction: column; + align-items: stretch; + gap: 0.5rem; + padding: 0.5rem; + } + + .perm-label { + text-align: center; + margin-bottom: 0.25rem; + } + + .perm-controls { + justify-content: center; + } + + .add-permission-card { + padding: 0.75rem; + } + + .permission-legend { + gap: 0.5rem; + justify-content: center; + } + + .permissions-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .empty-permissions { + flex-direction: column; + text-align: center; + } + + .empty-icon { + margin-right: 0; + margin-bottom: 0.5rem; + } + + .section-title { + font-size: 0.9rem; + } + + .c-button-group .btn { + font-size: 0.7rem; + padding: 0.25rem 0.3rem; + min-width: 24px; + } + + .c-modal-body { + max-height: 90vh; + padding: 0.5rem !important; + } + + .c-card-body { + padding: 0.75rem !important; + } + + .add-permission-form { + padding: 0.75rem !important; + } +} + +@media (max-width: 768px) { + .c-modal-footer { + flex-direction: column; + align-items: stretch; + } + + .c-modal-footer > div { + width: 100%; + text-align: center; + } } \ No newline at end of file diff --git a/src/app/views/user_tasks/user_tasks.component.html b/src/app/views/user_tasks/user_tasks.component.html index f7da0bc..f37fd7c 100644 --- a/src/app/views/user_tasks/user_tasks.component.html +++ b/src/app/views/user_tasks/user_tasks.component.html @@ -57,146 +57,255 @@ - -
    Editing device {{SelectedTask['name']}}
    -
    Adding new task
    + +
    + Edit Task: {{SelectedTask['name']}} +
    +
    + Create New Task +
    - -
    - - + + +
    +
    +
    Basic Information
    + Define the task name, description and type +
    + + + + + + + + + + +
    - -
    - - + +
    +
    +
    Task Configuration
    + Configure task-specific settings and parameters +
    + + +
    + + +
    Backup Configuration
    +
    + +
    + + This task will create configuration backups of selected devices. Backups are stored securely and can be restored later. +
    +
    +
    +
    + + + + +
    Firmware Update Strategy
    +
    + +
    + + + + + +
    + +
    + + Uses global MikroWizard update strategy settings. Check Settings page for configuration. +
    + +
    + + Downloads latest firmware from mikrotik.com. Server needs internet access. +
    + +
    + + + + + + + + + + +
    +
    +
    + + +
    + + +
    Script/Snippet Configuration
    +
    + + + + + + The selected script will be executed on all target devices when this task runs. + + +
    +
    - - - - - -
    Update Version Strategy
    - - - - - - - - - - - - - - - - - - The version of firmware will be selected based on global settings of Mikrowizard Update strategy. -
    - Please check settings page for more info and configuration -
    -
    - - - - The version of firmware will be selected based on latest availble version from Mikrotik website!. -
    - V6 Firmware update Behavior and safe install is based on global Mikrowizard setting.(check settings page) -
    - **with this option MikroWizard will download latest availble firmware from mikrotik.com. Please keep in mind that server needs internet access to mikrotik.com
    -
    - - - - - - * The version of firmware to install routers - - - - - - - * The version of firmware to install on V6 routers - - -
    -
    - - - - -
    - - + + +
    +
    +
    Schedule Configuration
    + Set when this task should run automatically +
    +
    + +
    +
    + + +
    +
    +
    +
    {{cron.label}}
    +
    {{cron.value}}
    +
    {{cron.description}}
    +
    +
    +
    + No matching cron presets found +
    +
    + + {{getCronDescription()}} + +
    + + Quick Examples: + * * * * * = every minute | + 0 2 * * * = daily at 2 AM | + 0 */6 * * * = every 6 hours + +
    +
    + + +
    +
    +
    Target Selection
    + Choose which devices or groups this task will affect +
    +
    + + + + +
    - - - - - -
    Members :
    - - - -   {{value}} - - - - {{value}} - - - - - - - - -
    - + + + +
    + Selected {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} +
    +
    + {{SelectedMembers.length}} selected + +
    +
    + +
    + +
    No {{SelectedTask['selection_type']}} selected
    +

    Click "Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}" to select targets for this task

    +
    +
    + + + +
    + + {{value}} +
    +
    +
    + + + {{value}} + + + + + + + +
    +
    +
    +
    +
    - - - - + +
    + All fields marked with * are required +
    +
    + + + +
    - - -
    Editing Group
    + + +
    + Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} to Task +
    - - -
    Group Members :
    - - - -   {{value}} - - - - {{value}} - - - - - {{value}} - - - -
    -
    -
    + + +
    + + + {{NewMemberRows.length}} {{SelectedTask['selection_type']}}(s) selected for addition to this task + +
    + + + + +
    + + Available {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} ({{availbleMembers.length}} total) +
    +
    + +
    + +
    All {{SelectedTask['selection_type']}} are already assigned
    +

    No available {{SelectedTask['selection_type']}} to add to this task

    +
    +
    + + + +
    + + {{value}} +
    +
    +
    + + + {{value}} + + + + + {{value}} + + +
    +
    +
    +
    - - - + +
    + Select {{SelectedTask['selection_type']}} from the list above to add them to this task +
    +
    + + +
    @@ -340,4 +491,6 @@ Close
    - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/app/views/user_tasks/user_tasks.component.scss b/src/app/views/user_tasks/user_tasks.component.scss new file mode 100644 index 0000000..ccea1e6 --- /dev/null +++ b/src/app/views/user_tasks/user_tasks.component.scss @@ -0,0 +1,371 @@ +/* Task Form Sections */ +.task-form-section, .task-config-section, .schedule-section, .target-section { + border: 1px solid #e9ecef; + border-radius: 6px; + padding: 0.75rem; + background: #f8f9fa; +} + +.section-header { + border-bottom: 1px solid #dee2e6; + padding-bottom: 0.375rem; + margin-bottom: 0.75rem; +} + +.section-title { + color: #495057; + font-weight: 600; + font-size: 0.875rem; +} + +/* Form Inputs */ +.form-input { + border: 1px solid #ced4da; + border-radius: 4px; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-input:focus { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +/* Strategy Buttons */ +.strategy-buttons .btn-group { + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.strategy-buttons button { + font-size: 0.8rem; + padding: 0.375rem 0.75rem; +} + +/* Cron Dropdown Styles */ +.cron-input-wrapper { + position: relative; +} + +.input-group { + position: relative; + display: flex; + flex-wrap: nowrap; + align-items: stretch; + width: 100%; +} + +.input-group .search-input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: none; + flex: 1; +} + +.input-group .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: 1px solid #ced4da; + flex-shrink: 0; +} + +.search-select-wrapper { + position: relative; + width: 100%; +} + +.search-input { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid #ced4da; + border-radius: 6px; + font-size: 0.9rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.search-input:focus { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); + outline: 0; +} + +.search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 6px 6px; + max-height: 300px; + overflow-y: auto; + z-index: 1000; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.search-option { + padding: 0.5rem 0.75rem; + cursor: pointer; + border-bottom: 1px solid #f8f9fa; + transition: background-color 0.15s ease-in-out; +} + +.search-option:hover { + background-color: #f8f9fa; +} + +.search-option:last-child { + border-bottom: none; +} + +.search-no-results { + padding: 0.75rem; + text-align: center; + color: #6c757d; + font-style: italic; +} + +/* Cron-specific dropdown styles */ +.cron-dropdown { + max-height: 400px; +} + +.cron-option { + padding: 0.75rem; + border-bottom: 1px solid #e9ecef; +} + +.cron-option:hover { + background-color: #e3f2fd; +} + +.cron-option.selected { + background-color: #bbdefb; + border-left: 4px solid #2196f3; +} + +.cron-option.selected:hover { + background-color: #90caf9; +} + +.cron-label { + font-weight: 600; + color: #495057; + font-size: 0.9rem; +} + +.cron-value { + font-family: 'Courier New', monospace; + color: #007bff; + font-size: 0.85rem; + margin: 0.25rem 0; + background: #f8f9fa; + padding: 0.25rem 0.5rem; + border-radius: 4px; + display: inline-block; +} + +.cron-description { + color: #6c757d; + font-size: 0.8rem; + font-style: italic; +} + +/* Target Type Selector */ +.target-type-selector .btn-group { + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.target-type-selector button { + font-size: 0.8rem; + padding: 0.375rem 0.75rem; +} + +/* Card Enhancements */ +.c-card { + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + border: 1px solid #e9ecef; +} + +.c-card-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-bottom: 1px solid #dee2e6; + font-weight: 600; +} + +/* Alert Enhancements */ +.alert { + border-radius: 8px; + border: none; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.alert-info { + background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%); + color: #0c5460; +} + +.alert-success { + background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); + color: #155724; +} + +.alert-warning { + background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%); + color: #856404; +} + +/* Badge Enhancements */ +.badge { + font-size: 0.7rem; + padding: 0.25em 0.5em; + border-radius: 4px; +} + +/* Button Enhancements */ +.btn { + border-radius: 4px; + font-weight: 500; + transition: all 0.15s ease-in-out; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); +} + +/* Modal Enhancements */ +.modal-header { + border-bottom: 1px solid #dee2e6; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); +} + +.modal-footer { + border-top: 1px solid #dee2e6; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); +} + +/* Grid Enhancements */ +.gui-grid { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .task-form-section, .task-config-section, .schedule-section, .target-section { + padding: 0.5rem; + } + + .section-title { + font-size: 0.8rem; + } + + .strategy-buttons button, + .target-type-selector button { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + } + + .form-input { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; + } + + .cron-label { + font-size: 0.8rem; + } + + .cron-value { + font-size: 0.75rem; + } + + .cron-description { + font-size: 0.7rem; + } + + .c-modal-body { + padding: 0.75rem !important; + } +} + +@media (max-width: 576px) { + .task-form-section, .task-config-section, .schedule-section, .target-section { + padding: 0.375rem; + margin-bottom: 0.75rem; + } + + .section-header { + margin-bottom: 0.5rem; + } + + .strategy-buttons button, + .target-type-selector button { + font-size: 0.7rem; + padding: 0.25rem 0.375rem; + } + + .input-group .btn { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; + } +} + +/* Animation for smooth transitions */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +.search-dropdown { + animation: fadeIn 0.2s ease-out; +} + +/* Focus states for accessibility */ +.search-option:focus, +.cron-option:focus { + outline: 2px solid #007bff; + outline-offset: -2px; + background-color: #e3f2fd; +} + +/* Loading states */ +.loading-spinner { + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid #f3f3f3; + border-top: 2px solid #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Code examples */ +code { + background-color: #f8f9fa; + color: #e83e8c; + padding: 0.2em 0.4em; + border-radius: 3px; + font-size: 0.875em; + font-family: 'Courier New', monospace; +} + +/* Improved spacing */ +.mt-1 { margin-top: 0.25rem !important; } +.mt-2 { margin-top: 0.5rem !important; } +.me-1 { margin-right: 0.25rem !important; } +.me-2 { margin-right: 0.5rem !important; } +.ms-2 { margin-left: 0.5rem !important; } +.d-block { display: block !important; } \ No newline at end of file diff --git a/src/app/views/user_tasks/user_tasks.component.ts b/src/app/views/user_tasks/user_tasks.component.ts index 63d6163..b19e2e6 100644 --- a/src/app/views/user_tasks/user_tasks.component.ts +++ b/src/app/views/user_tasks/user_tasks.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy } from "@angular/core"; +import { Component, OnInit, OnDestroy, QueryList, ViewChildren } from "@angular/core"; import { dataProvider } from "../../providers/mikrowizard/data"; import { Router } from "@angular/router"; import { loginChecker } from "../../providers/login_checker"; @@ -16,16 +16,28 @@ import { } from "@generic-ui/ngx-grid"; import { NgxSuperSelectOptions } from "ngx-super-select"; import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform"; +import { AppToastComponent } from "../toast-simple/toast.component"; +import { ToasterComponent } from "@coreui/angular"; @Component({ templateUrl: "user_tasks.component.html", + styleUrls: ["user_tasks.component.scss"] }) export class UserTasksComponent implements OnInit { public uid: number; public uname: string; public ispro: boolean = false; + @ViewChildren(ToasterComponent) viewChildren!: QueryList; + toasterForm = { + autohide: true, + delay: 10000, + position: "fixed", + fade: true, + closeButton: true, + }; + constructor( private data_provider: dataProvider, private router: Router, @@ -76,6 +88,55 @@ export class UserTasksComponent implements OnInit { public firmwaretoinstallv6: string = "none"; public updateBehavior: string = "keep"; public firms_loaded: boolean = false; + public predefinedCrons: any[] = [ + // High Frequency Monitoring + { label: 'Every minute', value: '* * * * *', description: 'Critical monitoring - runs every minute' }, + { label: 'Every 2 minutes', value: '*/2 * * * *', description: 'High frequency monitoring' }, + { label: 'Every 5 minutes', value: '*/5 * * * *', description: 'Standard monitoring interval' }, + { label: 'Every 10 minutes', value: '*/10 * * * *', description: 'Regular monitoring checks' }, + { label: 'Every 15 minutes', value: '*/15 * * * *', description: 'Moderate monitoring frequency' }, + { label: 'Every 30 minutes', value: '*/30 * * * *', description: 'Low frequency monitoring' }, + + // Hourly Operations + { label: 'Every hour', value: '0 * * * *', description: 'Hourly network checks' }, + { label: 'Every 2 hours', value: '0 */2 * * *', description: 'Bi-hourly operations' }, + { label: 'Every 4 hours', value: '0 */4 * * *', description: 'Quarterly daily checks' }, + { label: 'Every 6 hours', value: '0 */6 * * *', description: 'Four times daily' }, + { label: 'Every 8 hours', value: '0 */8 * * *', description: 'Three times daily' }, + { label: 'Every 12 hours', value: '0 */12 * * *', description: 'Twice daily operations' }, + + // Daily Maintenance + { label: 'Daily at midnight', value: '0 0 * * *', description: 'Daily maintenance at 00:00' }, + { label: 'Daily at 1 AM', value: '0 1 * * *', description: 'Daily backup at 01:00' }, + { label: 'Daily at 2 AM', value: '0 2 * * *', description: 'Daily maintenance at 02:00' }, + { label: 'Daily at 3 AM', value: '0 3 * * *', description: 'Low traffic maintenance at 03:00' }, + { label: 'Daily at 6 AM', value: '0 6 * * *', description: 'Pre-business hours check' }, + { label: 'Daily at 6 PM', value: '0 18 * * *', description: 'End of business day backup' }, + { label: 'Daily at 10 PM', value: '0 22 * * *', description: 'Evening maintenance at 22:00' }, + + // Business Hours + { label: 'Workdays at 8 AM', value: '0 8 * * 1-5', description: 'Start of business day - Mon to Fri' }, + { label: 'Workdays at 9 AM', value: '0 9 * * 1-5', description: 'Business hours start check' }, + { label: 'Workdays at 12 PM', value: '0 12 * * 1-5', description: 'Midday check - Mon to Fri' }, + { label: 'Workdays at 5 PM', value: '0 17 * * 1-5', description: 'End of business day - Mon to Fri' }, + { label: 'Workdays at 6 PM', value: '0 18 * * 1-5', description: 'After hours backup - Mon to Fri' }, + + // Weekly Operations + { label: 'Weekly (Sunday midnight)', value: '0 0 * * 0', description: 'Weekly maintenance - Sunday 00:00' }, + { label: 'Weekly (Monday midnight)', value: '0 0 * * 1', description: 'Weekly start - Monday 00:00' }, + { label: 'Weekly (Friday 6 PM)', value: '0 18 * * 5', description: 'End of week backup - Friday 18:00' }, + { label: 'Weekly (Saturday 2 AM)', value: '0 2 * * 6', description: 'Weekend maintenance - Saturday 02:00' }, + + // Monthly Operations + { label: 'Monthly (1st at midnight)', value: '0 0 1 * *', description: 'Monthly maintenance - 1st of month' }, + { label: 'Monthly (1st at 2 AM)', value: '0 2 1 * *', description: 'Monthly backup - 1st at 02:00' }, + { label: 'Monthly (15th at midnight)', value: '0 0 15 * *', description: 'Mid-month maintenance - 15th' }, + { label: 'Monthly (last day)', value: '0 0 28-31 * *', description: 'End of month operations' } + ]; + public showCronDropdown: boolean = false; + public cronSearch: string = ''; + public selectedCronPreset: any = null; + public filteredCrons: any[] = []; public sorting = { enabled: true, multiSorting: true, @@ -156,22 +217,43 @@ export class UserTasksComponent implements OnInit { this.initGridTable(); } + show_toast(title: string, body: string, color: string) { + const { ...props } = { ...this.toasterForm, color, title, body }; + const componentRef = this.viewChildren.first.addToast( + AppToastComponent, + props, + {} + ); + componentRef.instance["closeButton"] = props.closeButton; + } + submit(action: string) { var _self = this; if (action == "add") { this.data_provider .Add_task(_self.SelectedTask, _self.SelectedTaskItems) .then((res) => { - _self.initGridTable(); + if (res && res.status === 'failed') { + _self.show_toast("Error", res.err, "danger"); + } else { + _self.show_toast("Success", "Task created successfully", "success"); + _self.initGridTable(); + _self.EditTaskModalVisible = false; + } }); } else { this.data_provider .Edit_task(_self.SelectedTask, _self.SelectedTaskItems) .then((res) => { - _self.initGridTable(); + if (res && res.status === 'failed') { + _self.show_toast("Error", res.err, "danger"); + } else { + _self.show_toast("Success", "Task updated successfully", "success"); + _self.initGridTable(); + _self.EditTaskModalVisible = false; + } }); } - this.EditTaskModalVisible = false; } onSelectedRowsNewMembers(rows: Array): void { @@ -197,7 +279,7 @@ export class UserTasksComponent implements OnInit { this.SelectedTask = { id: 0, action: "add", - taskcron: "* * * * *", + cron: "0 2 * * *", desc_cron: "", description: "", members: "", @@ -206,7 +288,9 @@ export class UserTasksComponent implements OnInit { snippetid: "", task_type: "backup", }; - this.SelectedTask['data'] = { 'strategy': 'system', 'version_to_install': '', 'version_to_install_6': '' } + this.SelectedTask['data'] = { 'strategy': 'system', 'version_to_install': '', 'version_to_install_6': '' }; + this.cronSearch = ''; + this.selectedCronPreset = null; this.SelectedMembers = []; this.SelectedTaskItems = []; this.EditTaskModalVisible = true; @@ -215,6 +299,12 @@ export class UserTasksComponent implements OnInit { var _self = this; this.SelectedTask = { ...item }; + + // Initialize cron search and preset tracking + this.cronSearch = ''; + const currentCron = this.SelectedTask['cron']; + this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null; + if (this.SelectedTask['task_type'] == 'firmware' && 'data' in this.SelectedTask && this.SelectedTask['data']) { this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']); if (this.SelectedTask['data']['strategy'] == 'defined') { @@ -243,7 +333,7 @@ export class UserTasksComponent implements OnInit { } } - _self.data_provider.get_snippets("", "", "", 0, 1000).then((res) => { + _self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => { _self.Snippets = res.map((x: any) => { return { id: x.id, name: x.name }; }); @@ -315,7 +405,7 @@ export class UserTasksComponent implements OnInit { onSnippetsValueChanged(v: any) { var _self = this; if (v == "" || v.length < 3) return; - _self.data_provider.get_snippets(v, "", "", 0, 1000).then((res) => { + _self.data_provider.get_snippets(v, "", "", 0, 1000,false).then((res) => { _self.Snippets = res.map((x: any) => { return { id: String(x.id), name: x.name }; }); @@ -333,7 +423,12 @@ export class UserTasksComponent implements OnInit { } else { var _self = this; this.data_provider.Delete_task(_self.SelectedTask["id"]).then((res) => { - _self.initGridTable(); + if (res && res.status === 'failed') { + _self.show_toast("Error", res.err, "danger"); + } else { + _self.show_toast("Success", "Task deleted successfully", "success"); + _self.initGridTable(); + } _self.DeleteConfirmModalVisible = false; }); } @@ -363,4 +458,79 @@ export class UserTasksComponent implements OnInit { _self.loading = false; }); } + + selectCron(cron: any): void { + this.SelectedTask['cron'] = cron.value; + this.selectedCronPreset = cron; + this.cronSearch = ''; + this.showCronDropdown = false; + } + + filterCrons(event: any): void { + const searchTerm = event.target.value.toLowerCase(); + this.cronSearch = searchTerm; + this.selectedCronPreset = null; + + if (searchTerm.length > 0) { + this.filteredCrons = this.predefinedCrons.filter(cron => + cron.label.toLowerCase().includes(searchTerm) || + cron.description.toLowerCase().includes(searchTerm) || + cron.value.includes(searchTerm) + ); + } else { + this.filteredCrons = this.predefinedCrons; + } + } + + hideCronDropdown(): void { + setTimeout(() => { + this.showCronDropdown = false; + }, 200); + } + + onCronInputFocus(): void { + this.filteredCrons = this.predefinedCrons; + this.showCronDropdown = true; + + // If current cron matches a preset, highlight it + const currentCron = this.SelectedTask['cron']; + this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null; + } + + onCronInputChange(event: any): void { + this.SelectedTask['cron'] = event.target.value; + this.selectedCronPreset = null; + + // Check if the entered value matches any preset + const enteredValue = event.target.value; + const matchingPreset = this.predefinedCrons.find(cron => cron.value === enteredValue); + if (matchingPreset) { + this.selectedCronPreset = matchingPreset; + } + } + + getCronDescription(): string { + if (this.selectedCronPreset) { + return this.selectedCronPreset.description; + } + + const currentCron = this.SelectedTask['cron']; + const matchingPreset = this.predefinedCrons.find(cron => cron.value === currentCron); + return matchingPreset ? matchingPreset.description : 'Custom cron expression'; + } + + onTaskTypeChange(): void { + if (this.SelectedTask['task_type'] === 'snippet') { + this.loadSnippets(); + } + } + + loadSnippets(): void { + var _self = this; + _self.data_provider.get_snippets("", "", "", 0, 10).then((res) => { + _self.Snippets = res.map((x: any) => { + return { id: x.id, name: x.name }; + }); + }); + } } diff --git a/src/app/views/user_tasks/user_tasks.module.ts b/src/app/views/user_tasks/user_tasks.module.ts index fdc3f47..e59ef63 100644 --- a/src/app/views/user_tasks/user_tasks.module.ts +++ b/src/app/views/user_tasks/user_tasks.module.ts @@ -9,6 +9,9 @@ import { GridModule, ModalModule, ButtonGroupModule, + BadgeModule, + AlertModule, + ToastModule, } from "@coreui/angular"; import { UserTasksRoutingModule } from "./user_tasks-routing.module"; import { UserTasksComponent } from "./user_tasks.component"; @@ -25,6 +28,9 @@ import { NgxSuperSelectModule} from "ngx-super-select"; FormModule, ButtonModule, ButtonGroupModule, + BadgeModule, + AlertModule, + ToastModule, GuiGridModule, ModalModule, ReactiveFormsModule, diff --git a/src/app/views/vault/vault.component.html b/src/app/views/vault/vault.component.html index 762126c..41661a3 100644 --- a/src/app/views/vault/vault.component.html +++ b/src/app/views/vault/vault.component.html @@ -1,360 +1,398 @@ - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - -
    User Exceptions
    -
    - - - - -
    -
    - -
    -
    - -
    -
    -
    - - - - -   {{value}} - - - - - - - - -
    - -
    - - -
    Password list
    -
    - - - - -
    -
    - -
    -
    - -
    -
    -
    - - - -   {{value}} - - - - - - - -
    -
    -
    - - - -
    -
    -
    - - - - -
    Efected Groups
    - - - -   {{value}} - - - - - - - -
    - - - - -
    -
    -
    - - - - -
    Reports
    - - - -   {{value}} - - - -   {{value}} - - - - - - - -
    -
    -
    -
    -
    - - -
    - - - Username - - - - Device IP - - - - Device Name - - - -
    -
    - - - - - - - -   {{value}} - - - - {{value}} + + + +
    +
    + +
    +
    Password Vault Settings
    +

    Configure automated password management for your device groups. Set schedules, define password policies, and manage affected devices.

    +
    +
    +
    + + +
    +
    Password Vault Configuration
    + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + +
    User Exceptions
    + Users excluded from password changes +
    + +
    + + +
    +
    +
    +
    + + + + {{value}} - - - {{value}} - - - - - {{value}} - - - + - - - - - +
    +
    + +
    + + +
    Password List
    + Pre-defined passwords for rotation +
    + +
    + + +
    +
    +
    +
    + + + + •••••••• + + + + + + + + +
    +
    + +
    + + +
    Affected Device Groups
    + Device groups where password changes will be applied +
    + + + +
    +
    + + + + {{value}} + + + + + + + + +
    +
    + +
    +
    + + +
    +
    Execution Reports
    + + + + {{value}} + + + + + {{value}} + + + + + + + + +
    +
    +
    +
    + + + + +
    +
    + +
    +
    Stored Passwords
    +

    View and manage passwords stored by the vault system. Use filters to find specific devices or users.

    +
    +
    +
    + + +
    +
    Filters
    + + + + Username + + + + + + Device IP + + + + + + Device Name + + + + +
    + + +
    +
    Stored Passwords
    + + + +
    + +
    +
    {{value}}
    + {{item.devip}} +
    +
    +
    +
    + + + {{value}} + + + + + {{value}} + + + + + + + +
    +
    +
    +
    + -
    Password
    +
    Password Reveal
    -

    - - - - -

    - - Your attempt to reveal password is logged in system! - +
    + + Security Notice: Your attempt to reveal this password is logged in the system. +
    +
    + + + +
    - +
    - - + -
    Confirm RUN {{ SelectedTask['name'] }}
    +
    Confirm Execution
    - Are you sure that You want to run Vault Password Job ? -
    +
    + Warning: This will execute the password vault job and change passwords on all configured device groups. +
    +

    Are you sure you want to proceed with the password vault execution?

    - +
    - + -
    Editing Group
    +
    Add Device Groups
    - -
    Group Members :
    - - - -   {{value}} - - - - {{value}} - - - - - {{value}} - - - -
    -
    -
    +

    Select device groups to include in the password vault task:

    + + + + {{value}} + + + + + {{value}} + + + + + {{value}} + + +
    - - - +
    + + + All Local Users:
    + Changes passwords for all router users except exceptions.

    + MikroWizard Users Only:
    + Changes only passwords for users defined in MikroWizard system. +
    + + + Frequency:
    + How often passwords should be changed automatically.

    + Manual:
    + Only when executed manually. +
    + + + Random:
    + System generates random passwords.

    + Pre-defined:
    + Use passwords from your custom list. +
    + \ No newline at end of file diff --git a/src/app/views/vault/vault.component.ts b/src/app/views/vault/vault.component.ts index acf81ee..9fbe59a 100644 --- a/src/app/views/vault/vault.component.ts +++ b/src/app/views/vault/vault.component.ts @@ -201,6 +201,14 @@ export class VaultComponent implements OnInit { get_passwords(){ var _self=this; this.data_provider.get_passwords(this.filters).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.passwords=res.data.map((d: any) => { d.changed = formatInTimeZone( d.changed.split(".")[0] + ".000Z", @@ -209,6 +217,7 @@ export class VaultComponent implements OnInit { ); return d; }); + } }); } @@ -216,8 +225,17 @@ export class VaultComponent implements OnInit { var _self=this; _self.password=""; this.data_provider.reveal_password(devid,username).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.password=res.password; _self.PasswordModalVisible=true; + } }); } @@ -255,6 +273,14 @@ export class VaultComponent implements OnInit { get_vault_history(){ var _self=this; this.data_provider.vault_history().then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ let index = 1; _self.vault_history=res.data.map((d: any) => { d.index = index; @@ -275,6 +301,7 @@ export class VaultComponent implements OnInit { index += 1; return d; }); + } }); } @@ -408,6 +435,14 @@ export class VaultComponent implements OnInit { + copyToClipboard(text: string) { + navigator.clipboard.writeText(text).then(() => { + this.show_toast('Success', 'Password copied to clipboard', 'success'); + }).catch(() => { + this.show_toast('Error', 'Failed to copy password', 'danger'); + }); + } + logger(item: any) { console.dir(item); } diff --git a/src/app/views/vault/vault.module.ts b/src/app/views/vault/vault.module.ts index e98d364..d52b8c2 100644 --- a/src/app/views/vault/vault.module.ts +++ b/src/app/views/vault/vault.module.ts @@ -12,6 +12,7 @@ import { TabsModule, ToastModule, CollapseModule, + TooltipModule, } from "@coreui/angular"; import { VaultRoutingModule } from "./vault-routing.module"; import { VaultComponent } from "./vault.component"; @@ -36,7 +37,8 @@ import { MatFormFieldModule } from "@angular/material/form-field"; ToastModule, MatInputModule, MatFormFieldModule, - CollapseModule + CollapseModule, + TooltipModule ], declarations: [VaultComponent], }) diff --git a/src/app/views/vault/vault.scss b/src/app/views/vault/vault.scss index 6acb3bb..da87598 100644 --- a/src/app/views/vault/vault.scss +++ b/src/app/views/vault/vault.scss @@ -1,29 +1,278 @@ - +// Compact Navigation Styles .nav-underline { - border-bottom: 2px solid var(--cui-nav-underline-border-color, #c4c9d0) + border-bottom: 2px solid var(--cui-nav-underline-border-color, #c4c9d0); + + .nav-item { + margin-bottom: -2px; + cursor: pointer; + } + + .nav-link { + color: var(--cui-nav-underline-link-color, #768192); + border-style: none none solid!important; + border-width: 2px; + position: relative; + bottom: -1px; + cursor: pointer; + font-size: 0.9rem; + padding: 0.5rem 1rem; + + &:hover, &:focus { + border-color: var(--cui-nav-underline-link-active-border-color, #321fdb); + } + + &.active, .show > & { + color: var(--cui-nav-underline-link-active-color, #321fdb); + background: transparent; + border-color: var(--cui-nav-underline-link-active-border-color, #321fdb); + } + } } -.nav-underline .nav-item { - margin-bottom: -2px; - cursor: pointer; +// Compact Form Styles +.form-label.small { + font-size: 0.8rem; + font-weight: 600; + color: #495057; + margin-bottom: 0.25rem; } -.nav-underline .nav-link { - color: var(--cui-nav-underline-link-color, #768192); - border-style: none none solid!important; - border-width:2px; - position:relative; - bottom:-1px; - cursor: pointer; - +.form-select-sm { + font-size: 0.85rem; + padding: 0.25rem 0.5rem; } -.nav-underline .nav-link:hover,.nav-underline .nav-link:focus { - border-color: var(--cui-nav-underline-link-active-border-color, #321fdb) +// Card Header Improvements +.c-card-header { + background: #f8f9fa; + border-bottom: 1px solid #dee2e6; + padding: 0.75rem 1rem; + + h6 { + font-size: 0.9rem; + font-weight: 600; + color: #495057; + + i { + color: #6c757d; + } + } + + .text-muted { + font-size: 0.75rem; + } } -.nav-underline .nav-link.active,.nav-underline .show>.nav-link { - color: var(--cui-nav-underline-link-active-color, #321fdb); - background: transparent; - border-color: var(--cui-nav-underline-link-active-border-color, #321fdb) +// Grid Improvements +gui-grid { + .gui-grid { + font-size: 0.85rem; + } } + +// Input Group Compact +.input-group-sm { + .form-control, .input-group-text { + font-size: 0.8rem; + padding: 0.25rem 0.5rem; + } +} + +// Alert Improvements +.alert { + font-size: 0.85rem; + padding: 0.5rem 0.75rem; + + i { + margin-right: 0.5rem; + } +} + +// Button Improvements +.btn { + font-size: 0.85rem; + + &.btn-sm { + font-size: 0.8rem; + padding: 0.25rem 0.5rem; + } + + i { + font-size: 0.8rem; + } +} + +// Modal Improvements +.c-modal-header { + padding: 0.75rem 1rem; + + h6 { + font-size: 0.95rem; + font-weight: 600; + } +} + +.c-modal-body { + padding: 1rem; + font-size: 0.9rem; +} + +.c-modal-footer { + padding: 0.75rem 1rem; +} + +// Responsive Design +@media (max-width: 768px) { + .c-card-header { + .nav-underline { + margin-bottom: 0.5rem; + } + + .text-end { + text-align: left !important; + } + } + + .input-group-sm { + flex-direction: column; + + .form-control { + margin-bottom: 0.25rem; + } + + .btn { + width: 100%; + } + } + + .c-modal-dialog { + margin: 0.5rem; + } + + .c-card-footer { + .btn { + width: 100%; + margin-bottom: 0.5rem; + + &:last-child { + margin-bottom: 0; + } + } + } +} + +@media (max-width: 576px) { + .nav-underline .nav-link { + font-size: 0.8rem; + padding: 0.4rem 0.8rem; + } + + .c-card-header h6 { + font-size: 0.85rem; + } + + .form-label.small { + font-size: 0.75rem; + } + + .btn { + font-size: 0.8rem; + + &.btn-sm { + font-size: 0.75rem; + padding: 0.2rem 0.4rem; + } + } +} + +// Material Form Field Compact +::ng-deep .mat-form-field { + .mat-form-field-wrapper { + padding-bottom: 0.5em; + } + + .mat-form-field-infix { + padding: 0.5em 0; + } + + .mat-form-field-label { + font-size: 0.85rem; + } + + input { + font-size: 0.85rem; + } +} + +// Grid Cell Improvements +::ng-deep gui-grid { + .gui-grid-cell { + padding: 0.5rem; + font-size: 0.85rem; + } + + .gui-grid-header-cell { + font-size: 0.8rem; + font-weight: 600; + padding: 0.5rem; + } +} + +// Tab Info Headers +.tab-info-header { + padding: 1rem 0; + + h5 { + color: #495057; + font-weight: 600; + font-size: 1.1rem; + } + + .fs-4 { + font-size: 1.5rem !important; + } +} + +// Utility Classes +.fw-semibold { + font-weight: 600; +} + +.text-xs { + font-size: 0.75rem; +} + +.compact-spacing { + .c-row { + margin-bottom: 0.5rem; + } + + .c-col { + padding: 0.25rem; + } +} + +// Responsive tab headers +@media (max-width: 576px) { + .tab-info-header { + padding: 0.75rem 0; + + h5 { + font-size: 1rem; + } + + .fs-4 { + font-size: 1.25rem !important; + } + + .d-flex { + flex-direction: column; + text-align: center; + } + + .me-2 { + margin-right: 0 !important; + margin-bottom: 0.5rem; + } + } +} \ No newline at end of file diff --git a/src/assets/Network-Icons-SVG/README.md b/src/assets/Network-Icons-SVG/README.md new file mode 100644 index 0000000..5f99005 --- /dev/null +++ b/src/assets/Network-Icons-SVG/README.md @@ -0,0 +1,6 @@ +# Network Icons SVG + +A collection of SVG format network icons. These can be used freely and don't adhere to any vendor at all, meaning so that you can use the router icon to represent any brand router in your diagram. +Being SVG a vectorial graphics format, no resizing, format change or conversion operation will degrade icon quality. + +This icons were originally shared by BobTheButcher. \ No newline at end of file diff --git a/src/assets/Network-Icons-SVG/arista-switch.svg b/src/assets/Network-Icons-SVG/arista-switch.svg new file mode 100644 index 0000000..9b7010b --- /dev/null +++ b/src/assets/Network-Icons-SVG/arista-switch.svg @@ -0,0 +1,46 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cisco-asa.svg b/src/assets/Network-Icons-SVG/cisco-asa.svg new file mode 100644 index 0000000..62863b0 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cisco-asa.svg @@ -0,0 +1,117 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cisco-nexus.svg b/src/assets/Network-Icons-SVG/cisco-nexus.svg new file mode 100644 index 0000000..44b3907 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cisco-nexus.svg @@ -0,0 +1,74 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cisco-router-firewall.svg b/src/assets/Network-Icons-SVG/cisco-router-firewall.svg new file mode 100644 index 0000000..25ec316 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cisco-router-firewall.svg @@ -0,0 +1,127 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cisco-router.svg b/src/assets/Network-Icons-SVG/cisco-router.svg new file mode 100644 index 0000000..0947707 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cisco-router.svg @@ -0,0 +1,58 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cisco-switch-l2.svg b/src/assets/Network-Icons-SVG/cisco-switch-l2.svg new file mode 100644 index 0000000..93b76f4 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cisco-switch-l2.svg @@ -0,0 +1,58 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cisco-switch-l3-with-rp.svg b/src/assets/Network-Icons-SVG/cisco-switch-l3-with-rp.svg new file mode 100644 index 0000000..b8d84df --- /dev/null +++ b/src/assets/Network-Icons-SVG/cisco-switch-l3-with-rp.svg @@ -0,0 +1,65 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cisco-switch-l3.svg b/src/assets/Network-Icons-SVG/cisco-switch-l3.svg new file mode 100644 index 0000000..cc5f984 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cisco-switch-l3.svg @@ -0,0 +1,51 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cisco-switch-vss.svg b/src/assets/Network-Icons-SVG/cisco-switch-vss.svg new file mode 100644 index 0000000..c9f29a9 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cisco-switch-vss.svg @@ -0,0 +1,83 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cloud-v1.svg b/src/assets/Network-Icons-SVG/cloud-v1.svg new file mode 100644 index 0000000..7b89ce8 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cloud-v1.svg @@ -0,0 +1,43 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cloud-v2.svg b/src/assets/Network-Icons-SVG/cloud-v2.svg new file mode 100644 index 0000000..b004c42 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cloud-v2.svg @@ -0,0 +1,39 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cumulus-router-v2.svg b/src/assets/Network-Icons-SVG/cumulus-router-v2.svg new file mode 100644 index 0000000..9b6f33c --- /dev/null +++ b/src/assets/Network-Icons-SVG/cumulus-router-v2.svg @@ -0,0 +1,49 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cumulus-router-v3.svg b/src/assets/Network-Icons-SVG/cumulus-router-v3.svg new file mode 100644 index 0000000..6839245 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cumulus-router-v3.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/cumulus-switch-v2.svg b/src/assets/Network-Icons-SVG/cumulus-switch-v2.svg new file mode 100644 index 0000000..fb5fe39 --- /dev/null +++ b/src/assets/Network-Icons-SVG/cumulus-switch-v2.svg @@ -0,0 +1,53 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/f5-gtm.svg b/src/assets/Network-Icons-SVG/f5-gtm.svg new file mode 100644 index 0000000..e907799 --- /dev/null +++ b/src/assets/Network-Icons-SVG/f5-gtm.svg @@ -0,0 +1,43 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/f5-ltm.svg b/src/assets/Network-Icons-SVG/f5-ltm.svg new file mode 100644 index 0000000..646e981 --- /dev/null +++ b/src/assets/Network-Icons-SVG/f5-ltm.svg @@ -0,0 +1,47 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-adc-colour-3d.svg b/src/assets/Network-Icons-SVG/generic-adc-colour-3d.svg new file mode 100644 index 0000000..71c4c9b --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-adc-colour-3d.svg @@ -0,0 +1,63 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-adc-colour.svg b/src/assets/Network-Icons-SVG/generic-adc-colour.svg new file mode 100644 index 0000000..c6a159a --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-adc-colour.svg @@ -0,0 +1,42 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-adc-mono.svg b/src/assets/Network-Icons-SVG/generic-adc-mono.svg new file mode 100644 index 0000000..1b6e916 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-adc-mono.svg @@ -0,0 +1,42 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v1-colour.svg b/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v1-colour.svg new file mode 100644 index 0000000..ea0a1b3 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v1-colour.svg @@ -0,0 +1,147 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v1-mono.svg b/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v1-mono.svg new file mode 100644 index 0000000..c8ac0f1 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v1-mono.svg @@ -0,0 +1,150 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v2-colour.svg b/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v2-colour.svg new file mode 100644 index 0000000..427c19f --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v2-colour.svg @@ -0,0 +1,142 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v2-mono.svg b/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v2-mono.svg new file mode 100644 index 0000000..d9808a8 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-firewall-flat-colour-label-v2-mono.svg @@ -0,0 +1,151 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-firewall-v1-colour-3d.svg b/src/assets/Network-Icons-SVG/generic-firewall-v1-colour-3d.svg new file mode 100644 index 0000000..d82cca3 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-firewall-v1-colour-3d.svg @@ -0,0 +1,175 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-firewall-v1-colour.svg b/src/assets/Network-Icons-SVG/generic-firewall-v1-colour.svg new file mode 100644 index 0000000..642f031 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-firewall-v1-colour.svg @@ -0,0 +1,130 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-firewall-v1-mono.svg b/src/assets/Network-Icons-SVG/generic-firewall-v1-mono.svg new file mode 100644 index 0000000..b28ae04 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-firewall-v1-mono.svg @@ -0,0 +1,130 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-firewall-v2-colour-3d.svg b/src/assets/Network-Icons-SVG/generic-firewall-v2-colour-3d.svg new file mode 100644 index 0000000..70c7d7b --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-firewall-v2-colour-3d.svg @@ -0,0 +1,152 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-firewall-v2-colour.svg b/src/assets/Network-Icons-SVG/generic-firewall-v2-colour.svg new file mode 100644 index 0000000..575b40e --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-firewall-v2-colour.svg @@ -0,0 +1,135 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-gtm-colour-3d.svg b/src/assets/Network-Icons-SVG/generic-gtm-colour-3d.svg new file mode 100644 index 0000000..2f7d9a6 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-gtm-colour-3d.svg @@ -0,0 +1,60 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-gtm-colour.svg b/src/assets/Network-Icons-SVG/generic-gtm-colour.svg new file mode 100644 index 0000000..6c07ad6 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-gtm-colour.svg @@ -0,0 +1,42 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-gtm-mono.svg b/src/assets/Network-Icons-SVG/generic-gtm-mono.svg new file mode 100644 index 0000000..ee2b6ee --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-gtm-mono.svg @@ -0,0 +1,39 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-router-colour.svg b/src/assets/Network-Icons-SVG/generic-router-colour.svg new file mode 100644 index 0000000..2a10047 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-router-colour.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-router-flat-label-colour.svg b/src/assets/Network-Icons-SVG/generic-router-flat-label-colour.svg new file mode 100644 index 0000000..70ba721 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-router-flat-label-colour.svg @@ -0,0 +1,47 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-router-flat-label-mono.svg b/src/assets/Network-Icons-SVG/generic-router-flat-label-mono.svg new file mode 100644 index 0000000..daa3868 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-router-flat-label-mono.svg @@ -0,0 +1,50 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-router-mono.svg b/src/assets/Network-Icons-SVG/generic-router-mono.svg new file mode 100644 index 0000000..4a487e4 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-router-mono.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-router-square-colour-3d.svg b/src/assets/Network-Icons-SVG/generic-router-square-colour-3d.svg new file mode 100644 index 0000000..624ab70 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-router-square-colour-3d.svg @@ -0,0 +1,93 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-router-square-colour.svg b/src/assets/Network-Icons-SVG/generic-router-square-colour.svg new file mode 100644 index 0000000..e524305 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-router-square-colour.svg @@ -0,0 +1,39 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-router-square-mono.svg b/src/assets/Network-Icons-SVG/generic-router-square-mono.svg new file mode 100644 index 0000000..cb9e6ee --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-router-square-mono.svg @@ -0,0 +1,42 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-server-1.svg b/src/assets/Network-Icons-SVG/generic-server-1.svg new file mode 100644 index 0000000..3eb614e --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-server-1.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-server-2.svg b/src/assets/Network-Icons-SVG/generic-server-2.svg new file mode 100644 index 0000000..f3919fe --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-server-2.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-server-3.svg b/src/assets/Network-Icons-SVG/generic-server-3.svg new file mode 100644 index 0000000..ad4bb5b --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-server-3.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v1-colour.svg b/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v1-colour.svg new file mode 100644 index 0000000..9d3c1cb --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v1-colour.svg @@ -0,0 +1,55 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v1-mono.svg b/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v1-mono.svg new file mode 100644 index 0000000..3a93303 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v1-mono.svg @@ -0,0 +1,54 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v2-colour.svg b/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v2-colour.svg new file mode 100644 index 0000000..03f7ef9 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v2-colour.svg @@ -0,0 +1,54 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v2-mono.svg b/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v2-mono.svg new file mode 100644 index 0000000..01e8233 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-flat-l2-label-v2-mono.svg @@ -0,0 +1,54 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-flat-l3-label-colour.svg b/src/assets/Network-Icons-SVG/generic-switch-flat-l3-label-colour.svg new file mode 100644 index 0000000..a8552fd --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-flat-l3-label-colour.svg @@ -0,0 +1,47 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-flat-l3-label-mono.svg b/src/assets/Network-Icons-SVG/generic-switch-flat-l3-label-mono.svg new file mode 100644 index 0000000..bddd9ee --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-flat-l3-label-mono.svg @@ -0,0 +1,50 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-l2-v1-colour-3d.svg b/src/assets/Network-Icons-SVG/generic-switch-l2-v1-colour-3d.svg new file mode 100644 index 0000000..b193882 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-l2-v1-colour-3d.svg @@ -0,0 +1,98 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-l2-v1-colour.svg b/src/assets/Network-Icons-SVG/generic-switch-l2-v1-colour.svg new file mode 100644 index 0000000..96da516 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-l2-v1-colour.svg @@ -0,0 +1,50 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-l2-v1-mono.svg b/src/assets/Network-Icons-SVG/generic-switch-l2-v1-mono.svg new file mode 100644 index 0000000..b7d7204 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-l2-v1-mono.svg @@ -0,0 +1,46 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-l2-v2-colour-3d.svg b/src/assets/Network-Icons-SVG/generic-switch-l2-v2-colour-3d.svg new file mode 100644 index 0000000..de6ebb7 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-l2-v2-colour-3d.svg @@ -0,0 +1,98 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-l2-v2-colour.svg b/src/assets/Network-Icons-SVG/generic-switch-l2-v2-colour.svg new file mode 100644 index 0000000..4e2dbfa --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-l2-v2-colour.svg @@ -0,0 +1,47 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-l2-v2-mono.svg b/src/assets/Network-Icons-SVG/generic-switch-l2-v2-mono.svg new file mode 100644 index 0000000..0920f85 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-l2-v2-mono.svg @@ -0,0 +1,46 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-l3-colour-3d.svg b/src/assets/Network-Icons-SVG/generic-switch-l3-colour-3d.svg new file mode 100644 index 0000000..c4c49bb --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-l3-colour-3d.svg @@ -0,0 +1,90 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-l3-colour.svg b/src/assets/Network-Icons-SVG/generic-switch-l3-colour.svg new file mode 100644 index 0000000..c13ddce --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-l3-colour.svg @@ -0,0 +1,40 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/generic-switch-l3-mono.svg b/src/assets/Network-Icons-SVG/generic-switch-l3-mono.svg new file mode 100644 index 0000000..dd8c096 --- /dev/null +++ b/src/assets/Network-Icons-SVG/generic-switch-l3-mono.svg @@ -0,0 +1,44 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/juniper-firewall-router.svg b/src/assets/Network-Icons-SVG/juniper-firewall-router.svg new file mode 100644 index 0000000..f465936 --- /dev/null +++ b/src/assets/Network-Icons-SVG/juniper-firewall-router.svg @@ -0,0 +1,147 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/juniper-router.svg b/src/assets/Network-Icons-SVG/juniper-router.svg new file mode 100644 index 0000000..2462287 --- /dev/null +++ b/src/assets/Network-Icons-SVG/juniper-router.svg @@ -0,0 +1,54 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/juniper-switch-l2.svg b/src/assets/Network-Icons-SVG/juniper-switch-l2.svg new file mode 100644 index 0000000..5ce44d8 --- /dev/null +++ b/src/assets/Network-Icons-SVG/juniper-switch-l2.svg @@ -0,0 +1,61 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/juniper-switch-l3.svg b/src/assets/Network-Icons-SVG/juniper-switch-l3.svg new file mode 100644 index 0000000..3303efd --- /dev/null +++ b/src/assets/Network-Icons-SVG/juniper-switch-l3.svg @@ -0,0 +1,54 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/network-icons.svg b/src/assets/Network-Icons-SVG/network-icons.svg new file mode 100644 index 0000000..4a4274b --- /dev/null +++ b/src/assets/Network-Icons-SVG/network-icons.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/assets/Network-Icons-SVG/viptela-vbond.svg b/src/assets/Network-Icons-SVG/viptela-vbond.svg new file mode 100644 index 0000000..696fc25 --- /dev/null +++ b/src/assets/Network-Icons-SVG/viptela-vbond.svg @@ -0,0 +1,46 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/viptela-vedge-square.svg b/src/assets/Network-Icons-SVG/viptela-vedge-square.svg new file mode 100644 index 0000000..7edbda2 --- /dev/null +++ b/src/assets/Network-Icons-SVG/viptela-vedge-square.svg @@ -0,0 +1,60 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/viptela-vedge.svg b/src/assets/Network-Icons-SVG/viptela-vedge.svg new file mode 100644 index 0000000..27ad7e4 --- /dev/null +++ b/src/assets/Network-Icons-SVG/viptela-vedge.svg @@ -0,0 +1,61 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/viptela-vmanage.svg b/src/assets/Network-Icons-SVG/viptela-vmanage.svg new file mode 100644 index 0000000..cf24c84 --- /dev/null +++ b/src/assets/Network-Icons-SVG/viptela-vmanage.svg @@ -0,0 +1,45 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/viptela-vsmart.svg b/src/assets/Network-Icons-SVG/viptela-vsmart.svg new file mode 100644 index 0000000..de760d0 --- /dev/null +++ b/src/assets/Network-Icons-SVG/viptela-vsmart.svg @@ -0,0 +1,46 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/assets/Network-Icons-SVG/vmware-compute.svg b/src/assets/Network-Icons-SVG/vmware-compute.svg new file mode 100644 index 0000000..5a67f76 --- /dev/null +++ b/src/assets/Network-Icons-SVG/vmware-compute.svg @@ -0,0 +1,63 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + +