diff --git a/client-reactjs/package-lock.json b/client-reactjs/package-lock.json index 2083dbe8a..6ffbbbaab 100644 --- a/client-reactjs/package-lock.json +++ b/client-reactjs/package-lock.json @@ -459,6 +459,16 @@ "regl": "^1.3.11" } }, + "node_modules/@antv/g-webgpu-engine/node_modules/@webgpu/types": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.0.31.tgz", + "integrity": "sha512-cvvCMSZBT4VsRNtt0lI6XQqvOIIWw6+NRUtnPUMDVDgsI4pCZColz3qzF5QcP9wIYOHEc3jssIBse8UWONKhlQ==" + }, + "node_modules/@antv/g-webgpu/node_modules/@webgpu/types": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.0.31.tgz", + "integrity": "sha512-cvvCMSZBT4VsRNtt0lI6XQqvOIIWw6+NRUtnPUMDVDgsI4pCZColz3qzF5QcP9wIYOHEc3jssIBse8UWONKhlQ==" + }, "node_modules/@antv/g2": { "version": "5.1.15", "resolved": "https://registry.npmjs.org/@antv/g2/-/g2-5.1.15.tgz", @@ -1323,7 +1333,7 @@ "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.13.0" } }, "node_modules/@babel/plugin-proposal-decorators": { @@ -1340,7 +1350,7 @@ "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { @@ -2175,7 +2185,7 @@ "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-new-target": { @@ -2418,7 +2428,7 @@ "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { @@ -4243,13 +4253,13 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -4264,9 +4274,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -4277,8 +4287,8 @@ "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -4287,9 +4297,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -4986,7 +4996,7 @@ "wait-for-expect": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">= 8.3" } }, "node_modules/@testing-library/react/node_modules/@types/istanbul-reports": { @@ -5036,6 +5046,11 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, "engines": { "node": ">=6" } @@ -6057,9 +6072,9 @@ "dev": true }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -6079,9 +6094,9 @@ "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { @@ -6102,15 +6117,15 @@ "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { @@ -6138,28 +6153,28 @@ "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -6167,24 +6182,24 @@ } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -6193,12 +6208,12 @@ } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -6207,11 +6222,6 @@ "resolved": "https://registry.npmjs.org/@webgpu/glslang/-/glslang-0.0.15.tgz", "integrity": "sha512-niT+Prh3Aff8Uf1MVBVUsaNjFj9rJAKDXuoHIKiQbB+6IUP/3J3JIhBNyZ7lDhytvXxw6ppgnwKZdDJ08UMj4Q==" }, - "node_modules/@webgpu/types": { - "version": "0.0.31", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.0.31.tgz", - "integrity": "sha512-cvvCMSZBT4VsRNtt0lI6XQqvOIIWw6+NRUtnPUMDVDgsI4pCZColz3qzF5QcP9wIYOHEc3jssIBse8UWONKhlQ==" - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -7315,7 +7325,7 @@ "dev": true, "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -7368,6 +7378,21 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -7853,15 +7878,12 @@ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "node_modules/coa/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/coa/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -7924,6 +7946,21 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "extraneous": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "extraneous": true + }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -8545,12 +8582,12 @@ } }, "node_modules/css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", "dev": true, "dependencies": { - "mdn-data": "2.0.4", + "mdn-data": "2.0.14", "source-map": "^0.6.1" }, "engines": { @@ -8709,34 +8746,6 @@ "node": ">=8.0.0" } }, - "node_modules/csso/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "node_modules/csso/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -9579,7 +9588,7 @@ "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", @@ -11465,9 +11474,9 @@ } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -12338,9 +12347,9 @@ "dev": true }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "engines": { "node": ">= 0.4" }, @@ -12448,11 +12457,14 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12653,6 +12665,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/istanbul-lib-report/node_modules/yallist": { @@ -15305,9 +15320,9 @@ } }, "node_modules/mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "dev": true }, "node_modules/media-typer": { @@ -15839,12 +15854,12 @@ } }, "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -17706,34 +17721,6 @@ "postcss": "^8.2.15" } }, - "node_modules/postcss-svgo/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/postcss-svgo/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "node_modules/postcss-svgo/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-svgo/node_modules/svgo": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", @@ -18047,42 +18034,6 @@ "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/rbush": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", @@ -19314,9 +19265,9 @@ } }, "node_modules/regl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/regl/-/regl-1.7.0.tgz", - "integrity": "sha512-bEAtp/qrtKucxXSJkD4ebopFZYP0q1+3Vb2WECWv/T8yQEgKxDxJ7ztO285tAMaYZVR6mM1GgI6CCn8FROtL1w==" + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/regl/-/regl-1.6.1.tgz", + "integrity": "sha512-7Z9rmpEqmLNwC9kCYCyfyu47eWZaQWeNpwZfwz99QueXN8B/Ow40DB0N+OeUeM/yu9pZAB01+JgJ+XghGveVoA==" }, "node_modules/relateurl": { "version": "0.2.7", @@ -21055,6 +21006,19 @@ "nth-check": "^1.0.2" } }, + "node_modules/svgo/node_modules/css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/svgo/node_modules/css-what": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", @@ -21133,6 +21097,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/svgo/node_modules/mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, "node_modules/svgo/node_modules/nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -21142,6 +21112,15 @@ "boolbase": "~1.0.0" } }, + "node_modules/svgo/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/svgo/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -22269,9 +22248,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", diff --git a/client-reactjs/src/App.js b/client-reactjs/src/App.js index 86530eb2a..f1144e6e1 100644 --- a/client-reactjs/src/App.js +++ b/client-reactjs/src/App.js @@ -39,9 +39,7 @@ const OrbitMonitoring = React.lazy(() => import('./components/application/orbitM const SuperFileMonitoring = React.lazy(() => import('./components/application/superfileMonitoring/SuperFileMonitoring') ); -// const Notifications = React.lazy(() => import('./components/application/dashboard/notifications/Notifications')); const Orbit = React.lazy(() => import('./components/application/dashboard/Orbit/Orbit')); -// const ClusterUsage = React.lazy(() => import('./components/application/dashboard/clusterUsage/ClusterUsage')); const Notifications = React.lazy(() => import('./components/application/dashboard/notifications')); const ClusterUsage = React.lazy(() => import('./components/application/dashboard/clusterUsage/')); const ClusterMonitoring = React.lazy(() => import('./components/application/clusterMonitoring')); @@ -57,7 +55,8 @@ const Regulations = React.lazy(() => import('./components/admin/ControlsAndRegul const GitHubSettings = React.lazy(() => import('./components/admin/GitHubSettings/GitHubSettings')); const ScheduledJobsPage = React.lazy(() => import('./components/admin/ScheduledJobsPage')); const Compliance = React.lazy(() => import('./components/admin/Compliance/Compliance')); -const Integrations = React.lazy(() => import('./components/admin/Integrations/Integrations')); +const Integrations = React.lazy(() => import('./components/admin/Integrations')); +const IntegrationSettings = React.lazy(() => import('./components/admin/Integrations/IntegrationSettings')); const TeamsNotification = React.lazy(() => import('./components/admin/notifications/MsTeams/Teams')); // Shared layout, etc. @@ -213,6 +212,7 @@ class App extends React.Component { + { - const [notificationForm] = Form.useForm(); - const [teamsHooks, setTeamsHook] = useState([]); - - useEffect(() => { - //Get all teams hook - (async () => { - try { - const allTeamsHook = await getAllTeamsHook(); - setTeamsHook(allTeamsHook); - } catch (error) { - message.error('Error fetching teams hook'); - } - })(); - }, []); - - useEffect(() => { - notificationForm.setFieldsValue({ - notificationEmailsSev3: selectedIntegration?.metaData?.notificationEmailsSev3, - megaphone: selectedIntegration?.config?.megaphoneActive, - notificationEmails: selectedIntegration?.metaData?.notificationEmails, - notificationWebhooks: selectedIntegration?.metaData?.notificationWebhooks, - }); - }, [selectedIntegration]); - - return ( - <> -
- - -

General

- { - if (!value || value.length === 0) { - return Promise.reject(new Error('Please add at least one email!')); - } - if (value.length > 20) { - return Promise.reject(new Error('Too many emails')); - } - if (!value.every((v) => isEmail(v))) { - return Promise.reject(new Error('One or more emails are invalid')); - } - return Promise.resolve(); - }, - }, - ]}> - { - setNotifications({ ...notifications, notificationEmails: e }); - }} - /> - - - - -
-
-
- - ); -}; -export default ASRForm; diff --git a/client-reactjs/src/components/admin/Integrations/IntegrationNotFound.jsx b/client-reactjs/src/components/admin/Integrations/IntegrationNotFound.jsx new file mode 100644 index 000000000..67a7c77cd --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/IntegrationNotFound.jsx @@ -0,0 +1,33 @@ +// Package Imports +import React from 'react'; +import { Result, Button } from 'antd'; +import { useHistory } from 'react-router-dom'; + +// Local Imports +import './integrations.css'; + +function IntegrationNotFound() { + const history = useHistory(); + + // Handle Go to Integration + const handleGoToIntegration = () => { + history.push('/admin/integrations'); + }; + + return ( +
+ + Go to Integrations + + } + /> +
+ ); +} + +export default IntegrationNotFound; diff --git a/client-reactjs/src/components/admin/Integrations/IntegrationSettings.jsx b/client-reactjs/src/components/admin/Integrations/IntegrationSettings.jsx new file mode 100644 index 000000000..38d857e16 --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/IntegrationSettings.jsx @@ -0,0 +1,45 @@ +// Package imports +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { useSelector } from 'react-redux'; + +// Local imports +import IntegrationNotFound from './IntegrationNotFound'; + +function IntegrationSettings() { + // Redux + const { + applicationReducer: { + integrations, + application: { applicationId }, + }, + } = useSelector((state) => state); + + // Integration name from URL + let { integrationName } = useParams(); + + // The integration name from url be present in the integrations list in redux store + const valid = integrations.some((i) => i.name === integrationName && i.application_id === applicationId); + + // If the integration name is not valid, show the IntegrationNotFound component + if (!valid) { + return ; + } else { + // Try importing the integration component with the name - integrationName + // If error occurs, show the IntegrationNotFound component + try { + // pass relation id as props + const relation_id = integrations.find( + (i) => i.name === integrationName && i.application_id === applicationId + ).integration_to_app_mapping_id; + + const IntegrationComponent = require(`./${integrationName.toLowerCase()}`).default; + + return ; + } catch (error) { + return ; + } + } +} + +export default IntegrationSettings; diff --git a/client-reactjs/src/components/admin/Integrations/Integrations.js b/client-reactjs/src/components/admin/Integrations/Integrations.js deleted file mode 100644 index 1e46e1269..000000000 --- a/client-reactjs/src/components/admin/Integrations/Integrations.js +++ /dev/null @@ -1,180 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Tooltip, Space, Table, Switch, Modal, Button, message } from 'antd'; -import { EditOutlined } from '@ant-design/icons'; -import BreadCrumbs from '../../common/BreadCrumbs.js'; -import { authHeader } from '../../common/AuthHeader.js'; -import { useSelector, useDispatch } from 'react-redux'; -import { applicationActions } from '../../../redux/actions/Application.js'; -import useWindowSize from '../../../hooks/useWindowSize.js'; -import ASRForm from './IntegrationForms/ASRForm.js'; - -const Integrations = () => { - const [integrations, setIntegrations] = useState([]); - const [modalVisible, setModalVisible] = useState(false); - const [modalWidth, setModalWidth] = useState(0); - const [confirmLoading, setConfirmLoading] = useState(false); - const [selectedIntegration, setSelectedIntegration] = useState({}); - const [notifications, setNotifications] = useState({}); - const [active, setActive] = useState(false); - - const windowSize = useWindowSize(); - - // Changes modal size per screen vw - useEffect(() => { - const { width } = windowSize.inner; - if (width > 1500) { - setModalWidth('40vw'); - } else if (width > 1000) { - integrations; - setModalWidth('60vw'); - } else { - setModalWidth('100vw'); - } - }, [windowSize]); - - const dispatch = useDispatch(); - const { - application: { applicationId }, - } = useSelector((state) => state.applicationReducer); - - useEffect(() => { - if (applicationId) getIntegrations(); - }, [applicationId]); - - const getIntegrations = async () => { - try { - const payload = { - method: 'GET', - header: authHeader(), - }; - const response = await fetch(`/api/integrations/get/${applicationId}`, payload); - - const data = await response.json(); - - if (data) { - setIntegrations(data); - } - dispatch(applicationActions.getIntegrations(applicationId)); - } catch (err) { - console.log(err); - } - }; - - const editIntegration = async (record) => { - await setModalVisible(true); - await setSelectedIntegration(record); - await setNotifications(record.metaData); - await setActive(record.config?.megaphoneActive); - }; - - const handleSave = async () => { - setConfirmLoading(true); - - const body = { notifications, active: { megaphoneActive: active } }; - - const payload = { - method: 'PUT', - header: authHeader(), - body: JSON.stringify(body), - }; - - const response = await fetch(`/api/integrations/update/${applicationId}/${selectedIntegration.name}`, payload); - - if (response.ok) { - getIntegrations(); - setConfirmLoading(false); - setModalVisible(false); - dispatch(applicationActions.getIntegrations(applicationId)); - - message.success('Successfully updated Integration'); - } else { - message.success('An Error Occured, Integration not updated'); - } - }; - - const saveBtn = ( - - ); - const cancelModal = async () => { - setModalVisible(false); - }; - - const toggleIntegration = async (name) => { - try { - const payload = { - method: 'PUT', - header: authHeader(), - }; - const response = await fetch(`/api/integrations/toggle/${applicationId}/${name}`, payload); - - if (response.ok) { - getIntegrations(); - } - dispatch(applicationActions.getIntegrations(applicationId)); - } catch (err) { - console.log(err); - } - }; - - const columns = [ - { title: 'Name', dataIndex: 'name' }, - { title: 'Description', dataIndex: 'description' }, - { - title: 'Activate', - dataIndex: 'activate', - render: (_, record) => ( - - - - toggleIntegration(record.name)} /> - - - - ), - }, - { - title: 'Edit', - dataIndex: 'edit', - render: (_, record) => ( - - - editIntegration(record)} /> - - - ), - }, - ]; - - return ( - <> - -
- record.id} /> - - {selectedIntegration?.name === 'ASR' && ( - <> - - - )} - - - ); -}; - -export default Integrations; diff --git a/client-reactjs/src/components/admin/Integrations/IntegrationsTable.jsx b/client-reactjs/src/components/admin/Integrations/IntegrationsTable.jsx new file mode 100644 index 000000000..d48c440dd --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/IntegrationsTable.jsx @@ -0,0 +1,84 @@ +//Package imports +import React from 'react'; +import { Table, Switch, Button, message } from 'antd'; +import { SettingOutlined } from '@ant-design/icons'; +import { useHistory } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; + +//Local Imports +import './integrations.css'; +import { toggleIntegration } from './integration-utils.js'; +import { applicationActions } from '../../../redux/actions/Application.js'; + +//JSX +function IntegrationsTable({ allIntegrations }) { + const history = useHistory(); + const dispatch = useDispatch(); + + // Get list of active integrations and current app ID from redux store + const { + applicationReducer: { + application: { applicationId }, + integrations, + }, + } = useSelector((state) => state); + + // Check if an integration is active + const isIntegrationActive = ({ integration_id, applicationId }) => { + return integrations.some((i) => i.integration_id === integration_id && i.application_id === applicationId); + }; + + // Handle integration active status change + const handleToggleIntegrationStatus = async ({ record, active, application_id }) => { + try { + await toggleIntegration({ integrationId: record.id, application_id, active }); + + // dispatch below actions so redux store gets fresh data + dispatch(applicationActions.getAllActiveIntegrations()); + } catch (error) { + message.error('Failed to toggle integration', error); + } + }; + + //Constants + const tableColumns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + width: '10%', + }, + { + title: 'Description', + dataIndex: 'description', + key: 'description', + }, + { + title: 'Actions', + key: 'actions', + width: '10%', + render: (record) => ( +
+ handleToggleIntegrationStatus({ record, application_id: applicationId, active })} + /> + {(() => { + return ( + isIntegrationActive({ integration_id: record.id, applicationId }) && ( + + ) + ); + })()} +
+ ), + }, + ]; + + return
row.id} />; +} + +export default IntegrationsTable; diff --git a/client-reactjs/src/components/admin/Integrations/asr/DomainModal.jsx b/client-reactjs/src/components/admin/Integrations/asr/DomainModal.jsx new file mode 100644 index 000000000..6f69ddef2 --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/asr/DomainModal.jsx @@ -0,0 +1,146 @@ +// Package imports +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { Modal, Form, Input, Select, message } from 'antd'; + +//Local Imports +import { createNewDomain, getDomains, updateDomain } from './asr-integration-util.js'; + +// Constants +const { Option } = Select; + +const DomainModal = ({ + domainModalOpen, + setDomainModalOpen, + monitoringTypes, + setDomains, + domains, + selectedDomain, + setSelectedDomain, +}) => { + const [form] = Form.useForm(); + + //Effects + useEffect(() => { + if (selectedDomain) { + let activityTypesIds = selectedDomain.activityTypes.map((d) => d.id); + activityTypesIds = activityTypesIds.filter((id) => id !== null); + form.setFieldsValue({ name: selectedDomain.name, monitoringTypeIds: activityTypesIds }); + } + }, [selectedDomain]); + + //Redux + const { + authenticationReducer: { + user: { firstName, lastName, email }, + }, + } = useSelector((state) => state); + + // Update Domain + const saveUpdatedDomain = async () => { + // Validate form + try { + await form.validateFields(); + } catch (err) { + return; + } + + // Save domain + try { + const payload = form.getFieldsValue(); + payload.updatedBy = { firstName, lastName, email }; + await updateDomain({ id: selectedDomain.id, payload }); + // Get all domains and replace the current domains with the new ones + const domains = await getDomains(); + setDomains(domains); + message.success('Domain updated successfully'); + setSelectedDomain(null); + form.resetFields(); + setDomainModalOpen(false); + } catch (err) { + message.error('Failed to update domain'); + } + }; + + // Create/save new domain + const saveDomain = async () => { + // Validate form + try { + await form.validateFields(); + } catch (err) { + return; + } + + //Check if name is already taken + const name = form.getFieldValue('name'); + const domainExists = domains.some((domain) => domain.name === name); + if (domainExists) { + message.error('Domain already exists'); + return; + } + + // Save domain + try { + const payload = form.getFieldsValue(); + payload.createdBy = { firstName, lastName, email }; + + await createNewDomain({ payload }); + // Get all domains and replace the current domains with the new ones + const domains = await getDomains(); + setDomains(domains); + message.success('Domain saved successfully'); + form.resetFields(); + setDomainModalOpen(false); + } catch (err) { + message.error('Failed to save domain'); + } + }; + + // const handle form submission + const handleFromSubmission = () => { + if (selectedDomain) { + saveUpdatedDomain(); + } else { + saveDomain(); + } + }; + + // When cancel button or close icon is clicked + const handleCancel = () => { + setSelectedDomain(null); + form.resetFields(); + setDomainModalOpen(false); + }; + + // JSX return + return ( + +
+ + + + + + + +
+ ); +}; + +export default DomainModal; diff --git a/client-reactjs/src/components/admin/Integrations/asr/DomainsTab.jsx b/client-reactjs/src/components/admin/Integrations/asr/DomainsTab.jsx new file mode 100644 index 000000000..ff2f3589a --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/asr/DomainsTab.jsx @@ -0,0 +1,113 @@ +// Package imports +import React, { useState, useEffect } from 'react'; +import { Table, Tag, Space, Popconfirm, message } from 'antd'; +import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; + +//Local Imports +import { deleteDomain } from './asr-integration-util'; + +const DomainsTab = ({ domains, setSelectedDomain, setDomainModalOpen }) => { + const [domainData, setDomainData] = useState([]); + + // Table columns + const columns = [ + { + title: 'Domain Name', + dataIndex: 'name', + width: '15%', + }, + { + title: 'Activity Type', + dataIndex: 'activityTypes', + render: (tags) => ( + <> + {tags.map( + (tag, i) => + tag.name && ( + + {tag.name} + + ) + )} + + ), + }, + { + title: 'Actions', + key: 'action', + render: (text, record) => ( + + handleEdit(record)} /> + handleDelete(record)} + okText="Yes" + okButtonProps={{ type: 'danger' }}> + + + + ), + width: '10%', + }, + ]; + + // Effect + useEffect(() => { + if (domains) { + const domainAndActivityTypes = []; + domains.forEach((d) => { + domainAndActivityTypes.push({ + name: d.name, + id: d.id, + activityType: { id: d['monitoringTypes.id'], name: d['monitoringTypes.name'] }, + }); + }); + + const organizedData = domainAndActivityTypes.reduce((acc, item) => { + // Find an existing entry for the current id + const existingEntry = acc.find((entry) => entry.id === item.id); + + if (existingEntry) { + // If an entry exists, add the current activityType to its activityTypes array + existingEntry.activityTypes.push(item.activityType); + } else { + // If no entry exists, create a new one with the current item's id, name, and activityType + acc.push({ + id: item.id, + name: item.name, + activityTypes: [item.activityType], + }); + } + + return acc; + }, []); + + setDomainData(organizedData); + } + }, [domains]); + + // When edit icon is clicked + const handleEdit = (record) => { + setSelectedDomain(record); + setDomainModalOpen(true); + }; + + // Handle when delete icon is clicked + const handleDelete = async (record) => { + try { + await deleteDomain({ id: record.id }); + message.success('Domain deleted successfully'); + setDomainData((prev) => prev.filter((d) => d.id !== record.id)); + } catch (err) { + message.error('Failed to delete domain'); + } + }; + + return ( + <> +
record.id} /> + + ); +}; + +export default DomainsTab; diff --git a/client-reactjs/src/components/admin/Integrations/asr/GeneralSettingsEditModal.jsx b/client-reactjs/src/components/admin/Integrations/asr/GeneralSettingsEditModal.jsx new file mode 100644 index 000000000..d165bd46b --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/asr/GeneralSettingsEditModal.jsx @@ -0,0 +1,211 @@ +// Package Imports +import React, { useEffect, useState } from 'react'; +import { Modal, Form, Checkbox, Select, Card, message } from 'antd'; +import { isEmail } from 'validator'; + +//Local Imports +const { updateIntegrationSettings } = require('../integration-utils.js'); + +function GeneralSettingsEditModal({ + displayGeneralSettingsEditModal, + setDisplayGeneralSettingsEditModal, + integrationDetails, + setIntegrationDetails, + teamsChannels, +}) { + //Local State + const [displayRecipients, setDisplayRecipients] = useState({ + severity3Alerts: false, + megaPhoneAlerts: false, + }); + + // Hooks + const [form] = Form.useForm(); + + // Effects + useEffect(() => { + const severity3AlertsActive = integrationDetails?.appSpecificIntegrationMetaData?.severity3Alerts?.active; + const megaPhoneAlertsActive = integrationDetails?.appSpecificIntegrationMetaData?.megaPhoneAlerts?.active; + const severity3EmailContacts = + integrationDetails?.appSpecificIntegrationMetaData?.severity3Alerts?.emailContacts || []; + const megaphoneEmailContacts = + integrationDetails?.appSpecificIntegrationMetaData?.megaPhoneAlerts?.emailContacts || []; + const megaPhoneTeamsContacts = + integrationDetails?.appSpecificIntegrationMetaData?.megaPhoneAlerts?.teamsChannel || []; + + // Set display recipients + setDisplayRecipients({ + severity3Alerts: severity3AlertsActive, + megaPhoneAlerts: megaPhoneAlertsActive, + }); + + // Populate form fields + form.setFieldsValue({ + severity3AlertsActive, + severity3EmailContacts, + megaPhoneAlertsActive, + megaphoneEmailContacts, + megaPhoneTeamsContacts, + }); + }, [integrationDetails]); + + // Handle Modal Close + const handleModalClose = () => { + setDisplayGeneralSettingsEditModal(false); + }; + + // Handle severity3AlertsActive checkbox change + const handleSeverity3AlertsActiveChange = (e) => { + setDisplayRecipients({ ...displayRecipients, severity3Alerts: e.target.checked }); + }; + + // Handle megaPhoneAlertsActive checkbox change + const handleMegaPhoneAlertsActiveChange = (e) => { + setDisplayRecipients({ ...displayRecipients, megaPhoneAlerts: e.target.checked }); + }; + + // Handle form submission + const handleFormSubmit = async () => { + // If error return + try { + await form.validateFields(); + } catch (err) { + return; + } + + // Construct payload + const payload = { + integrationSettings: { + severity3Alerts: { + active: form.getFieldValue('severity3AlertsActive'), + emailContacts: form.getFieldValue('severity3EmailContacts'), + }, + megaPhoneAlerts: { + active: form.getFieldValue('megaPhoneAlertsActive'), + emailContacts: form.getFieldValue('megaphoneEmailContacts'), + teamsChannel: form.getFieldValue('megaPhoneTeamsContacts'), + }, + }, + }; + + // Save the payload + try { + await updateIntegrationSettings({ + integrationMappingId: integrationDetails.integrationMappingId, + integrationSettings: payload, + }); + + // Update the integration details + setIntegrationDetails({ + ...integrationDetails, + appSpecificIntegrationMetaData: { + ...integrationDetails.appSpecificIntegrationMetaData, + ...payload.integrationSettings, + }, + }); + + handleModalClose(); + } catch (err) { + message.error('Unable to update the integration settings'); + } + }; + + // JSX + return ( + + +
+ + Activate Severity-3 alerts + + {displayRecipients.severity3Alerts && ( + { + if (!value || value.length === 0) { + return Promise.reject(new Error('Please add at least one email!')); + } + if (value.length > 20) { + return Promise.reject(new Error('Too many emails')); + } + if (!value.every((v) => isEmail(v))) { + return Promise.reject(new Error('One or more emails are invalid')); + } + return Promise.resolve(); + }, + }, + ]}> + + + + + + + )} + +
+
+ ); +} + +export default GeneralSettingsEditModal; diff --git a/client-reactjs/src/components/admin/Integrations/asr/GeneralTab.jsx b/client-reactjs/src/components/admin/Integrations/asr/GeneralTab.jsx new file mode 100644 index 000000000..eae8b63aa --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/asr/GeneralTab.jsx @@ -0,0 +1,110 @@ +// Package imports +import React, { useState, useEffect } from 'react'; +import { Space, Tag, Card } from 'antd'; +import { CheckSquareFilled, CloseSquareFilled } from '@ant-design/icons'; + +// Local imports +import '../integrations.css'; + +function GeneralTab({ integrationDetails, teamsChannels }) { + const [severity3AlertRecipients, setSeverity3AlertRecipients] = useState(null); + const [megaphoneAlertRecipients, setMegaphoneAlertRecipients] = useState(null); + + // Effects + useEffect(() => { + const severity3EmailContacts = + integrationDetails?.appSpecificIntegrationMetaData?.severity3Alerts?.emailContacts || []; + const megaphoneEmailContacts = + integrationDetails?.appSpecificIntegrationMetaData?.megaPhoneAlerts?.emailContacts || []; + const megaPhoneTeamsContacts = + integrationDetails?.appSpecificIntegrationMetaData?.megaPhoneAlerts?.teamsChannel || []; + + setSeverity3AlertRecipients({ emailContacts: severity3EmailContacts }); + setMegaphoneAlertRecipients({ emailContacts: megaphoneEmailContacts, teamsChannel: megaPhoneTeamsContacts }); + }, [integrationDetails]); + + // JSX for General Tab + return ( + <> +
+ {integrationDetails?.appSpecificIntegrationMetaData?.severity3Alerts?.active ? ( + <> + + + + Severity 3 alerts ACTIVE + + +
+ + {severity3AlertRecipients?.emailContacts.map((e, i) => { + return ( + + {e} + + ); + })} + +
+ + ) : ( + <> + + + + Severity 3 alerts INACTIVE + + + + )} +
+ +
+ {integrationDetails?.appSpecificIntegrationMetaData?.megaPhoneAlerts?.active ? ( + <> + + + + Megaphone alerts ACTIVE + + +
+ + {megaphoneAlertRecipients?.emailContacts.map((e, i) => { + return ( + + {e} + + ); + })} + {megaphoneAlertRecipients?.teamsChannel.map((t, i) => { + return teamsChannels.map((tc) => { + if (tc.id === t) { + return ( + + {tc.name} + + ); + } + return null; + }); + })} + +
+ + ) : ( + <> + + + + Megaphone alerts INACTIVE + + + + )} +
+ + ); +} + +export default GeneralTab; diff --git a/client-reactjs/src/components/admin/Integrations/asr/ProductModal.jsx b/client-reactjs/src/components/admin/Integrations/asr/ProductModal.jsx new file mode 100644 index 000000000..2f1ff4633 --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/asr/ProductModal.jsx @@ -0,0 +1,161 @@ +// Package imports +import React, { useEffect } from 'react'; +import { Modal, Form, Input, Select, message } from 'antd'; +import { useSelector } from 'react-redux'; + +// Local imports +import { createNewProduct, getProducts, updateProduct } from './asr-integration-util'; + +// Product tiers +const tiers = [ + { label: 'No Tier', value: 0 }, + { label: 'Tier 1', value: 1 }, + { label: 'Tier 2', value: 2 }, + { label: 'Tier 3', value: 3 }, +]; + +const ProductModal = ({ + productModalOpen, + setProductModalOpen, + domains, + selectedProduct, + setSelectedProduct, + setProducts, +}) => { + const [form] = Form.useForm(); + + // Redux + const { + authenticationReducer: { user }, + } = useSelector((state) => state); + + //Effects + useEffect(() => { + if (selectedProduct) { + let associatedDomains = []; + if (selectedProduct.domains) { + // Remove null domains + associatedDomains = selectedProduct.domains.filter((d) => d.id !== null).map((d) => d.id); + } + form.setFieldsValue({ + name: selectedProduct.name, + shortCode: selectedProduct.shortCode, + tier: selectedProduct.tier, + domainIds: associatedDomains, + }); + } + }, [selectedProduct]); + + // When Ok (Save/update) modal on from is clicked + const handleOk = async () => { + // Validate form + try { + await form.validateFields(); + } catch (error) { + console.error('Failed to validate form', error); + } + + // Create new product + try { + const formValues = form.getFieldsValue(); + if (!selectedProduct) { + await saveNewProduct(formValues); + } else { + await updateExistingProduct(formValues); + } + + const products = await getProducts(); + setProducts(products); + form.resetFields(); + setSelectedProduct(null); + setProductModalOpen(false); + } catch (err) { + message.error('Failed to create product', err.message); + } + }; + + // Create new product + const saveNewProduct = async (values) => { + try { + const payload = { + ...values, + createdBy: { lastName: user.lastName, firstName: user.firstName, email: user.email }, + }; + await createNewProduct({ payload }); + message.success('Product created successfully'); + } catch (error) { + throw new Error('Failed to create product'); + } + }; + + // Const update product + const updateExistingProduct = async (values) => { + try { + const payload = { + ...values, + updatedBy: { lastName: user.lastName, firstName: user.firstName, email: user.email }, + }; + await updateProduct({ id: selectedProduct.id, payload }); + message.success('Product updated successfully'); + } catch (error) { + throw new Error('Failed to update product'); + } + }; + + // When cancel is clicked + const handleCancel = () => { + form.resetFields(); + setSelectedProduct(null); + setProductModalOpen(false); + }; + + // JSX + return ( + +
+ + + + + + + + + + + + + +
+ ); +}; + +export default ProductModal; diff --git a/client-reactjs/src/components/admin/Integrations/asr/ProductsTab.jsx b/client-reactjs/src/components/admin/Integrations/asr/ProductsTab.jsx new file mode 100644 index 000000000..f41cd49e3 --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/asr/ProductsTab.jsx @@ -0,0 +1,135 @@ +// Package imorts +import React, { useState, useEffect } from 'react'; +import { Table, Space, Popconfirm, Tag, message } from 'antd'; +import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; + +// Local imports +import { getProducts, deleteProduct } from './asr-integration-util'; + +const ProductsTab = ({ products, setSelectedProduct, setProductModalOpen, setProducts }) => { + // Local State + const [productData, setProductData] = useState([]); + + // Effect + useEffect(() => { + if (products) { + const productAndDomain = []; + products.forEach((p) => { + productAndDomain.push({ + name: p.name, + id: p.id, + tier: p.tier, + shortCode: p.shortCode, + domain: { id: p['associatedDomains.id'], name: p['associatedDomains.name'] }, + }); + }); + + const organizedData = productAndDomain.reduce((acc, item) => { + // Find an existing entry for the current id + const existingEntry = acc.find((entry) => entry.id === item.id); + + if (existingEntry) { + // If an entry exists, add the current activityType to its activityTypes array + existingEntry.domains.push(item.domain); + } else { + // If no entry exists, create a new one with the current item's id, name, and activityType + acc.push({ + id: item.id, + name: item.name, + tier: item.tier, + shortCode: item.shortCode, + domains: [item.domain], + }); + } + + return acc; + }, []); + + setProductData(organizedData); + } + }, [products]); + + // Product table columns + const columns = [ + { + title: 'Product Name', + dataIndex: 'name', + key: 'name', + width: '15%', + }, + { + title: 'Short code', + dataIndex: 'shortCode', + key: 'shortCode', + width: '15%', + }, + { + title: 'Tier', + dataIndex: 'tier', + key: 'tier', + width: '15%', + render: (tier) => { + return tier === 0 ? 'No Tier' : `Tier - ${tier}`; + }, + }, + { + title: 'Domains', + dataIndex: 'domains', + key: 'domains', + render: (tags) => ( + <> + {tags.map( + (tag, i) => + tag.name && ( + + {tag.name} + + ) + )} + + ), + }, + { + title: 'Action', + key: 'action', + render: (text, record) => ( + + handleEdit(record)} /> + handleDelete(record)} + okButtonProps={{ type: 'danger' }}> + + + + ), + width: '10%', + }, + ]; + + // When edit icon is clicked + const handleEdit = (record) => { + setSelectedProduct(record); + setProductModalOpen(true); + }; + + // Handle product deletion + const handleDelete = async (record) => { + try { + await deleteProduct({ id: record.id }); + const updatedProducts = await getProducts(); + setProducts(updatedProducts); + } catch (err) { + message.error('Failed to delete product'); + } + }; + + // JSX + return ( + <> +
record.id} /> + + ); +}; + +export default ProductsTab; diff --git a/client-reactjs/src/components/admin/Integrations/asr/asr-integration-util.js b/client-reactjs/src/components/admin/Integrations/asr/asr-integration-util.js new file mode 100644 index 000000000..2415601ee --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/asr/asr-integration-util.js @@ -0,0 +1,193 @@ +// Local Imports +import { authHeader } from '../../../common/AuthHeader.js'; + +// Get all monitorings +export const getMonitoringTypes = async () => { + const payload = { + method: 'GET', + headers: authHeader(), + }; + const response = await fetch('/api/monitorings', payload); + + if (response.status !== 200) { + throw new Error('Failed to get monitorings'); + } + + const data = await response.json(); + + return data; +}; + +// Add new domain +export const createNewDomain = async ({ payload }) => { + console.log(payload); + const payloadOptions = { + method: 'POST', + headers: { + ...authHeader(), + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }; + + const response = await fetch('/api/asr/domains', payloadOptions); + + if (response.status !== 200) { + throw new Error('Failed to add domain'); + } + + const data = await response.json(); + + return data; +}; + +// Get all domains and the associated monitoring types +export const getDomains = async () => { + const payload = { + method: 'GET', + headers: authHeader(), + }; + const response = await fetch('/api/asr/domains', payload); + + if (response.status !== 200) { + throw new Error('Failed to get domains'); + } + + const data = await response.json(); + + return data; +}; + +// Update domain and the associated relation to monitoring types +export const updateDomain = async ({ id, payload }) => { + const payloadOptions = { + method: 'PATCH', + headers: { + ...authHeader(), + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }; + + const response = await fetch(`/api/asr/domains/${id}`, payloadOptions); + + if (response.status !== 200) { + throw new Error('Failed to update domain'); + } + + const data = await response.json(); + + return data; +}; + +// Delete domain +export const deleteDomain = async ({ id }) => { + const payload = { + method: 'DELETE', + headers: authHeader(), + }; + const response = await fetch(`/api/asr/domains/${id}`, payload); + + if (response.status !== 200) { + throw new Error('Failed to delete domain'); + } + + const data = await response.json(); + + return data; +}; + +// Get all products +export const getProducts = async () => { + const payload = { + method: 'GET', + headers: authHeader(), + }; + const response = await fetch('/api/asr/products', payload); + + if (response.status !== 200) { + throw new Error('Failed to get products'); + } + + const data = await response.json(); + + return data; +}; + +// Create new product +export const createNewProduct = async ({ payload }) => { + const payloadOptions = { + method: 'POST', + headers: { + ...authHeader(), + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }; + + const response = await fetch('/api/asr/products', payloadOptions); + + if (response.status !== 200) { + throw new Error('Failed to add product'); + } + + const data = await response.json(); + + return data; +}; + +// Update product +export const updateProduct = async ({ id, payload }) => { + const payloadOptions = { + method: 'PUT', + headers: { + ...authHeader(), + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }; + + const response = await fetch(`/api/asr/products/${id}`, payloadOptions); + + if (response.status !== 200) { + throw new Error('Failed to update product'); + } + + const data = await response.json(); + + return data; +}; + +// Delete products +export const deleteProduct = async ({ id }) => { + const payload = { + method: 'DELETE', + headers: authHeader(), + }; + const response = await fetch(`/api/asr/products/${id}`, payload); + + if (response.status !== 200) { + throw new Error('Failed to delete product'); + } + + const data = await response.json(); + + return data; +}; + +// Get all teams channels +export const getTeamsChannels = async () => { + const payload = { + method: 'GET', + headers: authHeader(), + }; + const response = await fetch('/api/teamsHook/', payload); + + if (response.status !== 200) { + throw new Error('Failed to get teams channels'); + } + + const data = await response.json(); + + return data; +}; diff --git a/client-reactjs/src/components/admin/Integrations/asr/index.js b/client-reactjs/src/components/admin/Integrations/asr/index.js new file mode 100644 index 000000000..3ff8af48e --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/asr/index.js @@ -0,0 +1,213 @@ +// Package imports +import React, { useState, useEffect } from 'react'; +import { Tabs, Card, Button, Dropdown, Space, message } from 'antd'; +import { DownOutlined } from '@ant-design/icons'; + +// Local Imports +import BreadCrumbs from '../../../common/BreadCrumbs.js'; +import GeneralTab from './GeneralTab.jsx'; +import DomainsTab from './DomainsTab.jsx'; +import ProductsTab from './ProductsTab.jsx'; +import GeneralSettingsEditModal from './GeneralSettingsEditModal.jsx'; +import { getIntegrationDetailsByRelationId } from '../integration-utils.js'; +import { getMonitoringTypes, getDomains, getProducts, getTeamsChannels } from './asr-integration-util.js'; +import DomainModal from './DomainModal.jsx'; +import ProductModal from './ProductModal.jsx'; + +// Constants +const { TabPane } = Tabs; + +function AsrIntegrationSettings({ integration_to_app_mapping_id }) { + // Local states + const [activeTab, setActiveTab] = useState('1'); + const [tabExtraContent, setTabExtraContent] = useState(null); + const [displayGeneralSettingsEditModal, setDisplayGeneralSettingsEditModal] = useState(false); + const [integrationDetails, setIntegrationDetails] = useState(null); + const [domainModalOpen, setDomainModalOpen] = useState(false); + const [productModalOpen, setProductModalOpen] = useState(false); + const [monitoringTypes, setMonitoringTypes] = useState([]); + const [domains, setDomains] = useState([]); + const [selectedDomain, setSelectedDomain] = useState(null); + const [products, setProducts] = useState([]); + const [selectedProduct, setSelectedProduct] = useState(null); + const [teamsChannels, setTeamsChannels] = useState([]); + + // Action items for the domains tab + const domainActionItems = [ + { + label: 'Add Domain', + onClick: () => setDomainModalOpen(true), + key: '1', + }, + ]; + + // Action items for the products tab + const productTabActions = [ + { + label: 'Add Product', + onClick: () => setProductModalOpen(true), + key: '1', + }, + ]; + + // Get integration Details for the selected integration + useEffect(() => { + //Get integration details + (async () => { + try { + const integrationDetails = await getIntegrationDetailsByRelationId({ + relationId: integration_to_app_mapping_id, + }); + setIntegrationDetails(integrationDetails); + } catch (err) { + message.error('Unable to get integration details'); + } + })(); + + // Get all monitoring types + (async () => { + try { + const monitoringTypes = await getMonitoringTypes(); + setMonitoringTypes(monitoringTypes); + } catch (err) { + return; + } + })(); + }, [integration_to_app_mapping_id]); + + // Get all domains and products - only once when the component mounts + useEffect(() => { + // Get domains + (async () => { + try { + const domains = await getDomains(); + setDomains(domains); + } catch (err) { + return; + } + })(); + + //Get products + (async () => { + try { + const products = await getProducts(); + setProducts(products); + } catch (err) { + return; + } + })(); + + // Get teams channels + (async () => { + try { + const response = await getTeamsChannels(); + setTeamsChannels(response); + } catch (err) { + message.error('Failed to get teams channels'); + } + })(); + }, []); + + //Handle Tab change + const handleTabChange = (value) => { + setActiveTab(value); + }; + + // Render tab bar extra content based on the active tab + useEffect(() => { + if (activeTab === '1') { + setTabExtraContent( + + ); + } + + if (activeTab === '2') { + setTabExtraContent( + + + + ); + } + + if (activeTab === '3') { + setTabExtraContent( + + + + ); + } + }, [activeTab]); + + //JSX return + return ( + <> + + + handleTabChange(value)}> + + {} + + + + + + + + + + + + + + ); +} + +export default AsrIntegrationSettings; diff --git a/client-reactjs/src/components/admin/Integrations/index.js b/client-reactjs/src/components/admin/Integrations/index.js new file mode 100644 index 000000000..445ebbbb4 --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/index.js @@ -0,0 +1,34 @@ +// Package imports +import React, { useState, useEffect } from 'react'; +const { message } = require('antd'); + +//Local Imports +import IntegrationsTable from './IntegrationsTable.jsx'; +import BreadCrumbs from '../../common/BreadCrumbs.js'; +import { getAllIntegrations } from './integration-utils.js'; + +function Integrations() { + //Local States + const [allIntegrations, setAllIntegrations] = useState([]); + + // Get all Integrations + useEffect(() => { + (async () => { + try { + const integrations = await getAllIntegrations(); + setAllIntegrations(integrations); + } catch (error) { + message.error('Failed to get integrations', error); + } + })(); + }, []); + + return ( + <> + + + + ); +} + +export default Integrations; diff --git a/client-reactjs/src/components/admin/Integrations/integration-utils.js b/client-reactjs/src/components/admin/Integrations/integration-utils.js new file mode 100644 index 000000000..a4af872d3 --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/integration-utils.js @@ -0,0 +1,76 @@ +// Local imports +import { authHeader } from '../../common/AuthHeader.js'; + +// Get all integrations +export const getAllIntegrations = async () => { + const payload = { + method: 'GET', + header: authHeader(), + }; + const response = await fetch('/api/integrations/all', payload); + + if (response.status !== 200) { + throw new Error('Failed to get integrations'); + } + + const data = await response.json(); + + return data; +}; + +// Get Integration By integration to app mapping id +export const getIntegrationDetailsByRelationId = async ({ relationId }) => { + const payload = { + method: 'GET', + header: authHeader(), + }; + const response = await fetch(`/api/integrations/integrationDetails/${relationId}`, payload); + + if (response.status !== 200) { + throw new Error('Failed to get integration'); + } + + const responseRawData = await response.json(); + + // Flatten the object + const data = { ...responseRawData, ...responseRawData.integration }; + delete data.integration; + + return data; +}; + +// Activate or deactivate integration +export const toggleIntegration = async ({ integrationId, application_id, active }) => { + const payload = { + method: 'POST', + header: authHeader(), + body: JSON.stringify({ integrationId, application_id, active }), + }; + const response = await fetch(`/api/integrations/toggleStatus`, payload); + + if (response.status !== 200) { + throw new Error('Failed to toggle integration'); + } + + const data = await response.json(); + + return data; +}; + +// Update integration settings/configuration +export const updateIntegrationSettings = async ({ integrationMappingId, integrationSettings }) => { + const requestOptions = { + method: 'PUT', + headers: authHeader(), + body: JSON.stringify(integrationSettings), + }; + const response = await fetch(`/api/integrations/updateIntegrationSettings/${integrationMappingId}`, requestOptions); + + if (response.status !== 200) { + throw new Error('Failed to update integration settings'); + } + + const data = await response.json(); + + return data; +}; diff --git a/client-reactjs/src/components/admin/Integrations/integrations.css b/client-reactjs/src/components/admin/Integrations/integrations.css new file mode 100644 index 000000000..5b962ff5a --- /dev/null +++ b/client-reactjs/src/components/admin/Integrations/integrations.css @@ -0,0 +1,28 @@ +.integrationTable__actionIcons { + display: flex; + place-items: center; +} +.integrationTable__actions-settings-icon-active { + cursor: pointer; +} + +.integrationTable__actions-settings-icon-inactive { + color: #9e9e9e; + cursor: not-allowed; +} + +/* -------------------------------------- */ + +.integrationSettings__unavailable { + display: flex; + justify-content: center; + place-items: center; + height: 90vh; +} + +/* -------------------------------------- */ + +.integrations__shifted-form-item { + margin-left: 25px; + margin-top: -5px; +} diff --git a/client-reactjs/src/components/admin/apps/Applications.js b/client-reactjs/src/components/admin/apps/Applications.js index b826e7d52..433779a81 100644 --- a/client-reactjs/src/components/admin/apps/Applications.js +++ b/client-reactjs/src/components/admin/apps/Applications.js @@ -249,7 +249,7 @@ class Applications extends Component { (record.creator !== this.props.username && record.visibility !== 'Public') ? ( <> + '?'} + title={} onConfirm={() => this.handleRemove(record.id)} icon={}> diff --git a/client-reactjs/src/components/common/BreadCrumbs.js b/client-reactjs/src/components/common/BreadCrumbs.js index e936ac24d..20c8bbc7f 100644 --- a/client-reactjs/src/components/common/BreadCrumbs.js +++ b/client-reactjs/src/components/common/BreadCrumbs.js @@ -53,7 +53,11 @@ class BreadCrumbs extends Component { } }); - return newBreadCrumbItems; + const alteredCrumbs = newBreadCrumbItems.map((item) => { + return { ...item, title: item.title[0].charAt(0).toUpperCase() + item.title.slice(1) }; + }); + + return alteredCrumbs; }; return ( diff --git a/client-reactjs/src/components/layout/Header.js b/client-reactjs/src/components/layout/Header.js index 7ebc0d4a1..a4213e572 100644 --- a/client-reactjs/src/components/layout/Header.js +++ b/client-reactjs/src/components/layout/Header.js @@ -61,25 +61,25 @@ class AppHeader extends Component { }, 100); // eslint-disable-next-line react/no-deprecated - // componentWillReceiveProps(props) { - // if (props.application && props.application.applicationTitle !== '') { - // this.setState((prev) => { - // const currentAppId = props.application.applicationId; - // if (currentAppId) { - // let appList = prev.applications; - // // when user adds new app he will change state in redux by selecting that app, we will check if that app exists in our list, is now we will add it. - // const app = appList.find((app) => app.value === currentAppId); - // if (!app) - // appList = [ - // ...appList, - // { value: props.application.applicationId, display: props.application.applicationTitle }, - // ]; - // return { selected: props.application.applicationTitle, applications: appList }; - // } - // return { selected: props.application.applicationTitle }; - // }); - // } - // } + componentWillReceiveProps(props) { + if (props.application && props.application.applicationTitle !== '') { + this.setState((prev) => { + const currentAppId = props.application.applicationId; + if (currentAppId) { + let appList = prev.applications; + // when user adds new app he will change state in redux by selecting that app, we will check if that app exists in our list, is now we will add it. + const app = appList.find((app) => app.value === currentAppId); + if (!app) + appList = [ + ...appList, + { value: props.application.applicationId, display: props.application.applicationTitle }, + ]; + return { selected: props.application.applicationTitle, applications: appList }; + } + return { selected: props.application.applicationTitle }; + }); + } + } componentDidMount() { if (this.props.location.pathname.includes('manualJobDetails')) { return; @@ -141,7 +141,8 @@ class AppHeader extends Component { this.props.dispatch(applicationActions.getConsumers()); this.props.dispatch(applicationActions.getLicenses()); this.props.dispatch(applicationActions.getConstraints()); - this.props.dispatch(applicationActions.getIntegrations(this.props.application?.applicationId)); + // this.props.dispatch(applicationActions.getIntegrations(this.props.application.applicationId)); + this.props.dispatch(applicationActions.getAllActiveIntegrations()); } if (this.props.newApplication) { diff --git a/client-reactjs/src/components/layout/LeftNav.js b/client-reactjs/src/components/layout/LeftNav.js index 26c9bf78a..5a7acaa76 100644 --- a/client-reactjs/src/components/layout/LeftNav.js +++ b/client-reactjs/src/components/layout/LeftNav.js @@ -68,7 +68,7 @@ class LeftNav extends Component { const applicationId = this.props?.applicationId || ''; const integrations = this.props?.integrations || []; - const asrActive = integrations.find((i) => i.name === 'ASR')?.active; + const asrActive = integrations.some((i) => i.name === 'ASR' && i.application_id === applicationId); if (!this.props.loggedIn || !this.props.user || Object.getOwnPropertyNames(this.props.user).length == 0) { return null; diff --git a/client-reactjs/src/index.css b/client-reactjs/src/index.css index 591d128bf..905352a02 100644 --- a/client-reactjs/src/index.css +++ b/client-reactjs/src/index.css @@ -3,6 +3,7 @@ /* some colors from bootstrap */ --primary: #007bff; --secondary: #6c757d; + --secondary-light: #d3d3d3; --success: #28a745; --info: #17a2b8; --warning: #ffc107; diff --git a/client-reactjs/src/redux/actions/Application.js b/client-reactjs/src/redux/actions/Application.js index eaccbfd9b..b8c839263 100644 --- a/client-reactjs/src/redux/actions/Application.js +++ b/client-reactjs/src/redux/actions/Application.js @@ -11,8 +11,9 @@ export const applicationActions = { getLicenses, getConstraints, updateConstraints, - getIntegrations, - updateIntegrations, + // getIntegrations, + // updateIntegrations, + getAllActiveIntegrations, }; function applicationSelected(applicationId, applicationTitle) { @@ -83,15 +84,45 @@ function updateConstraints(constraints) { return { type: Constants.UPDATE_CONSTRAINTS, constraints }; } -function getIntegrations(applicationId) { - return (dispatch) => { - fetch(`/api/integrations/get/${applicationId}`, { headers: authHeader() }) - .then((response) => (response.ok ? response.json() : handleError(response))) - .then((integrations) => dispatch({ type: Constants.INTEGRATIONS_RETRIEVED, integrations })) - .catch(console.log); +// function getIntegrations(applicationId) { +// return (dispatch) => { +// fetch(`/api/integrations/get/${applicationId}`, { headers: authHeader() }) +// .then((response) => (response.ok ? response.json() : handleError(response))) +// .then((integrations) => dispatch({ type: Constants.INTEGRATIONS_RETRIEVED, integrations })) +// .catch(console.log); +// }; +// } + +// Get all active Integrations aka integrations that are in the integrations to application mapping table +function getAllActiveIntegrations() { + return async (dispatch) => { + try { + const response = await fetch('/api/integrations/getAllActive', { headers: authHeader() }); + + if (!response.ok) { + throw handleError(response); + } + + const data = await response.json(); + const integrations = []; + if (data.length > 0) { + data.forEach((d) => { + integrations.push({ + name: d.integration.name, + integration_id: d.integration_id, + application_id: d.application_id, + integration_to_app_mapping_id: d.id, + }); + }); + } + + dispatch({ type: Constants.INTEGRATIONS_RETRIEVED, integrations }); + } catch (error) { + console.log(error); + } }; } -function updateIntegrations(integrations) { - return { type: Constants.UPDATE_INTEGRATIONS, integrations }; -} +// function updateIntegrations(integrations) { +// return { type: Constants.UPDATE_INTEGRATIONS, integrations }; +// } diff --git a/server/config/config.js b/server/config/config.js index b3779d9e4..ec4b56fae 100644 --- a/server/config/config.js +++ b/server/config/config.js @@ -7,14 +7,14 @@ require('dotenv').config({ path: ENVPath}); const logger = require('./logger'); const dbConfigOptions = { - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - host: process.env.DB_HOSTNAME, - dialect: "mysql", - seedStorage: "sequelize", - logging: (msg) => logger.debug(msg), // change winston settings to 'debug' to see this log -}; + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + host: process.env.DB_HOSTNAME, + dialect: 'mysql', + seederStorage: 'json', + logging: (msg) => logger.debug(msg), // change winston settings to 'debug' to see this log +} if(process.env.MYSQL_SSL_ENABLED === "true"){ dbConfigOptions.ssl = true; diff --git a/server/job-scheduler.js b/server/job-scheduler.js index 3851a85cf..3369be694 100644 --- a/server/job-scheduler.js +++ b/server/job-scheduler.js @@ -51,10 +51,6 @@ const { scheduleOrbitMonitoringOnServerStart, } = require("./jobSchedularMethods/orbitJobs.js"); -//import job directly to run it only once on server start -//remove this -// const { createIntegrations } = require("./jobs/integrationCreation.js"); - class JobScheduler { constructor() { this.bree = new Bree({ @@ -121,15 +117,11 @@ class JobScheduler { await this.scheduleSuperFileMonitoringOnServerStart(); await this.scheduleClusterMonitoringOnServerStart(); await this.scheduleKeyCheck(); - // await this.scheduleJobMonitoringOnServerStart(); await this.createClusterUsageHistoryJob(); await this.scheduleEmailNotificationProcessing(); await this.scheduleTeamsNotificationProcessing(); await this.scheduleOrbitMonitoringOnServerStart(); await this.createOrbitMegaphoneJob(); - - //one off jobs on server start - // await this.createIntegrations(); })(); } @@ -345,10 +337,6 @@ class JobScheduler { createOrbitMonitoringJob({ orbitMonitoring_id, cron }) { return createOrbitMonitoringJob.call(this, { orbitMonitoring_id, cron }); } - - // createIntegrations() { - // return createIntegrations.call(this); - // } } module.exports = new JobScheduler(); diff --git a/server/migrations/20240229155215-create-monitoring_types-table.js b/server/migrations/20240229155215-create-monitoring_types-table.js new file mode 100644 index 000000000..1788a5e64 --- /dev/null +++ b/server/migrations/20240229155215-create-monitoring_types-table.js @@ -0,0 +1,49 @@ +"use strict"; +const { DataTypes } = require("sequelize"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("monitoring_types", { + id: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + }, + name: { + allowNull: false, + unique: true, + type: DataTypes.STRING, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"), + }, + updatedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + deletedAt: { + type: DataTypes.DATE, + defaultValue: null, + }, + createdBy: { + allowNull: false, + type: DataTypes.JSON, + defaultValue: { email: "NA", lastName: "NA", firstName: "System" }, + }, + updatedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + deletedBy: { + type: DataTypes.JSON, + defaultValue: null, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("monitoring_types"); + }, +}; diff --git a/server/migrations/20240229155642-create-asr_domains-table.js b/server/migrations/20240229155642-create-asr_domains-table.js new file mode 100644 index 000000000..25f2642a6 --- /dev/null +++ b/server/migrations/20240229155642-create-asr_domains-table.js @@ -0,0 +1,50 @@ +"use strict"; +const { DataTypes } = require("sequelize"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("asr_domains", { + id: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + }, + name: { + allowNull: false, + unique: true, + type: DataTypes.STRING, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"), + }, + updatedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + deletedAt: { + allowNull: true, + type: DataTypes.DATE, + defaultValue: null, + }, + createdBy: { + allowNull: false, + type: DataTypes.JSON, + defaultValue: { email: "NA", lastName: "System", firstName: "NA" }, + }, + updatedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + deletedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("asr_domains"); + }, +}; \ No newline at end of file diff --git a/server/migrations/20240229160314-create-asr_products_table.js b/server/migrations/20240229160314-create-asr_products_table.js new file mode 100644 index 000000000..be735bb71 --- /dev/null +++ b/server/migrations/20240229160314-create-asr_products_table.js @@ -0,0 +1,58 @@ +"use strict"; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("asr_products", { + id: { + allowNull: false, + primaryKey: true, + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + }, + name: { + allowNull: false, + unique: true, + type: Sequelize.STRING, + }, + shortCode: { + allowNull: false, + unique: true, + type: Sequelize.STRING, + }, + tier: { + type: Sequelize.INTEGER, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW, + }, + updatedAt: { + allowNull: true, + type: Sequelize.DATE, + }, + deletedAt: { + allowNull: true, + type: Sequelize.DATE, + defaultValue: null, + }, + createdBy: { + allowNull: false, + type: Sequelize.JSON, + defaultValue: { email: "NA", lastName: "System", firstName: "NA" }, + }, + updatedBy: { + allowNull: true, + type: Sequelize.JSON, + }, + deletedBy: { + allowNull: true, + type: Sequelize.JSON, + defaultValue: null, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("asr_products"); + }, +}; diff --git a/server/migrations/20240229160921-create-asr_monitoring_type_to_domains_relation-table.js b/server/migrations/20240229160921-create-asr_monitoring_type_to_domains_relation-table.js new file mode 100644 index 000000000..06a352dbe --- /dev/null +++ b/server/migrations/20240229160921-create-asr_monitoring_type_to_domains_relation-table.js @@ -0,0 +1,69 @@ +"use strict"; +const { DataTypes } = require("sequelize"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("asr_monitoring_type_to_domains", { + id: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + }, + monitoring_type_id: { + type: DataTypes.UUID, + references: { + model: "monitoring_types", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + domain_id: { + type: DataTypes.UUID, + references: { + model: "asr_domains", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"), + }, + updatedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + deletedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + createdBy: { + allowNull: false, + type: DataTypes.JSON, + defaultValue: { email: "NA", lastName: "NA", firstName: "System" }, + }, + updatedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + deletedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + }); + + // Create constraints - monitoring_type_id, domain_id pair should be unique + await queryInterface.addConstraint("asr_monitoring_type_to_domains", { + type: "unique", + fields: ["monitoring_type_id", "domain_id"], + name: "unique_monitoring_type_id_domain_id", + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("asr_monitoring_type_to_domains"); + }, +}; diff --git a/server/migrations/20240229161547-create-asr_domain_to_products_relation-table.js b/server/migrations/20240229161547-create-asr_domain_to_products_relation-table.js new file mode 100644 index 000000000..d460c5a29 --- /dev/null +++ b/server/migrations/20240229161547-create-asr_domain_to_products_relation-table.js @@ -0,0 +1,69 @@ +"use strict"; +const { DataTypes } = require("sequelize"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("asr_domain_to_products", { + id: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + }, + domain_id: { + type: DataTypes.UUID, + references: { + model: "asr_domains", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + product_id: { + type: DataTypes.UUID, + references: { + model: "asr_products", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"), + }, + updatedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + deletedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + createdBy: { + allowNull: false, + type: DataTypes.JSON, + defaultValue: { name: "system", email: "NA" }, + }, + updatedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + deletedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + }); + + //Create constraint - domain_id, product_id pair should be unique + await queryInterface.addConstraint("asr_domain_to_products", { + fields: ["domain_id", "product_id"], + type: "unique", + name: "unique_domain_product_pair", + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("asr_domain_to_products"); + }, +}; diff --git a/server/migrations/20240301203455-create_integration_application_mapping.js b/server/migrations/20240301203455-create_integration_application_mapping.js index 633e53d57..04216d8a2 100644 --- a/server/migrations/20240301203455-create_integration_application_mapping.js +++ b/server/migrations/20240301203455-create_integration_application_mapping.js @@ -40,6 +40,10 @@ module.exports = { allowNull: false, defaultValue: Sequelize.NOW, }, + deletedAt: { + type: Sequelize.DATE, + allowNull: true, + }, }); }, diff --git a/server/models/asr_domain_to_products.js b/server/models/asr_domain_to_products.js new file mode 100644 index 000000000..3db519edf --- /dev/null +++ b/server/models/asr_domain_to_products.js @@ -0,0 +1,64 @@ +"use strict"; +module.exports = (sequelize, DataTypes) => { + const AsrDomainToProductsRelation = sequelize.define( + "asr_domain_to_products", + { + id: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + }, + domain_id: { + type: DataTypes.UUID, + }, + product_id: { + type: DataTypes.UUID, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + deletedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + createdBy: { + allowNull: false, + type: DataTypes.JSON, + defaultValue: {email: 'NA', lastName: "NA", firstName: 'System'} + }, + updatedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + deletedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + }, + { + freezeTableName: true, + } + ); + + AsrDomainToProductsRelation.associate = function (models) { + AsrDomainToProductsRelation.belongsTo(models.asr_domains, { + foreignKey: "domain_id", + onDelete: "CASCADE", + onUpdate: "CASCADE", + }); + AsrDomainToProductsRelation.belongsTo(models.asr_products, { + foreignKey: "product_id", + onDelete: "CASCADE", + onUpdate: "CASCADE", + }); + }; + + return AsrDomainToProductsRelation; +}; diff --git a/server/models/asr_domains.js b/server/models/asr_domains.js new file mode 100644 index 000000000..26ffa385f --- /dev/null +++ b/server/models/asr_domains.js @@ -0,0 +1,64 @@ +"use strict"; +module.exports = (sequelize, DataTypes) => { + const AsrDomains = sequelize.define( + "asr_domains", + { + id: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + }, + name: { + allowNull: false, + unique: true, + type: DataTypes.STRING, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + deletedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + createdBy: { + allowNull: false, + type: DataTypes.JSON, + defaultValue: { firstName: null, lastName: "System", email: "NA" }, + }, + updatedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + deletedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + }, + { + freezeTableName: true, + } + ); + + AsrDomains.associate = function (models) { + AsrDomains.belongsToMany(models.monitoring_types, { + through: "asr_monitoring_type_to_domains", + foreignKey: "domain_id", + as: "monitoringTypes", + }); + + AsrDomains.belongsToMany(models.asr_products, { + through: "asr_domain_to_products", + foreignKey: "domain_id", + as: "associatedProducts", + }); + } + + return AsrDomains; +}; diff --git a/server/models/asr_monitoring_type_to_domains.js b/server/models/asr_monitoring_type_to_domains.js new file mode 100644 index 000000000..1cca05809 --- /dev/null +++ b/server/models/asr_monitoring_type_to_domains.js @@ -0,0 +1,72 @@ +"use strict"; +module.exports = (sequelize, DataTypes) => { + const AsrMonitoringTypeToDomainsRelation = sequelize.define( + "asr_monitoring_type_to_domains", + { + id: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + }, + monitoring_type_id: { + type: DataTypes.UUID, + references: { + model: "monitoring_types", + key: "id", + }, + }, + domain_id: { + type: DataTypes.UUID, + references: { + model: "asr_domains", + key: "id", + }, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + deletedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + createdBy: { + allowNull: false, + type: DataTypes.JSON, + defaultValue: { name: "system", email: "NA" }, + }, + updatedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + deletedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + }, + { + freezeTableName: true, + } + ); + + // AsrMonitoringTypeToDomainsRelation.associate = function (models) { + // AsrMonitoringTypeToDomainsRelation.belongsTo(models.monitoring_types, { + // foreignKey: "monitoring_type_id", + // onDelete: "CASCADE", + // onUpdate: "CASCADE", + // }); + // AsrMonitoringTypeToDomainsRelation.belongsTo(models.asr_domains, { + // foreignKey: "domain_id", + // onDelete: "CASCADE", + // onUpdate: "CASCADE", + // }); + // }; + + return AsrMonitoringTypeToDomainsRelation; +}; diff --git a/server/models/asr_products.js b/server/models/asr_products.js new file mode 100644 index 000000000..9b965bec2 --- /dev/null +++ b/server/models/asr_products.js @@ -0,0 +1,69 @@ +"use strict"; +module.exports = (sequelize, DataTypes) => { + const AsrProducts = sequelize.define( + "asr_products", + { + id: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + }, + name: { + allowNull: false, + unique: true, + type: DataTypes.STRING, + }, + shortCode: { + allowNull: false, + unique: true, + type: DataTypes.STRING, + }, + tier: { + type: DataTypes.INTEGER, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + deletedAt: { + allowNull: true, + type: DataTypes.DATE, + defaultValue: null, + }, + createdBy: { + allowNull: false, + type: DataTypes.JSON, + defaultValue: { firstName: null, lastName: "System", email: "NA" }, + }, + updatedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + deletedBy: { + allowNull: true, + type: DataTypes.JSON, + defaultValue: null, + }, + }, + { + freezeTableName: true, + } + ); + + AsrProducts.associate = function (models) { + AsrProducts.belongsToMany(models.asr_domains, { + through: "asr_domain_to_products", + foreignKey: "product_id", + as: "associatedDomains", + }); + }; + + return AsrProducts; +}; diff --git a/server/models/integration_mapping.js b/server/models/integration_mapping.js index d2ac3b71a..cb53badc9 100644 --- a/server/models/integration_mapping.js +++ b/server/models/integration_mapping.js @@ -41,8 +41,15 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, defaultValue: DataTypes.NOW, }, + deletedAt: { + type: DataTypes.DATE, + allowNull: true, + }, }, - { freezeTableName: true } + { + freezeTableName: true, + paranoid: true, // This enables soft deletes + } ); // Association to integrations and application diff --git a/server/models/monitoring_types.js b/server/models/monitoring_types.js new file mode 100644 index 000000000..74d135fb0 --- /dev/null +++ b/server/models/monitoring_types.js @@ -0,0 +1,58 @@ +"use strict"; +module.exports = (sequelize, DataTypes) => { + const MonitoringTypes = sequelize.define( + "monitoring_types", + { + id: { + allowNull: false, + primaryKey: true, + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + }, + name: { + allowNull: false, + unique: true, + type: DataTypes.STRING, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + deletedAt: { + allowNull: true, + type: DataTypes.DATE, + }, + createdBy: { + allowNull: false, + type: DataTypes.JSON, + defaultValue: { firstName: null, lastName: "System", email: "NA" }, + }, + updatedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + deletedBy: { + allowNull: true, + type: DataTypes.JSON, + }, + }, + { + freezeTableName: true, + } + ); + + MonitoringTypes.associate = function (models) { + MonitoringTypes.belongsToMany(models.asr_domains, { + through: "asr_monitoring_type_to_domains", + foreignKey: "monitoring_type_id", + as: "asr_domains", + }); + }; + + return MonitoringTypes; +}; diff --git a/server/routes/asr/read.js b/server/routes/asr/read.js new file mode 100644 index 000000000..52830c361 --- /dev/null +++ b/server/routes/asr/read.js @@ -0,0 +1,347 @@ +const express = require("express"); +const router = express.Router(); +const { body, param, validationResult } = require("express-validator"); +const { sequelize } = require("../../models"); + +//Local Imports +const models = require("../../models"); +const logger = require("../../config/logger"); + +// Constants +const MonitoringTypes = models.monitoring_types; +const Domains = models.asr_domains; +const DomainMonitoringTypes = models.asr_monitoring_type_to_domains; +const Products = models.asr_products; +const DomainProduct = models.asr_domain_to_products; + +// Create a new domain +router.post("/domains/",[ + body("name").notEmpty().withMessage("Domain name is required"), + body("monitoringTypeIds") + .optional() + .isArray() + .withMessage("Monitoring type is required"), + body("createdBy").notEmpty().withMessage("Created by is required"), + ], async (req, res) => { + try { + // Validate the payload + const errors = validationResult(req); + if (!errors.isEmpty()) { + logger.error(errors.array()); + return res.status(400).json({ message: "Failed to save domain" }); + } + + /* if monitoring type is provided, + create domain, next iterate over monitoringTypeId and make entry to asr_domain_monitoring_types*/ + const { name, monitoringTypeIds, createdBy } = req.body; + let domain; + if (monitoringTypeIds) { + domain = await Domains.create({ name, createdBy }); + + // create domain monitoring type mapping + const createPromises = monitoringTypeIds.map((monitoringId) => { + return DomainMonitoringTypes.create({ + domain_id: domain.id, + monitoring_type_id: monitoringId, + createdBy, + }); + } + ); + + await Promise.all(createPromises); + } + + // if no monitoring type is provided, create domain without monitoring type + else { + domain = await Domains.create({ name, createdBy }); + } + res.status(200).json({message: "Domain created successfully", domain}); + } catch (error) { + logger.error(error); + res.status(500).json({ message: "Failed to create domain" }); + } + } +); + +//Get All domains +router.get("/domains/", async(req, res) => { + try{ + // get all domains and the associated monitoring types by using includes + const domains = await Domains.findAll({ + include: [ + { + model: MonitoringTypes, + through: { + attributes: [], // Exclude the junction table from the result + }, + as: "monitoringTypes", // Alias you used when defining the association + attributes: ["id", "name"], + }, + ], + raw: true, + }); + + res.status(200).json(domains); + }catch(err){ + logger.error(err); + res.status(500).json({message: 'Failed to fetch domains'}); + } +}); + +// Update a domain +router.patch( + "/domains/:id", + [ + param("id").isUUID().withMessage("ID must be a UUID"), + body("name").notEmpty().withMessage("Domain name is required"), + body("monitoringTypeIds") + .optional() + .isArray() + .withMessage("Monitoring must be array of UUIDs"), + body("updatedBy").isObject().withMessage("Updated by must be an object"), + ], + async (req, res) => { + try { + // Validate the request + const errors = validationResult(req); + if (!errors.isEmpty()) { + logger.error(errors.array()); + return res.status(400).json({ message: "Failed to update domain" }); + } + + // Update domain and delete or add relation in the junction table + const { name, monitoringTypeIds, updatedBy } = req.body; + let response; + if (monitoringTypeIds) { + response = await sequelize.transaction(async (t) => { + await Domains.update( + { name, updatedBy }, + { where: { id: req.params.id }, transaction: t } + ); + + // delete all monitoring types for the domain + await DomainMonitoringTypes.destroy({ + where: { domain_id: req.params.id }, + transaction: t, + }); + + // create domain monitoring type mapping + const createPromises = monitoringTypeIds.map((monitoringId) => { + return DomainMonitoringTypes.create( + { + domain_id: req.params.id, + monitoring_type_id: monitoringId, + updatedBy, + }, + { transaction: t } + ); + }); + + return Promise.all(createPromises); + }); + } else { + response = await Domains.update( + { name, updatedBy }, + { where: { id: req.params.id } } + ); + } + + const message = + response[0] === 0 ? "Domain not found" : "Successfully updated domain"; + res.status(200).json({ message }); + } catch (err) { + logger.error(err); + res.status(500).json({ message: "Failed to update domain" }); + } + } +); + +// Delete a domain - this should also delete monitoring types to domain mapping +router.delete("/domains/:id", +[ + param("id").isUUID().withMessage("ID must be a UUID") +], + async(req, res) => { + try{ + //Validate + const errors = validationResult(req); + if (!errors.isEmpty()) { + logger.error(errors.array()); + return res.status(400).json({ message: "Failed to delete product" }); + } + + const response = await Domains.destroy({ where: { id: req.params.id } }); + const message = + response === 0 ? "Domain not found" : "Domain deleted successfully"; + res.status(200).json({ message }); + }catch(err){ + logger.error(err); + res.status(500).json({message: 'Failed to delete domain'}); + } +}); + +// ----------------------------------- Products ------------------------------------- +//Create a new product +router.post("/products/", +[ + body("name").notEmpty().withMessage("Product name is required"), + body("shortCode").notEmpty().withMessage("Short code is required"), + body("tier").notEmpty().withMessage("Tier is required"), + body("createdBy").notEmpty().withMessage("Created by is required"), + body("domainIds").optional().isArray().withMessage("Domain ID must be an array of UUIDs"), +], +async(req, res) => { + try{ + // Validate the request + const errors = validationResult(req); + if(!errors.isEmpty()){ + logger.error(errors.array()); + return res.status(400).json({message: "Failed to save product"}); + } + + // If domainId is provided, create product domain relationship also + const {name, shortCode, tier, createdBy, domainIds} = req.body; + + let product; + if(domainIds){ + product = await Products.create({name, shortCode, tier, createdBy}); + + //Create product domain mapping + const createPromises = domainIds.map((domainId) => { + return DomainProduct.create({product_id: product.id, domain_id: domainId, createdBy}); + }); + await Promise.all(createPromises); + }else{ + product = await Products.create({name, shortCode, tier, createdBy}); + } + res.status(200).json({message: "Product created successfully", product}); + + }catch(error){ + console.log(error) + logger.error(error); + res.status(500).json({message: 'Failed to create product'}); + } +} +); + +// Get all products +router.get("/products/", async(req, res) => { + try { + // get all products and the associated domains + const products = await Products.findAll({ + include: [ + { + model: Domains, + through: { + attributes: [], // Exclude the junction table from the result + }, + as: "associatedDomains", + attributes: ["id", "name"], + }, + ], + raw: true, + }); + + res.status(200).json(products); + } catch (err) { + logger.error(err); + res.status(500).json({ message: "Failed to fetch domains" }); + } +}); + +// Patch a product +router.put( + "/products/:id", + [ + param("id").notEmpty().isUUID().withMessage("ID must be a UUID"), + body("name").notEmpty().isString().withMessage("Product name is required"), + body("shortCode").notEmpty().isString().withMessage("Short code is required"), + body("tier").notEmpty().isInt().withMessage("Tier is required"), + param("domainIds") + .optional() + .isArray() + .withMessage("Product ID must be an array of UUIDs"), + body("updatedBy").notEmpty().isObject().withMessage("Updated by must be an object"), + ], + async (req, res) => { + try { + // Validate the request + const errors = validationResult(req); + if (!errors.isEmpty()) { + logger.error(errors.array()); + return res.status(400).json({ message: "Failed to update product" }); + } + + // Update product and delete or add relation in the junction table + const { name, shortCode, tier, domainIds, updatedBy } = req.body; + + let response; + if (domainIds) { + response = await sequelize.transaction(async (t) => { + await Products.update( + { name, shortCode, tier, updatedBy }, + { where: { id: req.params.id }, transaction: t } + ); + + // delete all domains for the product + await DomainProduct.destroy({ + where: { product_id: req.params.id }, + transaction: t, + }); + + // create product domain mapping + const createPromises = domainIds.map((domainId) => { + return DomainProduct.create( + { + product_id: req.params.id, + domain_id: domainId, + updatedBy, + }, + { transaction: t } + ); + }); + + return Promise.all(createPromises); + }); + } else { + response = await Products.update( + { name, shortCode, tier, updatedBy }, + { where: { id: req.params.id } } + ); + } + + const message = + response[0] === 0 ? "Product not found" : "Successfully updated product"; + res.status(200).json({ message }); + } catch (err) { + logger.error(err); + res.status(500).json({ message: "Failed to update product" }); + } + } +); + +// Delete a product +router.delete("/products/:id", +[ + param("id").isUUID().withMessage("ID must be a UUID") +], async(req, res) => { + try{ + //Validate + const errors = validationResult(req); + if(!errors.isEmpty()){ + logger.error(errors.array()); + return res.status(400).json({message: "Failed to delete product"}); + } + + const response = await Products.destroy({where: {id: req.params.id}}); + + const message = response === 0 ? "Product not found" : "Product deleted successfully"; + res.status(200).json({message}); + }catch(err){ + logger.error(err); + res.status(500).json({message: 'Failed to delete product'}); + } +}); + +module.exports = router; + diff --git a/server/routes/integrations/read.js b/server/routes/integrations/read.js index 2eb0dc849..3dfa5658b 100644 --- a/server/routes/integrations/read.js +++ b/server/routes/integrations/read.js @@ -1,156 +1,187 @@ +const sequelize = require("sequelize"); +const express = require("express"); +const { param, body, validationResult } = require("express-validator"); + +const validatorUtil = require("../../utils/validator"); +const logger = require("../../config/logger"); const models = require("../../models"); + const integrations = models.integrations; const integration_mapping = models.integration_mapping; -const express = require("express"); const router = express.Router(); -const path = require("path"); -const fs = require("fs"); -const rootENV = path.join(process.cwd(), "..", ".env"); -const serverENV = path.join(process.cwd(), ".env"); -const ENVPath = fs.existsSync(rootENV) ? rootENV : serverENV; -const { param, body, validationResult } = require("express-validator"); -require("dotenv").config({ path: ENVPath }); -//return all integrations -router.get("/getAll", async (req, res) => { + +//Get all integrations - active or not from integrations table +router.get("/all", async (req, res) => { try { - console.log("running"); const result = await integrations.findAll(); res.status(200).send(result); } catch (err) { - // ... error checks - console.log(err); - res.status(500).send("Failed to get integrations: " + err); + logger.error(err); + res.status(500).json({ message: "Unable to get integrations" }); } }); -//get all integration_mappings with application_id -router.get( - "/getAll/:application_id", - [param("application_id").isUUID()], - async (req, res) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }); - } - try { - const { application_id } = req.params; - const result = await integration_mapping.findAll({ - where: { - application_id, - }, - }); - res.status(200).send(result); - } catch (err) { - // ... error checks - console.log(err); - res.status(500).send("Failed to get integration mappings: " + err); - } +// Get all active integrations from the integrations to application mapping table +router.get("/getAllActive/", async (req, res) => { + try { + const integrationMappingDetails = await integration_mapping.findAll( + { + include: [ + { + model: integrations, + as: "integration", + required: true, + attributes: ["name", "description", "metaData"], + }, + ], + }, + { + raw: true, + } + ); + res.status(200).send(integrationMappingDetails); + } catch (err) { + logger.error(err); + res.status(500).send("Failed to get active integrations: " + err); } -); +}); -//return one integration_mapping entry with application_id and integration_id paramter +// Get integration details by integration relation ID router.get( - "/getOne/:application_id/:integration_id", - [param("application_id").isUUID(), param("integration_id").isUUID()], + "/integrationDetails/:id", + [param("id").isUUID(4).withMessage("Invalid integration id")], async (req, res) => { - const errors = validationResult(req); + const errors = validationResult(req).formatWith( + validatorUtil.errorFormatter + ); if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }); + return res.status(422).json({ success: false, errors: errors.array() }); } try { - const { application_id, integration_id } = req.params; const result = await integration_mapping.findOne({ where: { - application_id, - integration_id, + id: req.params.id, }, + attributes: [ + [sequelize.col("integration_mapping.id"), "integrationMappingId"], + [ + sequelize.col("integration_mapping.metaData"), + "appSpecificIntegrationMetaData", + ], + "integration_mapping.application_id", + ], + include: [ + { + model: integrations, + as: "integration", + required: true, + attributes: [ + [sequelize.col("id"), "integrationId"], + [sequelize.col("name"), "integrationName"], + [sequelize.col("description"), "integrationDescription"], + [sequelize.col("metaData"), "integrationMetaData"], + ], + }, + ], }); + res.status(200).send(result); } catch (err) { - // ... error checks - console.log(err); - res.status(500).send("Failed to get integration mapping: " + err); + logger.error(err); + res.status(500).send("Failed to get integration details"); } } ); -//create entry into integration_mapping table with application_id paramter +// Change the active status of an integration router.post( - "/create", - [body("application_id").isUUID(), body("integration_id").isUUID()], + "/toggleStatus", + [body("integrationId").isUUID(4).withMessage("Invalid integration id")], + [body("application_id").isUUID(4).withMessage("Invalid integration id")], + [body("active").isBoolean().withMessage("Invalid active status")], async (req, res) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }); - } + const errors = validationResult(req).formatWith( + validatorUtil.errorFormatter + ); try { - const { application_id, integration_id, metaData } = req.body; - const result = await integration_mapping.create({ - application_id, - integration_id, - metaData, - }); - res.status(200).send(result); - } catch (err) { - // ... error checks - console.log(err); - res.status(500).send("Failed to create integration mapping: " + err); - } - } -); + /* + Intention to active + 1. restore if soft deleted + 2. create if not exists + Intention to deactivate + 1. destroy if exists + */ -//delete integration_mapping entry with application_id and integration_id paramter -router.delete( - "/delete", - [body("application_id").isUUID(), body("integration_id").isUUID()], - async (req, res) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }); - } - try { - const { application_id, integration_id } = req.body; - const result = await integration_mapping.destroy({ + if (!errors.isEmpty()) { + return res.status(422).json({ success: false, errors: errors.array() }); + } + const { integrationId, application_id, active } = req.body; + const result = await integration_mapping.findOne({ where: { application_id, - integration_id, + integration_id: integrationId, }, + paranoid: false, }); - res.status(200).send(result); + + if (active) { + if (result) { + await integration_mapping.restore({ + where: { + application_id, + integration_id: integrationId, + }, + }); + } else { + await integration_mapping.create({ + application_id, + integration_id: integrationId, + }); + } + } else { + await integration_mapping.destroy({ + where: { + application_id, + integration_id: integrationId, + }, + }); + } + res.status(200).json({ message: "Integration status changed" }); } catch (err) { - // ... error checks - console.log(err); - res.status(500).send("Failed to delete integration mapping: " + err); + logger.error(err); + res.status(500).json({ message: "Unable to update the integration" }); } } ); -//update integration_mapping entry with application_id and integration_id paramter +// Update the integration details (MetaData) by integration relation ID router.put( - "/update", - [body("application_id").isUUID(), body("integration_id").isUUID()], + "/updateIntegrationSettings/:id", + [param("id").isUUID(4).withMessage("Invalid integration id")], + [body("integrationSettings").isObject().withMessage("Invalid MetaData")], async (req, res) => { - const errors = validationResult(req); + const errors = validationResult(req).formatWith( + validatorUtil.errorFormatter + ); if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }); + return res.status(422).json({ success: false, errors: errors.array() }); } try { - const { application_id, integration_id, metaData } = req.body; const result = await integration_mapping.update( - { metaData }, + { + metaData: req.body.integrationSettings, + }, { where: { - application_id, - integration_id, + id: req.params.id, }, } ); res.status(200).send(result); } catch (err) { - // ... error checks - console.log(err); - res.status(500).send("Failed to update integration mapping: " + err); + logger.error(err); + res.status(500).send("Failed to update integration details"); } } ); diff --git a/server/routes/monitorings/read.js b/server/routes/monitorings/read.js new file mode 100644 index 000000000..02e71c04b --- /dev/null +++ b/server/routes/monitorings/read.js @@ -0,0 +1,90 @@ +const express = require("express"); +const router = express.Router(); +const { body, validationResult } = require("express-validator"); + + +//Local Imports +const models = require("../../models"); +const logger = require("../../config/logger"); + +// Constants +const MonitoringTypes = models.monitoring_types; + +// Route to get all monitoring types +router.get("/", async(req, res) => { + try{ + const monitoringTypes = await MonitoringTypes.findAll(); + res.status(200).json(monitoringTypes); + }catch(err){ + logger.error(err); + res.status(500).json({message: 'Failed to fetch monitoring types'}); + } +}); + +// Note - this route is for testing only . Monitoring types should be seeded in the database +// Route to post a new monitoring type +router.post("/", +[ + body("name").notEmpty().withMessage("Monitoring type name is required"), + body("createdBy").optional().isObject().withMessage("Created by is required") +], +async(req, res) => { + try{ + // Validate the request + const errors = validationResult(req); + if(!errors.isEmpty()){ + logger.error(errors.array()); + return res.status(400).json({message: "Failed to save monitoring type"}); + } + const monitoringType = await MonitoringTypes.create(req.body); + res.status(200).json(monitoringType); + }catch(error){ + logger.error(error); + res.status(500).json({message: 'Failed to create monitoring type'}); + } +}); + + +// Delete a monitoring type +router.delete("/:id", async(req, res) => { + try{ + const monitoringType = await MonitoringTypes.findByPk(req.params.id); + if(!monitoringType){ + return res.status(404).json({message: 'Monitoring type not found'}); + } + await monitoringType.destroy(); + res.status(200).json({message: 'Monitoring type deleted successfully'}); + }catch(error){ + logger.error(error); + res.status(500).json({message: 'Failed to delete monitoring type'}); + } +}); + +// update a monitoring type +router.put("/:id", +[ + body("name").notEmpty().withMessage("Monitoring type name is required"), + body("updatedBy").notEmpty().withMessage("Updated by is required") +], +async(req, res) => { + try{ + // Validate the request + const errors = validationResult(req); + if(!errors.isEmpty()){ + logger.error(errors.array()); + return res.status(400).json({message: "Failed to update monitoring type"}); + } + const monitoringType = await MonitoringTypes.findByPk(req.params.id); + if(!monitoringType){ + return res.status(404).json({message: 'Monitoring type not found'}); + } + await monitoringType.update(req.body); + res.status(200).json(monitoringType); + }catch(error){ + logger.error(error); + res.status(500).json({message: 'Failed to update monitoring type'}); + } +} +); + +module.exports = router; \ No newline at end of file diff --git a/server/seeders/20200204185532-rules.js b/server/seeders/20200204185532-rules.js index e34073532..bdf08c005 100644 --- a/server/seeders/20200204185532-rules.js +++ b/server/seeders/20200204185532-rules.js @@ -1,38 +1,47 @@ -'use strict'; -const { v4: uuidv4 } = require('uuid'); +"use strict"; +const { v4: uuidv4 } = require("uuid"); module.exports = { up: (queryInterface, Sequelize) => { - - return queryInterface.bulkInsert('rules', [{ - id: uuidv4(), - name : 'Date Validate', - createdAt : new Date(), - updatedAt : new Date() - },{ - id: uuidv4(), - name : 'Phone Validate', - createdAt : new Date(), - updatedAt : new Date() - },{ - id: uuidv4(), - name : 'Integer Validate', - createdAt : new Date(), - updatedAt : new Date() - },{ - id: uuidv4(), - name : 'Not Empty', - createdAt : new Date(), - updatedAt : new Date() - },{ - id: uuidv4(), - name : 'Assert', - createdAt : new Date(), - updatedAt : new Date() - }], {}); - + return queryInterface.bulkInsert( + "rules", + [ + { + id: uuidv4(), + name: "Date Validate", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: uuidv4(), + name: "Phone Validate", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: uuidv4(), + name: "Integer Validate", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: uuidv4(), + name: "Not Empty", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: uuidv4(), + name: "Assert", + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + {} + ); }, - down: (queryInterface, Sequelize) => { - } + down: async (queryInterface, Sequelize) => { + return queryInterface.bulkDelete("rules", null, {}); + }, }; diff --git a/server/seeders/20220818180347-constraints.js b/server/seeders/20220818180347-constraints.js index a2075c007..aaa75edb9 100644 --- a/server/seeders/20220818180347-constraints.js +++ b/server/seeders/20220818180347-constraints.js @@ -43,5 +43,7 @@ module.exports = { ); }, - down: (queryInterface, Sequelize) => {}, + down: async (queryInterface, Sequelize) => { + return queryInterface.bulkDelete("constraint", null, {}); + }, }; diff --git a/server/seeders/20240305192508-insert-ASR-integration-details.js b/server/seeders/20240305192508-insert-ASR-integration-details.js index 14db05635..ea1922aad 100644 --- a/server/seeders/20240305192508-insert-ASR-integration-details.js +++ b/server/seeders/20240305192508-insert-ASR-integration-details.js @@ -7,8 +7,7 @@ module.exports = { { id: uuidv4(), name: "ASR", - description: - "This integration enables ASR-related features in Tombolo and facilitates connections to Orbit servers, enhancing user capabilities and data accessibility", + description: "This integration enables ASR-related features in Tombolo and facilitates connections to Orbit servers, enhancing user capabilities and data accessibility", createdAt: new Date(), updatedAt: new Date(), }, diff --git a/server/seeders/20240312202046-seed-monitoring-types.js b/server/seeders/20240312202046-seed-monitoring-types.js new file mode 100644 index 000000000..31430f8ad --- /dev/null +++ b/server/seeders/20240312202046-seed-monitoring-types.js @@ -0,0 +1,23 @@ +"use strict"; +const { v4: uuidv4 } = require("uuid"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.bulkInsert("monitoring_types", [ + { + id: uuidv4(), + name: "Job Monitoring", + createdAt: new Date(), + createdBy: JSON.stringify({ + firstName: null, + lastName: "System", + email: "NA", + }), + }, + ]); + }, + + down: async (queryInterface, Sequelize) => { + return queryInterface.bulkDelete("monitoring_types", null, {}); + }, +}; diff --git a/server/server.js b/server/server.js index be01663bd..abf469229 100644 --- a/server/server.js +++ b/server/server.js @@ -82,6 +82,8 @@ const integrations = require("./routes/integrations/read"); const teamsHook = require("./routes/msTeamsHook/read"); const fido = require("./routes/fido/read"); const notification_queue = require("./routes/notification_queue/read"); +const monitorings = require("./routes/monitorings/read"); +const asr = require("./routes/asr/read"); // Log all HTTP requests app.use((req, res, next) => { @@ -95,6 +97,7 @@ app.use("/api/updateNotification", updateNotifications); //exposed API, requires api key for any routes app.use("/api/apikeys", api); +// Authenticate token before proceeding to route app.use(tokenService.verifyToken); app.use("/api/job", job); @@ -127,6 +130,8 @@ app.use("/api/integrations", integrations); app.use("/api/teamsHook", teamsHook); app.use("/api/fido", fido); app.use("/api/notification_queue", notification_queue); +app.use("/api/monitorings", monitorings); +app.use("/api/asr", asr); // Safety net for unhandled errors app.use((err, req, res, next) => { diff --git a/todo b/todo new file mode 100644 index 000000000..32f18997a --- /dev/null +++ b/todo @@ -0,0 +1,110 @@ +## Create Migration and Model for following tables: + +1. `monitoring_types` table: + - `id` (Primary Key, uuid , not null) + - `name` (String, not null, unique) + - createdAt (timestamp, not null, default: current_timestamp) + - updatedAt (timestamp, not null, default: current_timestamp) + - deletedAt (timestamp, default: null) + - createdBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - updatedBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - deletedBy (JSON, default: null) + - paranoid (boolean, default: true) + +2. `asr_domains` table: + - `id` (Primary Key, uuid , not null) + - `name` ( String, not null, unique) + - createdAt (timestamp, not null, default: current_timestamp) + - updatedAt (timestamp, not null, default: current_timestamp) + - createdBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - updatedBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - deletedBy (JSON, default: null) + - paranoid (boolean, default: true) + + +3. `asr_products` table: + - `id` (Primary Key, uuid , not null) + - `name` (String, not null, unique) + - createdAt (timestamp, not null, default: current_timestamp) + - updatedAt (timestamp, not null, default: current_timestamp) + - createdBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - updatedBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - deletedBy (JSON, default: null) + - paranoid (boolean, default: true) + +4. `asr_monitoring_type_to_domains_relation` junction table: + - `id` (Primary Key, uuid , not null) + - `monitoring_type_id` (Foreign Key referencing `monitoring_types.id`) + - `domain_id` (Foreign Key referencing `domains.id`) + - constraint unique (`monitoring_type_id`, `domain_id`) + - constraint on delete/update cascade on `monitoring_type_id` + - constraint on delete/update cascade on `domain_id` + - createdAt (timestamp, not null, default: current_timestamp) + - updatedAt (timestamp, not null, default: current_timestamp) + - createdBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - updatedBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - deletedBy (JSON, default: null) + - paranoid (boolean, default: true) + + +5. `asr_domain_to_products_relation` junction table: + - `id` (Primary Key, uuid , not null) + - `domain_id` (Foreign Key referencing `domains.id`) + - `product_id` (Foreign Key referencing `products.id`) + - constraint unique (`domain_id`, `product_id`) + - constraint on delete/update cascade on `domain_id` + - constraint on delete/update cascade on `product_id` + - createdAt (timestamp, not null, default: current_timestamp) + - updatedAt (timestamp, not null, default: current_timestamp) + - createdBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - updatedBy (JSON, not null, default: {name: 'system', email: 'NA'}) + - deletedBy (JSON, default: null) + - paranoid (boolean, default: true) + + PRODUCT has extra FIELDS +## Create seed data and files for above tables: + +## Create routes for following: + 1. Insert data in asr_domain tables - + the request comes with monitoring_type_id and domain name. entry should be made in asr_monitoring_type_to_domains_relation table asr_domain_to_products_relation + 2. Insert data in asr_products table - + the request comes with domain_id and product name. entry should be made in asr_domain_to_products_relation table + 3. Get all domains for a monitoring type + the request comes with monitoring_type_id. all the domains for that monitoring type should be returned + 4. Get all products for a domain + the request comes with domain_id. all the products for that domain should be returned + 5. Delete a domain + the request comes with domain_id. the domain should be deleted from asr_domains table and all the relations from asr_monitoring_type_to_domains_relation and asr_domain_to_products_relation should be deleted + 6. Delete a product + the request comes with product_id. the product should be deleted from asr_products table and all the relations from asr_domain_to_products_relation should be deleted + 7. Update a domain + the request comes with domain_id and new domain name. the domain name should be updated in asr_domains table + 8. Update a product + the request comes with product_id and new product name. the product name should be updated in asr_products table + 9. Get all domains regardless of monitoring type + all the domains should be returned + 10. Get all products regardless of domain + all the products should be returned + +## UI Fetch functons for above routes: + 1. Create fetch function for all the above routes + +## UI - ASR Integration settings page + 1. Display all the domains in a domain table + - the table should have columns - domain name, monitoring types displayed as tags, actions coulmn with delete, update, view icons + - Button to delete a domain - pop confirmation required + - Button to update a domain - should open a modal with a form. the form should pre-populate the domain name and monitoring types + - The monitoring types should be a select component + - The select options should be list of all monitoring types + - Existing monitoring types should be pre-selected and displayed as tags + - The select component when open should clearly show if a monitoring type is already selected with check mark or something related + - Saving from should give summary of changes that will be made and ask for re-confirmation + 2. Display all the products in a product table + - the table should have columns - product name, domains displayed as tags, actions coulmn with delete, update, view icons + - Button to delete a product - pop confirmation required + - Button to update a product - should open a modal with a form. the form should pre-populate the product name and domains + - The domains should be a select component + - The select options should be list of all domains + - Existing domains should be pre-selected and displayed as tags + - The select component when open should clearly show if a domain is already selected with check mark or something related + - Saving from should give summary of changes that will be made and ask for re-confirmation \ No newline at end of file