diff --git a/portals/admin/src/main/webapp/WEB-INF/web.xml b/portals/admin/src/main/webapp/WEB-INF/web.xml index c8947e852dc..48db6043918 100644 --- a/portals/admin/src/main/webapp/WEB-INF/web.xml +++ b/portals/admin/src/main/webapp/WEB-INF/web.xml @@ -119,6 +119,7 @@ index /tasks/* + /governance/* /microgateway/* /settings/* /categories/* diff --git a/portals/admin/src/main/webapp/package-lock.json b/portals/admin/src/main/webapp/package-lock.json index a651f06aa99..74c75598847 100644 --- a/portals/admin/src/main/webapp/package-lock.json +++ b/portals/admin/src/main/webapp/package-lock.json @@ -18,6 +18,7 @@ "@mui/lab": "^5.0.0-alpha.160", "@mui/material": "^5.15.4", "@mui/system": "^5.15.4", + "@mui/x-charts": "^7.24.0", "@mui/x-date-pickers": "^7.14.0", "@mui/x-tree-view": "^6.17.0", "@react-pdf/renderer": "^3.4.4", @@ -1962,9 +1963,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -2298,10 +2299,11 @@ "integrity": "sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww==" }, "node_modules/@formatjs/cli": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-6.2.12.tgz", - "integrity": "sha512-bt1NEgkeYN8N9zWcpsPu3fZ57vv+biA+NtIQBlyOZnCp1bcvh+vNTXvmwF4C5qxqDtCylpOIb3yi3Ktgp4v0JQ==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-6.5.1.tgz", + "integrity": "sha512-JptxQysoKiVZkGfd1u9GTlt88IQ1NUCUpxWI7Obkr7cZAupIBT5QbyL1NPq3UAyTgXODQ+09IUwZhJwVExW41g==", "dev": true, + "license": "MIT", "bin": { "formatjs": "bin/formatjs" }, @@ -2310,11 +2312,11 @@ }, "peerDependencies": { "@glimmer/env": "^0.1.7", - "@glimmer/reference": "^0.91.1 || ^0.92.0", - "@glimmer/syntax": "^0.92.0", - "@glimmer/validator": "^0.92.0", + "@glimmer/reference": "^0.91.1 || ^0.92.0 || ^0.93.0", + "@glimmer/syntax": "^0.92.0 || ^0.93.0", + "@glimmer/validator": "^0.92.0 || ^0.93.0", "@vue/compiler-core": "^3.4.0", - "content-tag": "^2.0.1", + "content-tag": "^2.0.1 || ^3.0.0", "ember-template-recast": "^6.1.4", "vue": "^3.4.0" }, @@ -3783,6 +3785,84 @@ } } }, + "node_modules/@mui/x-charts": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.24.1.tgz", + "integrity": "sha512-OdTS/nXaANPe4AoUFIDD4LlID8kK/00q+uqVOCkVClEvFQeAkj3pBaghdS4hY7rVqsCgsm+yOStQVJa9G2MR+Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-charts-vendor": "7.20.0", + "@mui/x-internals": "7.24.1", + "@react-spring/rafz": "^9.7.5", + "@react-spring/web": "^9.7.5", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts-vendor": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz", + "integrity": "sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg==", + "license": "MIT AND ISC", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@types/d3-color": "^3.1.3", + "@types/d3-delaunay": "^6.0.4", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-scale": "^4.0.8", + "@types/d3-shape": "^3.1.6", + "@types/d3-time": "^3.0.3", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "delaunator": "^5.0.1", + "robust-predicates": "^3.0.2" + } + }, + "node_modules/@mui/x-charts/node_modules/@mui/x-internals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.1.tgz", + "integrity": "sha512-9BvJzpLJnS9BDphvkiv6v0QOLxbnu8jhwcexFjtCQ2ZyxtVuVsWzGZ2npT9sGOil7+eaFDmWnJtea/tgrPvSwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/x-date-pickers": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.18.0.tgz", @@ -4157,6 +4237,78 @@ "integrity": "sha512-7KrPPCpgRPKR+g+T127PE4bpw9Q84ZiY07EYRwXKVtTEVW9wJ5BZiF9smT9IvH19s+MQaDLmYRgjESsnqlyH0Q==", "license": "MIT" }, + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -5037,6 +5189,57 @@ "@types/node": "*" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -7878,6 +8081,121 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8131,6 +8449,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -10921,6 +11248,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -16646,6 +16982,12 @@ "inherits": "^2.0.1" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", diff --git a/portals/admin/src/main/webapp/package.json b/portals/admin/src/main/webapp/package.json index 66aa97d7c84..4799708c806 100644 --- a/portals/admin/src/main/webapp/package.json +++ b/portals/admin/src/main/webapp/package.json @@ -37,6 +37,7 @@ "@mui/lab": "^5.0.0-alpha.160", "@mui/material": "^5.15.4", "@mui/system": "^5.15.4", + "@mui/x-charts": "^7.24.0", "@mui/x-date-pickers": "^7.14.0", "@mui/x-tree-view": "^6.17.0", "@react-pdf/renderer": "^3.4.4", diff --git a/portals/admin/src/main/webapp/services/login/login_callback.jsp b/portals/admin/src/main/webapp/services/login/login_callback.jsp index 2eaf85a7334..f6e4d8ad20e 100644 --- a/portals/admin/src/main/webapp/services/login/login_callback.jsp +++ b/portals/admin/src/main/webapp/services/login/login_callback.jsp @@ -140,6 +140,13 @@ cookie.setMaxAge((int) expiresIn); response.addCookie(cookie); + cookie = new Cookie("AM_ADMIN_ACC_TOKEN_DEFAULT_P2", accessTokenPart2); + cookie.setPath(proxyContext != null ? proxyContext + "/api/am/governance/" : "/api/am/governance/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setMaxAge((int) expiresIn); + response.addCookie(cookie); + cookie = new Cookie("AM_REF_TOKEN_DEFAULT_P2", refreshTokenPart2); cookie.setPath(context + "/"); cookie.setHttpOnly(true); diff --git a/portals/admin/src/main/webapp/services/logout/logout_callback.jsp b/portals/admin/src/main/webapp/services/logout/logout_callback.jsp index 815c6f7be67..af118b9cbc6 100644 --- a/portals/admin/src/main/webapp/services/logout/logout_callback.jsp +++ b/portals/admin/src/main/webapp/services/logout/logout_callback.jsp @@ -41,6 +41,13 @@ cookie.setMaxAge(2); response.addCookie(cookie); + cookie = new Cookie("AM_ADMIN_ACC_TOKEN_DEFAULT_P2", ""); + cookie.setPath("/api/am/governance/" + context + "/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setMaxAge(2); + response.addCookie(cookie); + cookie = new Cookie("AM_REF_TOKEN_DEFAULT_P2", ""); cookie.setPath(context + "/"); cookie.setHttpOnly(true); diff --git a/portals/admin/src/main/webapp/services/refresh/refresh.jsp b/portals/admin/src/main/webapp/services/refresh/refresh.jsp index 16f9841801c..3b2c56dcfd2 100644 --- a/portals/admin/src/main/webapp/services/refresh/refresh.jsp +++ b/portals/admin/src/main/webapp/services/refresh/refresh.jsp @@ -139,6 +139,13 @@ cookie.setMaxAge((int) expiresIn); response.addCookie(cookie); + cookie = new Cookie("AM_ADMIN_ACC_TOKEN_DEFAULT_P2", accessTokenPart2); + cookie.setPath("/api/am/governance/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setMaxAge((int) expiresIn); + response.addCookie(cookie); + cookie = new Cookie("AM_REF_TOKEN_DEFAULT_P2", refreshTokenPart2); cookie.setPath(context + "/"); cookie.setHttpOnly(true); diff --git a/portals/admin/src/main/webapp/site/public/locales/en.json b/portals/admin/src/main/webapp/site/public/locales/en.json index 10e11e91f56..4879ec485de 100644 --- a/portals/admin/src/main/webapp/site/public/locales/en.json +++ b/portals/admin/src/main/webapp/site/public/locales/en.json @@ -152,6 +152,14 @@ "AdminPages.Gateways.table.header.permission": "Visibility Permission", "AdminPages.Gateways.table.header.type": "Type", "AdminPages.Gateways.table.header.vhosts": "Virtual Host(s)", + "AdminPages.Governance.Policy.Delete.form.delete.confirmation.message": "Are you sure you want to delete this Policy?", + "AdminPages.Governance.Policy.Delete.form.delete.dialog.btn": "Delete", + "AdminPages.Governance.Policy.Delete.form.delete.dialog.title": "Delete Policy?", + "AdminPages.Governance.Policy.Delete.form.delete.successful": "Policy deleted successfully", + "AdminPages.Governance.Ruleset.Delete.form.delete.confirmation.message": "Are you sure you want to delete this Ruleset?", + "AdminPages.Governance.Ruleset.Delete.form.delete.dialog.btn": "Delete", + "AdminPages.Governance.Ruleset.Delete.form.delete.dialog.title": "Delete Ruleset?", + "AdminPages.Governance.Ruleset.Delete.form.delete.successful": "Ruleset deleted successfully", "AdminPages.KeyManager.Delete.form.delete.confirmation.message": "Are you sure you want to delete this KeyManager ?", "AdminPages.KeyManagers.Delete.form.delete.dialog.btn": "Delete", "AdminPages.KeyManagers.Delete.form.delete.dialog.title": "Delete KeyManager ?", @@ -311,11 +319,15 @@ "Base.RouteMenuMapping.custom.throttling.policies.items.Editing": "Edit Custom Policy", "Base.RouteMenuMapping.dashboard": "Dashboard", "Base.RouteMenuMapping.gateways": "Gateways", + "Base.RouteMenuMapping.governance": "Governance", + "Base.RouteMenuMapping.governance.policies": "Policies", "Base.RouteMenuMapping.keymanagers": "Key Managers", "Base.RouteMenuMapping.keymanagers.items.Adding": "Add Key Manager", "Base.RouteMenuMapping.keymanagers.items.Editing": "Edit Key Manager", + "Base.RouteMenuMapping.overview": "Overview", "Base.RouteMenuMapping.organizations": "Organizations", "Base.RouteMenuMapping.role.permissions": "Scope Assignments", + "Base.RouteMenuMapping.ruleset.catalog": "Ruleset Catalog", "Base.RouteMenuMapping.settings": "Settings", "Base.RouteMenuMapping.subscription.creation": "Subscription Creation", "Base.RouteMenuMapping.subscription.deletion": "Subscription Deletion", @@ -412,6 +424,178 @@ "GatewayEnvironments.AddEditVhost.httpsPort": "HTTPS Port", "GatewayEnvironments.AddEditVhost.wsPort": "WS Port", "GatewayEnvironments.AddEditVhost.wssPort": "WSS Port", + "Governance.Overview.APICompliance.PolicyAdherence.column.policy": "Policy", + "Governance.Overview.APICompliance.PolicyAdherence.column.rulesets": "Rulesets", + "Governance.Overview.APICompliance.PolicyAdherence.column.status": "Status", + "Governance.Overview.APICompliance.PolicyAdherence.empty.helper": "No governance policies have been applied to this API.", + "Governance.Overview.APICompliance.PolicyAdherence.empty.title": "No Policies Applied", + "Governance.Overview.APICompliance.PolicyAdherence.followed.count": "{followed}/{total} Followed", + "Governance.Overview.APICompliance.RuleViolation.column.description": "Description", + "Governance.Overview.APICompliance.RuleViolation.column.message": "Message", + "Governance.Overview.APICompliance.RuleViolation.column.path": "Path", + "Governance.Overview.APICompliance.RuleViolation.column.rule": "Rule", + "Governance.Overview.APICompliance.RuleViolation.empty.errors": "No Error violations found", + "Governance.Overview.APICompliance.RuleViolation.empty.info": "No Info violations found", + "Governance.Overview.APICompliance.RuleViolation.empty.passed": "No Passed rules found", + "Governance.Overview.APICompliance.RuleViolation.empty.warnings": "No Warning violations found", + "Governance.Overview.APICompliance.RuleViolation.tab.errors": "Errors ({count})", + "Governance.Overview.APICompliance.RuleViolation.tab.info": "Info ({count})", + "Governance.Overview.APICompliance.RuleViolation.tab.passed": "Passed ({count})", + "Governance.Overview.APICompliance.RuleViolation.tab.warnings": "Warnings ({count})", + "Governance.Overview.APICompliance.RulesetAdherence.column.ruleset": "Ruleset", + "Governance.Overview.APICompliance.RulesetAdherence.column.status": "Status", + "Governance.Overview.APICompliance.RulesetAdherence.column.violations": "Violations", + "Governance.Overview.APICompliance.RulesetAdherence.empty.helper": "No governance rulesets have been applied for this API.", + "Governance.Overview.APICompliance.RulesetAdherence.empty.title": "No Rulesets Found", + "Governance.Overview.APICompliance.RulesetAdherence.violations.tooltip": "Errors: {error}, Warnings: {warn}, Info: {info}", + "Governance.Overview.APICompliance.column.api": "API", + "Governance.Overview.APICompliance.column.policies": "Policies", + "Governance.Overview.APICompliance.column.status": "Status", + "Governance.Overview.APICompliance.empty.content": "No APIs Available", + "Governance.Overview.APICompliance.empty.helper": "Create APIs to start evaluating their compliance.", + "Governance.Overview.APICompliance.followed.count": "{followed}/{total} Followed", + "Governance.Overview.APICompliance.no.policies": "N/A - No policies to evaluate", + "Governance.Overview.Compliance.back.to.overview": "Back to Overview", + "Governance.Overview.Compliance.failed": "Failed", + "Governance.Overview.Compliance.passed": "Passed", + "Governance.Overview.Compliance.policy.adherence.summary": "Policy Adherence Summary", + "Governance.Overview.Compliance.ruleset.adherence": "Ruleset Adherence", + "Governance.Overview.Compliance.ruleset.adherence.summary": "Ruleset Adherence Summary", + "Governance.Overview.Compliance.title": "Compliance Summary - {artifactName}", + "Governance.Overview.PolicyAdherence.column.apis": "APIs", + "Governance.Overview.PolicyAdherence.column.policy": "Policy", + "Governance.Overview.PolicyAdherence.column.status": "Status", + "Governance.Overview.PolicyAdherence.compliant.count": "{followed}/{total} Compliant", + "Governance.Overview.PolicyAdherence.empty.content": "No Governance Policies Available", + "Governance.Overview.PolicyAdherence.empty.helper": "Create a new governance policy to start governing the APIs.", + "Governance.Overview.PolicyAdherence.no.apis": "N/A - No APIs to evaluate", + "Governance.Overview.Summary.api.compliance": "API Compliance", + "Governance.Overview.Summary.api.compliance.details": "API Compliance Details", + "Governance.Overview.Summary.api.compliant": "Compliant ({count})", + "Governance.Overview.Summary.api.non.compliant": "Non-Compliant ({count})", + "Governance.Overview.Summary.api.not.applicable": "Not Applicable ({count})", + "Governance.Overview.Summary.policy.adherence": "Policy Adherence", + "Governance.Overview.Summary.policy.adherence.details": "Policy Adherence Details", + "Governance.Overview.Summary.policy.followed": "Followed ({count})", + "Governance.Overview.Summary.policy.not.applied": "Not Applied ({count})", + "Governance.Overview.Summary.policy.violated": "Violated ({count})", + "Governance.Overview.title": "Overview", + "Governance.Policies.AddEdit.action.actions": "Actions", + "Governance.Policies.AddEdit.action.add": "Add Action Configuration", + "Governance.Policies.AddEdit.action.block": "Block", + "Governance.Policies.AddEdit.action.cancel": "Cancel", + "Governance.Policies.AddEdit.action.config.title": "Action Configuration", + "Governance.Policies.AddEdit.action.governedState": "Governed State", + "Governance.Policies.AddEdit.action.notify": "Notify", + "Governance.Policies.AddEdit.action.save": "Save", + "Governance.Policies.AddEdit.action.severity.levels": "Severity Levels", + "Governance.Policies.AddEdit.action.table.actions": "Actions", + "Governance.Policies.AddEdit.action.table.onError": "On Error", + "Governance.Policies.AddEdit.action.table.onInfo": "On Info", + "Governance.Policies.AddEdit.action.table.onWarn": "On Warn", + "Governance.Policies.AddEdit.action.table.state": "State", + "Governance.Policies.AddEdit.add.success": "Policy Added Successfully", + "Governance.Policies.AddEdit.edit.success": "Policy Updated Successfully", + "Governance.Policies.AddEdit.enforcement.description": "Provide details of when the policy will be applied", + "Governance.Policies.AddEdit.enforcement.title": "Enforcement Details", + "Governance.Policies.AddEdit.error.loading.labels": "Error loading labels", + "Governance.Policies.AddEdit.error.loading.rulesets": "Error loading rulesets", + "Governance.Policies.AddEdit.form.actions.invalid": "Actions must be properly configured", + "Governance.Policies.AddEdit.form.actions.invalid.block": "BLOCK action is not allowed for API_CREATE and API_UPDATE states", + "Governance.Policies.AddEdit.form.add.btn": "Create", + "Governance.Policies.AddEdit.form.cancel": "Cancel", + "Governance.Policies.AddEdit.form.description": "Description", + "Governance.Policies.AddEdit.form.description.help": "Description of the governance policy.", + "Governance.Policies.AddEdit.form.description.too.long": "Description cannot exceed 1024 characters", + "Governance.Policies.AddEdit.form.has.errors": "One or more fields contain errors.", + "Governance.Policies.AddEdit.form.labels.required": "At least one label is required when applying to specific APIs", + "Governance.Policies.AddEdit.form.name": "Name", + "Governance.Policies.AddEdit.form.name.help": "Name of the governance policy.", + "Governance.Policies.AddEdit.form.name.invalid": "Policy name can only contain alphanumeric characters, hyphens, underscores, and spaces", + "Governance.Policies.AddEdit.form.name.required": "Policy name is required", + "Governance.Policies.AddEdit.form.name.too.long": "Policy name cannot exceed 255 characters", + "Governance.Policies.AddEdit.form.name.too.short": "Policy name cannot be empty", + "Governance.Policies.AddEdit.form.rulesets.duplicate": "Duplicate rulesets are not allowed", + "Governance.Policies.AddEdit.form.rulesets.required": "At least one ruleset is required", + "Governance.Policies.AddEdit.form.update.btn": "Update", + "Governance.Policies.AddEdit.general.details": "General Details", + "Governance.Policies.AddEdit.general.details.description": "Provide name and description of the policy.", + "Governance.Policies.AddEdit.labels.applyAll": "Apply to all APIs", + "Governance.Policies.AddEdit.labels.applyNone": "Apply to none", + "Governance.Policies.AddEdit.labels.applySpecific": "Apply to APIs with specific labels", + "Governance.Policies.AddEdit.labels.description": "Choose whether to apply this policy to all APIs or only to APIs with specific labels", + "Governance.Policies.AddEdit.labels.helper": "Select one or more labels to determine which APIs this policy applies to", + "Governance.Policies.AddEdit.labels.input": "Select Labels", + "Governance.Policies.AddEdit.labels.title": "Applicability", + "Governance.Policies.AddEdit.rulesets.description": "Search and select rulesets to include in the policy. Selected rulesets will appear above the search bar.", + "Governance.Policies.AddEdit.rulesets.empty": "No rulesets available", + "Governance.Policies.AddEdit.rulesets.noSearchResults": "No rulesets found matching your search", + "Governance.Policies.AddEdit.rulesets.title": "Rulesets", + "Governance.Policies.AddEdit.title.edit": "Governance Policy - Edit", + "Governance.Policies.AddEdit.title.new": "Governance Policy - Create new", + "Governance.Policies.List.add.new.policy": "Create Policy", + "Governance.Policies.List.addPolicy.title": "Create Governance Policy", + "Governance.Policies.List.addPolicy.triggerButtonText": "Create Governance Policy", + "Governance.Policies.List.column.appliesTo": "Applies to", + "Governance.Policies.List.column.appliesWhen": "Applies when", + "Governance.Policies.List.column.policy": "Policy", + "Governance.Policies.List.description": "Create governance policies using rulesets from the catalog to standardize and regulate your APls effectively", + "Governance.Policies.List.edit.title": "Edit Policy", + "Governance.Policies.List.empty.content": "Governance policies help you enforce standards and compliance across your APIs. Click the Create button to add your first policy.", + "Governance.Policies.List.empty.title": "Governance Policies", + "Governance.Policies.List.search.default": "Search policies by name or label", + "Governance.Policies.List.title": "Governance Policies", + "Governance.Rulesets.AddEdit.add.success": "Ruleset Added Successfully", + "Governance.Rulesets.AddEdit.button.create": "Create", + "Governance.Rulesets.AddEdit.button.update": "Update", + "Governance.Rulesets.AddEdit.button.upload": "Upload File", + "Governance.Rulesets.AddEdit.confirm.overwrite.cancel": "Cancel", + "Governance.Rulesets.AddEdit.confirm.overwrite.message": "The editor contains existing content. Do you want to overwrite it with the uploaded file?", + "Governance.Rulesets.AddEdit.confirm.overwrite.ok": "Overwrite", + "Governance.Rulesets.AddEdit.confirm.overwrite.title": "Confirm Overwrite", + "Governance.Rulesets.AddEdit.content.description": "Define the ruleset content in YAML or JSON format", + "Governance.Rulesets.AddEdit.content.title": "Ruleset Content", + "Governance.Rulesets.AddEdit.edit.success": "Ruleset Updated Successfully", + "Governance.Rulesets.AddEdit.error.loading": "Error loading ruleset", + "Governance.Rulesets.AddEdit.file.read.error": "Error reading file", + "Governance.Rulesets.AddEdit.form.artifact.type": "Artifact Type", + "Governance.Rulesets.AddEdit.form.artifacttype.required": "Artifact type is required", + "Governance.Rulesets.AddEdit.form.cancel": "Cancel", + "Governance.Rulesets.AddEdit.form.description": "Description", + "Governance.Rulesets.AddEdit.form.description.too.long": "Description cannot exceed 1000 characters", + "Governance.Rulesets.AddEdit.form.doclink.invalid": "Documentation link must be a valid HTTP/HTTPS URL", + "Governance.Rulesets.AddEdit.form.doclink.too.long": "Documentation link cannot exceed 500 characters", + "Governance.Rulesets.AddEdit.form.documentation": "Documentation Link", + "Governance.Rulesets.AddEdit.form.has.errors": "One or more fields contain errors.", + "Governance.Rulesets.AddEdit.form.name": "Name", + "Governance.Rulesets.AddEdit.form.name.invalid": "Ruleset name can only contain alphanumeric characters, spaces, hyphens and underscores.", + "Governance.Rulesets.AddEdit.form.name.required": "Ruleset name is required", + "Governance.Rulesets.AddEdit.form.name.too.long": "Ruleset name cannot exceed 255 characters", + "Governance.Rulesets.AddEdit.form.ruleset.type": "Ruleset Type", + "Governance.Rulesets.AddEdit.form.rulesetcontent.required": "Ruleset content is required", + "Governance.Rulesets.AddEdit.form.ruletype.required": "Rule type is required", + "Governance.Rulesets.AddEdit.general.details": "General Details", + "Governance.Rulesets.AddEdit.general.details.description": "Provide name and description of the ruleset.", + "Governance.Rulesets.AddEdit.title.edit": "Edit Ruleset - {name}", + "Governance.Rulesets.AddEdit.title.new": "Create New Ruleset", + "Governance.Rulesets.Create.genai": "Create with GenAI", + "Governance.Rulesets.Create.genai.desc": "Use AI to help you create a ruleset", + "Governance.Rulesets.Create.options": "Choose Creation Method", + "Governance.Rulesets.Create.scratch": "Create from Scratch", + "Governance.Rulesets.Create.scratch.desc": "Create a ruleset manually", + "Governance.Rulesets.List.add.new.ruleset": "Create Ruleset", + "Governance.Rulesets.List.addRuleset.title": "Create Ruleset", + "Governance.Rulesets.List.addRuleset.triggerButtonText": "Create Ruleset", + "Governance.Rulesets.List.column.artifactType": "Artifact Type", + "Governance.Rulesets.List.column.provider": "Provider", + "Governance.Rulesets.List.column.ruleset": "Ruleset", + "Governance.Rulesets.List.column.rulesetType": "Ruleset Type", + "Governance.Rulesets.List.description": "Find comprehensive governance rulesets designed to ensure the consistency, security and reliability for your APls", + "Governance.Rulesets.List.edit.title": "Edit Ruleset", + "Governance.Rulesets.List.empty.content": "Rulesets are the building blocks for creating governance policies. They contain predefined rules and validations that can be used to enforce standards across your APIs. Click Create Ruleset to get started.", + "Governance.Rulesets.List.empty.title": "Ruleset Catalog", + "Governance.Rulesets.List.search.placeholder": "Search rulesets by name or type", + "Governance.Rulesets.List.title": "Ruleset Catalog", "KeyManager.AddEdit.Invalid.Roles.Found": "Invalid Role(s) Found", "KeyManager.AddEdit.roles.help": "Enter a valid role and press `Enter`", "KeyManager.AddEditKeyManager.permissions.add.description": "Permissions for the Key Manager", diff --git a/portals/admin/src/main/webapp/site/public/locales/fr.json b/portals/admin/src/main/webapp/site/public/locales/fr.json index 10e11e91f56..4879ec485de 100644 --- a/portals/admin/src/main/webapp/site/public/locales/fr.json +++ b/portals/admin/src/main/webapp/site/public/locales/fr.json @@ -152,6 +152,14 @@ "AdminPages.Gateways.table.header.permission": "Visibility Permission", "AdminPages.Gateways.table.header.type": "Type", "AdminPages.Gateways.table.header.vhosts": "Virtual Host(s)", + "AdminPages.Governance.Policy.Delete.form.delete.confirmation.message": "Are you sure you want to delete this Policy?", + "AdminPages.Governance.Policy.Delete.form.delete.dialog.btn": "Delete", + "AdminPages.Governance.Policy.Delete.form.delete.dialog.title": "Delete Policy?", + "AdminPages.Governance.Policy.Delete.form.delete.successful": "Policy deleted successfully", + "AdminPages.Governance.Ruleset.Delete.form.delete.confirmation.message": "Are you sure you want to delete this Ruleset?", + "AdminPages.Governance.Ruleset.Delete.form.delete.dialog.btn": "Delete", + "AdminPages.Governance.Ruleset.Delete.form.delete.dialog.title": "Delete Ruleset?", + "AdminPages.Governance.Ruleset.Delete.form.delete.successful": "Ruleset deleted successfully", "AdminPages.KeyManager.Delete.form.delete.confirmation.message": "Are you sure you want to delete this KeyManager ?", "AdminPages.KeyManagers.Delete.form.delete.dialog.btn": "Delete", "AdminPages.KeyManagers.Delete.form.delete.dialog.title": "Delete KeyManager ?", @@ -311,11 +319,15 @@ "Base.RouteMenuMapping.custom.throttling.policies.items.Editing": "Edit Custom Policy", "Base.RouteMenuMapping.dashboard": "Dashboard", "Base.RouteMenuMapping.gateways": "Gateways", + "Base.RouteMenuMapping.governance": "Governance", + "Base.RouteMenuMapping.governance.policies": "Policies", "Base.RouteMenuMapping.keymanagers": "Key Managers", "Base.RouteMenuMapping.keymanagers.items.Adding": "Add Key Manager", "Base.RouteMenuMapping.keymanagers.items.Editing": "Edit Key Manager", + "Base.RouteMenuMapping.overview": "Overview", "Base.RouteMenuMapping.organizations": "Organizations", "Base.RouteMenuMapping.role.permissions": "Scope Assignments", + "Base.RouteMenuMapping.ruleset.catalog": "Ruleset Catalog", "Base.RouteMenuMapping.settings": "Settings", "Base.RouteMenuMapping.subscription.creation": "Subscription Creation", "Base.RouteMenuMapping.subscription.deletion": "Subscription Deletion", @@ -412,6 +424,178 @@ "GatewayEnvironments.AddEditVhost.httpsPort": "HTTPS Port", "GatewayEnvironments.AddEditVhost.wsPort": "WS Port", "GatewayEnvironments.AddEditVhost.wssPort": "WSS Port", + "Governance.Overview.APICompliance.PolicyAdherence.column.policy": "Policy", + "Governance.Overview.APICompliance.PolicyAdherence.column.rulesets": "Rulesets", + "Governance.Overview.APICompliance.PolicyAdherence.column.status": "Status", + "Governance.Overview.APICompliance.PolicyAdherence.empty.helper": "No governance policies have been applied to this API.", + "Governance.Overview.APICompliance.PolicyAdherence.empty.title": "No Policies Applied", + "Governance.Overview.APICompliance.PolicyAdherence.followed.count": "{followed}/{total} Followed", + "Governance.Overview.APICompliance.RuleViolation.column.description": "Description", + "Governance.Overview.APICompliance.RuleViolation.column.message": "Message", + "Governance.Overview.APICompliance.RuleViolation.column.path": "Path", + "Governance.Overview.APICompliance.RuleViolation.column.rule": "Rule", + "Governance.Overview.APICompliance.RuleViolation.empty.errors": "No Error violations found", + "Governance.Overview.APICompliance.RuleViolation.empty.info": "No Info violations found", + "Governance.Overview.APICompliance.RuleViolation.empty.passed": "No Passed rules found", + "Governance.Overview.APICompliance.RuleViolation.empty.warnings": "No Warning violations found", + "Governance.Overview.APICompliance.RuleViolation.tab.errors": "Errors ({count})", + "Governance.Overview.APICompliance.RuleViolation.tab.info": "Info ({count})", + "Governance.Overview.APICompliance.RuleViolation.tab.passed": "Passed ({count})", + "Governance.Overview.APICompliance.RuleViolation.tab.warnings": "Warnings ({count})", + "Governance.Overview.APICompliance.RulesetAdherence.column.ruleset": "Ruleset", + "Governance.Overview.APICompliance.RulesetAdherence.column.status": "Status", + "Governance.Overview.APICompliance.RulesetAdherence.column.violations": "Violations", + "Governance.Overview.APICompliance.RulesetAdherence.empty.helper": "No governance rulesets have been applied for this API.", + "Governance.Overview.APICompliance.RulesetAdherence.empty.title": "No Rulesets Found", + "Governance.Overview.APICompliance.RulesetAdherence.violations.tooltip": "Errors: {error}, Warnings: {warn}, Info: {info}", + "Governance.Overview.APICompliance.column.api": "API", + "Governance.Overview.APICompliance.column.policies": "Policies", + "Governance.Overview.APICompliance.column.status": "Status", + "Governance.Overview.APICompliance.empty.content": "No APIs Available", + "Governance.Overview.APICompliance.empty.helper": "Create APIs to start evaluating their compliance.", + "Governance.Overview.APICompliance.followed.count": "{followed}/{total} Followed", + "Governance.Overview.APICompliance.no.policies": "N/A - No policies to evaluate", + "Governance.Overview.Compliance.back.to.overview": "Back to Overview", + "Governance.Overview.Compliance.failed": "Failed", + "Governance.Overview.Compliance.passed": "Passed", + "Governance.Overview.Compliance.policy.adherence.summary": "Policy Adherence Summary", + "Governance.Overview.Compliance.ruleset.adherence": "Ruleset Adherence", + "Governance.Overview.Compliance.ruleset.adherence.summary": "Ruleset Adherence Summary", + "Governance.Overview.Compliance.title": "Compliance Summary - {artifactName}", + "Governance.Overview.PolicyAdherence.column.apis": "APIs", + "Governance.Overview.PolicyAdherence.column.policy": "Policy", + "Governance.Overview.PolicyAdherence.column.status": "Status", + "Governance.Overview.PolicyAdherence.compliant.count": "{followed}/{total} Compliant", + "Governance.Overview.PolicyAdherence.empty.content": "No Governance Policies Available", + "Governance.Overview.PolicyAdherence.empty.helper": "Create a new governance policy to start governing the APIs.", + "Governance.Overview.PolicyAdherence.no.apis": "N/A - No APIs to evaluate", + "Governance.Overview.Summary.api.compliance": "API Compliance", + "Governance.Overview.Summary.api.compliance.details": "API Compliance Details", + "Governance.Overview.Summary.api.compliant": "Compliant ({count})", + "Governance.Overview.Summary.api.non.compliant": "Non-Compliant ({count})", + "Governance.Overview.Summary.api.not.applicable": "Not Applicable ({count})", + "Governance.Overview.Summary.policy.adherence": "Policy Adherence", + "Governance.Overview.Summary.policy.adherence.details": "Policy Adherence Details", + "Governance.Overview.Summary.policy.followed": "Followed ({count})", + "Governance.Overview.Summary.policy.not.applied": "Not Applied ({count})", + "Governance.Overview.Summary.policy.violated": "Violated ({count})", + "Governance.Overview.title": "Overview", + "Governance.Policies.AddEdit.action.actions": "Actions", + "Governance.Policies.AddEdit.action.add": "Add Action Configuration", + "Governance.Policies.AddEdit.action.block": "Block", + "Governance.Policies.AddEdit.action.cancel": "Cancel", + "Governance.Policies.AddEdit.action.config.title": "Action Configuration", + "Governance.Policies.AddEdit.action.governedState": "Governed State", + "Governance.Policies.AddEdit.action.notify": "Notify", + "Governance.Policies.AddEdit.action.save": "Save", + "Governance.Policies.AddEdit.action.severity.levels": "Severity Levels", + "Governance.Policies.AddEdit.action.table.actions": "Actions", + "Governance.Policies.AddEdit.action.table.onError": "On Error", + "Governance.Policies.AddEdit.action.table.onInfo": "On Info", + "Governance.Policies.AddEdit.action.table.onWarn": "On Warn", + "Governance.Policies.AddEdit.action.table.state": "State", + "Governance.Policies.AddEdit.add.success": "Policy Added Successfully", + "Governance.Policies.AddEdit.edit.success": "Policy Updated Successfully", + "Governance.Policies.AddEdit.enforcement.description": "Provide details of when the policy will be applied", + "Governance.Policies.AddEdit.enforcement.title": "Enforcement Details", + "Governance.Policies.AddEdit.error.loading.labels": "Error loading labels", + "Governance.Policies.AddEdit.error.loading.rulesets": "Error loading rulesets", + "Governance.Policies.AddEdit.form.actions.invalid": "Actions must be properly configured", + "Governance.Policies.AddEdit.form.actions.invalid.block": "BLOCK action is not allowed for API_CREATE and API_UPDATE states", + "Governance.Policies.AddEdit.form.add.btn": "Create", + "Governance.Policies.AddEdit.form.cancel": "Cancel", + "Governance.Policies.AddEdit.form.description": "Description", + "Governance.Policies.AddEdit.form.description.help": "Description of the governance policy.", + "Governance.Policies.AddEdit.form.description.too.long": "Description cannot exceed 1024 characters", + "Governance.Policies.AddEdit.form.has.errors": "One or more fields contain errors.", + "Governance.Policies.AddEdit.form.labels.required": "At least one label is required when applying to specific APIs", + "Governance.Policies.AddEdit.form.name": "Name", + "Governance.Policies.AddEdit.form.name.help": "Name of the governance policy.", + "Governance.Policies.AddEdit.form.name.invalid": "Policy name can only contain alphanumeric characters, hyphens, underscores, and spaces", + "Governance.Policies.AddEdit.form.name.required": "Policy name is required", + "Governance.Policies.AddEdit.form.name.too.long": "Policy name cannot exceed 255 characters", + "Governance.Policies.AddEdit.form.name.too.short": "Policy name cannot be empty", + "Governance.Policies.AddEdit.form.rulesets.duplicate": "Duplicate rulesets are not allowed", + "Governance.Policies.AddEdit.form.rulesets.required": "At least one ruleset is required", + "Governance.Policies.AddEdit.form.update.btn": "Update", + "Governance.Policies.AddEdit.general.details": "General Details", + "Governance.Policies.AddEdit.general.details.description": "Provide name and description of the policy.", + "Governance.Policies.AddEdit.labels.applyAll": "Apply to all APIs", + "Governance.Policies.AddEdit.labels.applyNone": "Apply to none", + "Governance.Policies.AddEdit.labels.applySpecific": "Apply to APIs with specific labels", + "Governance.Policies.AddEdit.labels.description": "Choose whether to apply this policy to all APIs or only to APIs with specific labels", + "Governance.Policies.AddEdit.labels.helper": "Select one or more labels to determine which APIs this policy applies to", + "Governance.Policies.AddEdit.labels.input": "Select Labels", + "Governance.Policies.AddEdit.labels.title": "Applicability", + "Governance.Policies.AddEdit.rulesets.description": "Search and select rulesets to include in the policy. Selected rulesets will appear above the search bar.", + "Governance.Policies.AddEdit.rulesets.empty": "No rulesets available", + "Governance.Policies.AddEdit.rulesets.noSearchResults": "No rulesets found matching your search", + "Governance.Policies.AddEdit.rulesets.title": "Rulesets", + "Governance.Policies.AddEdit.title.edit": "Governance Policy - Edit", + "Governance.Policies.AddEdit.title.new": "Governance Policy - Create new", + "Governance.Policies.List.add.new.policy": "Create Policy", + "Governance.Policies.List.addPolicy.title": "Create Governance Policy", + "Governance.Policies.List.addPolicy.triggerButtonText": "Create Governance Policy", + "Governance.Policies.List.column.appliesTo": "Applies to", + "Governance.Policies.List.column.appliesWhen": "Applies when", + "Governance.Policies.List.column.policy": "Policy", + "Governance.Policies.List.description": "Create governance policies using rulesets from the catalog to standardize and regulate your APls effectively", + "Governance.Policies.List.edit.title": "Edit Policy", + "Governance.Policies.List.empty.content": "Governance policies help you enforce standards and compliance across your APIs. Click the Create button to add your first policy.", + "Governance.Policies.List.empty.title": "Governance Policies", + "Governance.Policies.List.search.default": "Search policies by name or label", + "Governance.Policies.List.title": "Governance Policies", + "Governance.Rulesets.AddEdit.add.success": "Ruleset Added Successfully", + "Governance.Rulesets.AddEdit.button.create": "Create", + "Governance.Rulesets.AddEdit.button.update": "Update", + "Governance.Rulesets.AddEdit.button.upload": "Upload File", + "Governance.Rulesets.AddEdit.confirm.overwrite.cancel": "Cancel", + "Governance.Rulesets.AddEdit.confirm.overwrite.message": "The editor contains existing content. Do you want to overwrite it with the uploaded file?", + "Governance.Rulesets.AddEdit.confirm.overwrite.ok": "Overwrite", + "Governance.Rulesets.AddEdit.confirm.overwrite.title": "Confirm Overwrite", + "Governance.Rulesets.AddEdit.content.description": "Define the ruleset content in YAML or JSON format", + "Governance.Rulesets.AddEdit.content.title": "Ruleset Content", + "Governance.Rulesets.AddEdit.edit.success": "Ruleset Updated Successfully", + "Governance.Rulesets.AddEdit.error.loading": "Error loading ruleset", + "Governance.Rulesets.AddEdit.file.read.error": "Error reading file", + "Governance.Rulesets.AddEdit.form.artifact.type": "Artifact Type", + "Governance.Rulesets.AddEdit.form.artifacttype.required": "Artifact type is required", + "Governance.Rulesets.AddEdit.form.cancel": "Cancel", + "Governance.Rulesets.AddEdit.form.description": "Description", + "Governance.Rulesets.AddEdit.form.description.too.long": "Description cannot exceed 1000 characters", + "Governance.Rulesets.AddEdit.form.doclink.invalid": "Documentation link must be a valid HTTP/HTTPS URL", + "Governance.Rulesets.AddEdit.form.doclink.too.long": "Documentation link cannot exceed 500 characters", + "Governance.Rulesets.AddEdit.form.documentation": "Documentation Link", + "Governance.Rulesets.AddEdit.form.has.errors": "One or more fields contain errors.", + "Governance.Rulesets.AddEdit.form.name": "Name", + "Governance.Rulesets.AddEdit.form.name.invalid": "Ruleset name can only contain alphanumeric characters, spaces, hyphens and underscores.", + "Governance.Rulesets.AddEdit.form.name.required": "Ruleset name is required", + "Governance.Rulesets.AddEdit.form.name.too.long": "Ruleset name cannot exceed 255 characters", + "Governance.Rulesets.AddEdit.form.ruleset.type": "Ruleset Type", + "Governance.Rulesets.AddEdit.form.rulesetcontent.required": "Ruleset content is required", + "Governance.Rulesets.AddEdit.form.ruletype.required": "Rule type is required", + "Governance.Rulesets.AddEdit.general.details": "General Details", + "Governance.Rulesets.AddEdit.general.details.description": "Provide name and description of the ruleset.", + "Governance.Rulesets.AddEdit.title.edit": "Edit Ruleset - {name}", + "Governance.Rulesets.AddEdit.title.new": "Create New Ruleset", + "Governance.Rulesets.Create.genai": "Create with GenAI", + "Governance.Rulesets.Create.genai.desc": "Use AI to help you create a ruleset", + "Governance.Rulesets.Create.options": "Choose Creation Method", + "Governance.Rulesets.Create.scratch": "Create from Scratch", + "Governance.Rulesets.Create.scratch.desc": "Create a ruleset manually", + "Governance.Rulesets.List.add.new.ruleset": "Create Ruleset", + "Governance.Rulesets.List.addRuleset.title": "Create Ruleset", + "Governance.Rulesets.List.addRuleset.triggerButtonText": "Create Ruleset", + "Governance.Rulesets.List.column.artifactType": "Artifact Type", + "Governance.Rulesets.List.column.provider": "Provider", + "Governance.Rulesets.List.column.ruleset": "Ruleset", + "Governance.Rulesets.List.column.rulesetType": "Ruleset Type", + "Governance.Rulesets.List.description": "Find comprehensive governance rulesets designed to ensure the consistency, security and reliability for your APls", + "Governance.Rulesets.List.edit.title": "Edit Ruleset", + "Governance.Rulesets.List.empty.content": "Rulesets are the building blocks for creating governance policies. They contain predefined rules and validations that can be used to enforce standards across your APIs. Click Create Ruleset to get started.", + "Governance.Rulesets.List.empty.title": "Ruleset Catalog", + "Governance.Rulesets.List.search.placeholder": "Search rulesets by name or type", + "Governance.Rulesets.List.title": "Ruleset Catalog", "KeyManager.AddEdit.Invalid.Roles.Found": "Invalid Role(s) Found", "KeyManager.AddEdit.roles.help": "Enter a valid role and press `Enter`", "KeyManager.AddEditKeyManager.permissions.add.description": "Permissions for the Key Manager", diff --git a/portals/admin/src/main/webapp/source/dev/auth_login.js b/portals/admin/src/main/webapp/source/dev/auth_login.js index 2885d3a1c34..aaf543a4bcd 100644 --- a/portals/admin/src/main/webapp/source/dev/auth_login.js +++ b/portals/admin/src/main/webapp/source/dev/auth_login.js @@ -153,6 +153,13 @@ function devServerBefore(app) { maxAge, }); + res.cookie('AM_ACC_TOKEN_DEFAULT_P2', accessTokenPart2, { + path: '/api/am/governance/', + httpOnly: true, + secure: true, + maxAge, + }); + res.cookie('AM_ACC_TOKEN_DEFAULT_P2', accessTokenPart2, { path: '/api/am/service-catalog/v1/', httpOnly: true, diff --git a/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/ListBase.jsx b/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/ListBase.jsx index 38ce3db667b..878b0c8f1b6 100644 --- a/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/ListBase.jsx +++ b/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/ListBase.jsx @@ -56,6 +56,7 @@ function ListBase(props) { addedActions, enableCollapsable, renderExpandableRow, + useContentBase, } = props; const [searchText, setSearchText] = useState(''); @@ -223,6 +224,7 @@ function ListBase(props) { customToolbar: null, responsive: 'vertical', searchText, + rowsPerPageOptions: [5, 10, 25, 50, 100], onColumnSortChange, textLabels: { body: { @@ -244,137 +246,137 @@ function ListBase(props) { }, expandableRows: enableCollapsable, renderExpandableRow, + ...props.options, }; // If no apiCall is provided OR, // retrieved data is empty, display an information card. if (!apiCall || (data && data.length === 0)) { - return ( - - - - {emptyBoxTitle} - {emptyBoxContent} - - - {addButtonOverride || ( - EditComponent && () - )} - - - + const content = ( + + + {emptyBoxTitle} + {emptyBoxContent} + + + {addButtonOverride || ( + EditComponent && () + )} + + ); + + return useContentBase ? ( + {content} + ) : content; } // If apiCall is provided and data is not retrieved yet, display progress component if (!error && apiCall && !data) { - return ( - - - - - ); + const content = ; + return useContentBase ? ( + {content} + ) : content; } - if (error) { - return ( - - {error} - - ); + if (error) { + const content = {error}; + return useContentBase ? ( + {content} + ) : content; } - return ( + + const mainContent = ( <> - - {(searchActive || addButtonProps) && ( - - - + {(searchActive || addButtonProps) && ( + + + - - {searchActive && ()} - - - {searchActive && ( - ({ - '& .search-input': { - fontSize: theme.typography.fontSize, - }, - })} - InputProps={{ - disableUnderline: true, - className: 'search-input', - }} - // eslint-disable-next-line react/jsx-no-duplicate-props - inputProps={{ - 'aria-label': 'search-by-policy', - }} - onChange={filterData} - value={searchText} + + {searchActive && ()} + + + {searchActive && ( + ({ + '& .search-input': { + fontSize: theme.typography.fontSize, + }, + })} + InputProps={{ + disableUnderline: true, + className: 'search-input', + }} + // eslint-disable-next-line react/jsx-no-duplicate-props + inputProps={{ + 'aria-label': 'search-by-policy', + }} + onChange={filterData} + value={searchText} + /> + )} + + + {addButtonOverride || ( + EditComponent && ( + - )} - - - {addButtonOverride || ( - EditComponent && ( - - ) - )} - + )} + > + + - )} - > - - - - - + + - - + + + + )} +
+ {data && data.length > 0 && ( + )} +
+ {data && data.length === 0 && (
- {data && data.length > 0 && ( - - )} + + {noDataMessage} +
- {data && data.length === 0 && ( -
- - {noDataMessage} - -
- )} -
+ )} ); + + return useContentBase ? ( + {mainContent} + ) : mainContent; } ListBase.defaultProps = { @@ -404,7 +406,10 @@ ListBase.defaultProps = { columProps: null, enableCollapsable: false, renderExpandableRow: null, + useContentBase: true, + options: {}, }; + ListBase.propTypes = { EditComponent: PropTypes.element, editComponentProps: PropTypes.shape({}), @@ -432,5 +437,7 @@ ListBase.propTypes = { addedActions: PropTypes.shape([]), enableCollapsable: PropTypes.bool, renderExpandableRow: PropTypes.func, + useContentBase: PropTypes.bool, + options: PropTypes.shape({}), }; export default ListBase; diff --git a/portals/admin/src/main/webapp/source/src/app/components/Base/RouteMenuMapping.jsx b/portals/admin/src/main/webapp/source/src/app/components/Base/RouteMenuMapping.jsx index 71345b0f446..5c94688d981 100644 --- a/portals/admin/src/main/webapp/source/src/app/components/Base/RouteMenuMapping.jsx +++ b/portals/admin/src/main/webapp/source/src/app/components/Base/RouteMenuMapping.jsx @@ -38,12 +38,17 @@ import KeyManagers from 'AppComponents/KeyManagers'; import AiVendors from 'AppComponents/AiVendors'; import ListRoles from 'AppComponents//RolePermissions/ListRoles.jsx'; import TenantConfSave from 'AppComponents/AdvancedSettings/TenantConfSave'; +import Policies from 'AppComponents/Governance/Policies'; +import RulesetCatalog from 'AppComponents/Governance/RulesetCatalog'; +import Overview from 'AppComponents/Governance/Overview'; import BusinessIcon from '@mui/icons-material/Business'; import Organizations from 'AppComponents/Organizations/ListOrganizations'; import GamesIcon from '@mui/icons-material/Games'; import CategoryIcon from '@mui/icons-material/Category'; import PolicyIcon from '@mui/icons-material/Policy'; +import RuleIcon from '@mui/icons-material/Rule'; +import BarChartIcon from '@mui/icons-material/BarChart'; import BlockIcon from '@mui/icons-material/Block'; import AssignmentIcon from '@mui/icons-material/Assignment'; import ApplicationCreation from 'AppComponents/Workflow/ApplicationCreation'; @@ -275,6 +280,45 @@ const RouteMenuMapping = (intl) => [ }, ], }, + { + id: 'Governance', + displayText: intl.formatMessage({ + id: 'Base.RouteMenuMapping.governance', + defaultMessage: 'Governance', + }), + children: [ + { + id: 'Overview', + displayText: intl.formatMessage({ + id: 'Base.RouteMenuMapping.overview', + defaultMessage: 'Overview', + }), + path: '/governance/overview', + component: Overview, + icon: , + }, + { + id: 'Policies', + displayText: intl.formatMessage({ + id: 'Base.RouteMenuMapping.governance.policies', + defaultMessage: 'Policies', + }), + path: '/governance/policies', + component: Policies, + icon: , + }, + { + id: 'Ruleset Catalog', + displayText: intl.formatMessage({ + id: 'Base.RouteMenuMapping.ruleset.catalog', + defaultMessage: 'Ruleset Catalog', + }), + path: '/governance/ruleset-catalog', + component: RulesetCatalog, + icon: , + }, + ], + }, { id: 'Tasks', displayText: intl.formatMessage({ diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/Compliance.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/Compliance.jsx new file mode 100644 index 00000000000..66724e66d6e --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/Compliance.jsx @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useState } from 'react'; +import ContentBase from 'AppComponents/AdminPages/Addons/ContentBase'; +import { Link as RouterLink } from 'react-router-dom'; +import { + Grid, Card, CardContent, Typography, +} from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { Box } from '@mui/system'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import { FormattedMessage, useIntl } from 'react-intl'; +import DonutChart from 'AppComponents/Shared/DonutChart'; +import RuleViolationSummary from './RuleViolationSummary'; +import RulesetAdherenceSummaryTable from './RulesetAdherenceSummaryTable'; +import PolicyAdherenceSummaryTable from './PolicyAdherenceSummaryTable'; + +export default function Compliance(props) { + const intl = useIntl(); + const { match: { params: { id: artifactId } } } = props; + const [statusCounts, setStatusCounts] = useState({ passed: 0, failed: 0 }); + const [artifactName, setArtifactName] = useState(''); + + useEffect(() => { + const abortController = new AbortController(); + const restApi = new GovernanceAPI(); + + restApi.getComplianceByAPIId(artifactId, { signal: abortController.signal }) + .then((response) => { + setArtifactName(response.body.info.name); + const rulesetMap = new Map(); + + response.body.governedPolicies.forEach((policy) => { + policy.rulesetValidationResults.forEach((result) => { + // If ruleset not in map or if existing result is older, update the map + if (!rulesetMap.has(result.id)) { + rulesetMap.set(result.id, result); + } + }); + }); + + // Count statuses from unique rulesets + const counts = Array.from(rulesetMap.values()).reduce((acc, result) => { + if (result.status === 'PASSED') acc.passed += 1; + if (result.status === 'FAILED') acc.failed += 1; + return acc; + }, { passed: 0, failed: 0 }); + + setStatusCounts(counts); + }) + .catch((error) => { + if (!abortController.signal.aborted) { + console.error('Error fetching ruleset adherence data:', error); + setStatusCounts({ passed: 0, failed: 0 }); + setArtifactName(''); + } + }); + + return () => { + abortController.abort(); + }; + }, [artifactId]); + + return ( + + )} + pageStyle='paperLess' + > + + + + + + + + + {/* Rule Violation Summary section */} + + + + + + + + + {/* Policy Adherence Summary section */} + + + + + + + + + + + + {/* Ruleset Adherence Summary section */} + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/PolicyAdherenceSummaryTable.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/PolicyAdherenceSummaryTable.jsx new file mode 100644 index 00000000000..f910e110a92 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/PolicyAdherenceSummaryTable.jsx @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { + Typography, Chip, Box, LinearProgress, TableRow, TableCell, +} from '@mui/material'; +import ListBase from 'AppComponents/AdminPages/Addons/ListBase'; +import Stack from '@mui/material/Stack'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CancelIcon from '@mui/icons-material/Cancel'; +import { useIntl } from 'react-intl'; +import PolicyIcon from '@mui/icons-material/Policy'; + +import GovernanceAPI from 'AppData/GovernanceAPI'; +import Utils from 'AppData/Utils'; + +export default function PolicyAdherenceSummaryTable({ artifactId }) { + const intl = useIntl(); + + /** + * API call to get Policies + * @returns {Promise}. + */ + function apiCall() { + const restApi = new GovernanceAPI(); + return restApi + .getComplianceByAPIId(artifactId) + .then((result) => { + return result.body.governedPolicies; + }) + .catch((error) => { + throw error; + }); + } + + const renderProgress = (followed, total) => { + const percentage = (followed / total) * 100; + const isComplete = followed === total; + + return ( + + + + {intl.formatMessage({ + id: 'Governance.Overview.APICompliance.PolicyAdherence.followed.count', + defaultMessage: '{followed}/{total} Followed', + }, { followed, total })} + + + + + ); + }; + + const renderExpandableRow = (rowData) => { + const rulesets = rowData[3]; + return ( + + + + + {rulesets.map((ruleset) => ( + + {ruleset.status === 'PASSED' + ? + : } + + {ruleset.name} + + + ))} + + + + ); + }; + + const policyColumProps = [ + { + name: 'id', + options: { display: false }, + }, + { + name: 'name', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.PolicyAdherence.column.policy', + defaultMessage: 'Policy', + }), + options: { + width: '30%', + customBodyRender: (value) => ( + {value} + ), + setCellProps: () => ({ + style: { width: '30%' }, + }), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small', + }, + }, + }), + }, + }, + { + name: 'status', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.PolicyAdherence.column.status', + defaultMessage: 'Status', + }), + options: { + setCellProps: () => ({ + style: { width: '20%' }, + }), + customBodyRender: (value) => { + const getChipColor = (status) => { + if (status === 'FOLLOWED') return 'success'; + if (status === 'VIOLATED') return 'error'; + return 'default'; + }; + return ( + + ); + }, + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small', + }, + }, + }), + }, + }, + { + name: 'rulesetValidationResults', + options: { display: false }, + }, + { + name: 'rulesetsList', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.PolicyAdherence.column.rulesets', + defaultMessage: 'Rulesets', + }), + options: { + customBodyRender: (value, tableMeta) => { + const rulesets = tableMeta.rowData[3]; + const total = rulesets.length; + const followed = rulesets.filter((ruleset) => ruleset.status === 'PASSED').length; + return renderProgress(followed, total); + }, + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small', + }, + }, + }), + }, + }, + ]; + + const emptyStateContent = ( + + + + {intl.formatMessage({ + id: 'Governance.Overview.APICompliance.PolicyAdherence.empty.title', + defaultMessage: 'No Policies Applied', + })} + + + {intl.formatMessage({ + id: 'Governance.Overview.APICompliance.PolicyAdherence.empty.helper', + defaultMessage: 'No governance policies have been applied to this API.', + })} + + + ); + + return ( + + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/RuleViolationSummary.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/RuleViolationSummary.jsx new file mode 100644 index 00000000000..dc05c57748b --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/RuleViolationSummary.jsx @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { + Grid, Card, CardContent, Typography, Box, Tabs, Tab, Collapse, IconButton, +} from '@mui/material'; +import ReportIcon from '@mui/icons-material/Report'; +import WarningIcon from '@mui/icons-material/Warning'; +import InfoIcon from '@mui/icons-material/Info'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import LabelIcon from '@mui/icons-material/Label'; +import RuleIcon from '@mui/icons-material/Rule'; +import ListBase from 'AppComponents/AdminPages/Addons/ListBase'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import { useIntl } from 'react-intl'; + +// TODO: Improve the component +export default function RuleViolationSummary({ artifactId }) { + const intl = useIntl(); + const [selectedTab, setSelectedTab] = React.useState(0); + const [expandedItems, setExpandedItems] = React.useState([]); + + // TODO: Optimize + simplify + const apiCall = () => { + const restApi = new GovernanceAPI(); + return restApi.getComplianceByAPIId(artifactId) + .then((response) => { + // Get unique ruleset IDs from all policies + const rulesetIds = [...new Set( + response.body.governedPolicies.flatMap( + (policy) => policy.rulesetValidationResults.map((result) => result.id), + ), + )]; + + // Get validation results for each ruleset + return Promise.all( + rulesetIds.map((rulesetId) => restApi.getRulesetValidationResultsByAPIId(artifactId, rulesetId) + .then((result) => result.body)), + ).then((rulesets) => { + // Create rulesets array with severities catagorized + const rulesetCategories = rulesets.map((ruleset) => ({ + id: ruleset.id, + rulesetName: ruleset.name, + error: ruleset.violatedRules.filter((rule) => rule.severity === 'ERROR'), + warn: ruleset.violatedRules.filter((rule) => rule.severity === 'WARN'), + info: ruleset.violatedRules.filter((rule) => rule.severity === 'INFO'), + passed: ruleset.followedRules, + })); + + // Group by severity level + const severityGroups = { + errors: [], + warnings: [], + info: [], + passed: [], + }; + + rulesetCategories.forEach((ruleset) => { + if (ruleset.error.length > 0) { + severityGroups.errors.push({ + id: ruleset.id, + rulesetName: ruleset.rulesetName, + // tag: ruleset.tag, + rules: ruleset.error, + }); + } + if (ruleset.warn.length > 0) { + severityGroups.warnings.push({ + id: ruleset.id, + rulesetName: ruleset.rulesetName, + // tag: ruleset.tag, + rules: ruleset.warn, + }); + } + if (ruleset.info.length > 0) { + severityGroups.info.push({ + id: ruleset.id, + rulesetName: ruleset.rulesetName, + // tag: ruleset.tag, + rules: ruleset.info, + }); + } + if (ruleset.passed.length > 0) { + severityGroups.passed.push({ + id: ruleset.id, + rulesetName: ruleset.rulesetName, + // tag: ruleset.tag, + rules: ruleset.passed, + }); + } + }); + + return severityGroups; + }); + }) + .catch((error) => { + console.error('Error fetching ruleset adherence data:', error); + return { + errors: [], + warnings: [], + info: [], + passed: [], + }; + }); + }; + + // Remove the mock complianceData and use state instead + const [complianceData, setComplianceData] = React.useState({ + errors: [], + warnings: [], + info: [], + passed: [], + }); + + React.useEffect(() => { + apiCall().then(setComplianceData); + }, [artifactId]); + + const handleTabChange = (e, newValue) => { + setSelectedTab(newValue); + setExpandedItems([]); // Reset expanded items when tab changes + }; + + const handleExpandClick = (id) => { + setExpandedItems((prev) => { + const isExpanded = prev.includes(id); + return isExpanded + ? prev.filter((i) => i !== id) + : [...prev, id]; + }); + }; + + const getRuleData = (rules) => { + return Promise.resolve( + rules.map((rule) => [rule.name, rule.violatedPath, rule.message]), + ); + }; + + // Add new function for passed rules data + const getPassedRuleData = (rules) => { + return Promise.resolve( + rules.map((rule) => [rule.name, rule.description]), + ); + }; + + const ruleColumProps = [ + { + name: 'name', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.column.rule', + defaultMessage: 'Rule', + }), + options: { + customBodyRender: (value) => ( + {value} + ), + }, + }, + { + name: 'violatedPath', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.column.path', + defaultMessage: 'Path', + }), + options: { + customBodyRender: (value) => ( + {value.path} + ), + }, + }, + { + name: 'message', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.column.message', + defaultMessage: 'Message', + }), + options: { + customBodyRender: (value) => ( + {value} + ), + }, + }, + ]; + + // Add new column props for passed rules + const passedRuleColumnProps = [ + { + name: 'name', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.column.rule', + defaultMessage: 'Rule', + }), + options: { + customBodyRender: (value) => ( + {value} + ), + }, + }, + { + name: 'description', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.column.description', + defaultMessage: 'Description', + }), + options: { + customBodyRender: (value) => ( + {value} + ), + }, + }, + ]; + + const renderComplianceCards = (rulesets, isPassed = false) => { + return ( + <> + + {rulesets.map((item) => ( + + + + + + + + {/* {item.provider} / */} + {item.rulesetName} + {' '} + ( + {item.rules.length} + ) + + {/* */} + + handleExpandClick(item.id)} + aria-expanded={expandedItems.includes(item.id)} + aria-label='show more' + > + {expandedItems.includes(item.id) + ? : } + + + + + + (isPassed + ? getPassedRuleData(item.rules) : getRuleData(item.rules))} + searchProps={false} + addButtonProps={false} + showActionColumn={false} + useContentBase={false} + emptyBoxProps={{ + content: 'There are no rules to display', + }} + options={{ + elevation: 0, + setTableProps: () => ({ + size: 'small', + }), + rowsPerPage: 5, + }} + /> + + + + + ))} + + + ); + }; + + // Add this new function to calculate total rules + const getTotalRuleCount = (rulesets) => { + return rulesets.reduce((sum, ruleset) => sum + ruleset.rules.length, 0); + }; + + const renderEmptyContent = (message) => ( + + + + {message} + + + ); + + const getEmptyMessage = (tabIndex) => { + switch (tabIndex) { + case 0: + return intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.empty.errors', + defaultMessage: 'No Error violations found', + }); + case 1: + return intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.empty.warnings', + defaultMessage: 'No Warning violations found', + }); + case 2: + return intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.empty.info', + defaultMessage: 'No Info violations found', + }); + case 3: + return intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.empty.passed', + defaultMessage: 'No Passed rules found', + }); + default: + return ''; + } + }; + + return ( + <> + { + switch (selectedTab) { + case 0: + return theme.palette.error.main; + case 1: + return theme.palette.warning.main; + case 2: + return theme.palette.info.main; + case 3: + return theme.palette.success.main; + default: + return theme.palette.primary.main; + } + }, + }, + }, + }} + TabIndicatorProps={{ + sx: { + backgroundColor: (theme) => { + switch (selectedTab) { + case 0: + return theme.palette.error.main; + case 1: + return theme.palette.warning.main; + case 2: + return theme.palette.info.main; + case 3: + return theme.palette.success.main; + default: + return theme.palette.primary.main; + } + }, + }, + }} + > + } + iconPosition='start' + label={intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.tab.errors', + defaultMessage: 'Errors ({count})', + }, { count: getTotalRuleCount(complianceData.errors) })} + /> + } + iconPosition='start' + label={intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.tab.warnings', + defaultMessage: 'Warnings ({count})', + }, { count: getTotalRuleCount(complianceData.warnings) })} + /> + } + iconPosition='start' + label={intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.tab.info', + defaultMessage: 'Info ({count})', + }, { count: getTotalRuleCount(complianceData.info) })} + /> + } + iconPosition='start' + label={intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RuleViolation.tab.passed', + defaultMessage: 'Passed ({count})', + }, { count: getTotalRuleCount(complianceData.passed) })} + /> + + {selectedTab === 0 && ( + complianceData.errors.length > 0 + ? renderComplianceCards(complianceData.errors) + : renderEmptyContent(getEmptyMessage(0)) + )} + {selectedTab === 1 && ( + complianceData.warnings.length > 0 + ? renderComplianceCards(complianceData.warnings) + : renderEmptyContent(getEmptyMessage(1)) + )} + {selectedTab === 2 && ( + complianceData.info.length > 0 + ? renderComplianceCards(complianceData.info) + : renderEmptyContent(getEmptyMessage(2)) + )} + {selectedTab === 3 && ( + complianceData.passed.length > 0 + ? renderComplianceCards(complianceData.passed, true) + : renderEmptyContent(getEmptyMessage(3)) + )} + + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/RulesetAdherenceSummaryTable.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/RulesetAdherenceSummaryTable.jsx new file mode 100644 index 00000000000..c7cf1e9c7d9 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/APICompliance/RulesetAdherenceSummaryTable.jsx @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { + Typography, Chip, Box, Tooltip, +} from '@mui/material'; +import ListBase from 'AppComponents/AdminPages/Addons/ListBase'; +import ErrorIcon from '@mui/icons-material/Error'; +import WarningIcon from '@mui/icons-material/Warning'; +import InfoIcon from '@mui/icons-material/Info'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import { useIntl } from 'react-intl'; +import AssignmentIcon from '@mui/icons-material/Assignment'; +import Utils from 'AppData/Utils'; + +export default function RulesetAdherenceSummaryTable({ artifactId }) { + const intl = useIntl(); + + const apiCall = () => { + const restApi = new GovernanceAPI(); + return restApi.getComplianceByAPIId(artifactId) + .then((response) => { + // Get unique ruleset IDs from all policies + const rulesetIds = [...new Set( + response.body.governedPolicies.flatMap( + (policy) => policy.rulesetValidationResults.map((result) => result.id), + ), + )]; + + // Get validation results for each ruleset + return Promise.all( + rulesetIds.map((rulesetId) => restApi.getRulesetValidationResultsByAPIId(artifactId, rulesetId) + .then((result) => result.body)), + ); + }) + .catch((error) => { + console.error('Error fetching ruleset adherence data:', error); + return []; + }); + }; + + const renderComplianceIcons = (violations) => { + const { error, warn, info } = violations; + return ( + + + + + + {error} + + + + + + {warn} + + + + + + {info} + + + + + ); + }; + + const RulesetColumProps = [ + { + name: 'id', + options: { display: false }, + }, + { + name: 'name', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RulesetAdherence.column.ruleset', + defaultMessage: 'Ruleset', + }), + options: { + width: '40%', + customBodyRender: (value) => ( + {value} + ), + setCellProps: () => ({ + style: { width: '30%' }, + }), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small', + }, + }, + }), + }, + }, + { + name: 'status', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RulesetAdherence.column.status', + defaultMessage: 'Status', + }), + options: { + setCellProps: () => ({ + style: { width: '30%' }, + }), + customBodyRender: (value) => ( + + ), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small', + }, + }, + }), + }, + }, + { + name: 'violatedRules', + options: { display: false }, + }, + { + name: 'followedRules', + options: { display: false }, + }, + { + name: 'violationsSummary', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RulesetAdherence.column.violations', + defaultMessage: 'Violations', + }), + options: { + customBodyRender: (value, tableMeta) => { + // Count the number of errors, warnings, and info messages in the violations + const violations = tableMeta.rowData[3]; + const counts = violations.reduce((acc, { severity }) => { + acc[severity.toLowerCase()] += 1; + return acc; + }, { error: 0, warn: 0, info: 0 }); + + return renderComplianceIcons({ + error: counts.error, + warn: counts.warn, + info: counts.info, + }); + }, + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small', + }, + }, + }), + }, + }, + ]; + + const emptyStateContent = ( + + + + {intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RulesetAdherence.empty.title', + defaultMessage: 'No Rulesets Found', + })} + + + {intl.formatMessage({ + id: 'Governance.Overview.APICompliance.RulesetAdherence.empty.helper', + defaultMessage: 'No governance rulesets have been applied for this API.', + })} + + + ); + + return ( + + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/ApiComplianceTable.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/ApiComplianceTable.jsx new file mode 100644 index 00000000000..2b5f04c9a38 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/ApiComplianceTable.jsx @@ -0,0 +1,330 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Box, Chip, Typography, Tooltip, LinearProgress } from '@mui/material'; +import ListBase from 'AppComponents/AdminPages/Addons/ListBase'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import { Link as RouterLink } from 'react-router-dom'; +import ErrorIcon from '@mui/icons-material/Error'; +import WarningIcon from '@mui/icons-material/Warning'; +import InfoIcon from '@mui/icons-material/Info'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import { useIntl } from 'react-intl'; +import ApiIcon from '@mui/icons-material/Api'; +import Utils from 'AppData/Utils'; + +/** + * API call to get Policies + * @returns {Promise}. + */ +function apiCall() { + const restApi = new GovernanceAPI(); + return restApi + .getComplianceStatusListOfAPIs() + .then((result) => { + return result.body.list; + }) + .catch((error) => { + throw error; + }); +} + + +export default function ApiComplianceTable() { + const intl = useIntl(); + + const renderProgress = (followed, total) => { + if (total === 0) { + return ( + + {intl.formatMessage({ + id: 'Governance.Overview.APICompliance.no.policies', + defaultMessage: 'N/A - No policies to evaluate', + })} + + ); + } + + const percentage = (followed / total) * 100; + const isComplete = followed === total; + + return ( + + + + {intl.formatMessage({ + id: 'Governance.Overview.APICompliance.followed.count', + defaultMessage: '{followed}/{total} Followed', + }, { followed, total })} + + + + + ); + }; + + const renderComplianceIcons = (severityBasedRuleViolationSummary) => { + // get the error, warn, info counts + let errorCount = 0; + let warnCount = 0; + let infoCount = 0; + + severityBasedRuleViolationSummary.forEach((severity) => { + if (severity.severity === 'ERROR') { + errorCount = severity.violatedRulesCount; + } else if (severity.severity === 'WARN') { + warnCount = severity.violatedRulesCount; + } else if (severity.severity === 'INFO') { + infoCount = severity.violatedRulesCount; + } + }); + + return ( + + + + + + {errorCount || 0} + + + + + + {warnCount || 0} + + + + + + {infoCount || 0} + + + + + ); + }; + + const columProps = [ + { + name: 'id', + options: { display: false } + }, + { + name: 'info', + options: { display: false } + }, + { + name: 'name', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.column.api', + defaultMessage: 'API', + }), + options: { + customBodyRender: (value, tableMeta) => ( + + + {tableMeta.rowData[1].name} + + + + ), + setCellProps: () => ({ + style: { width: '30%' }, + }), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + { + name: 'status', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.column.status', + defaultMessage: 'Status', + }), + options: { + customBodyRender: (value) => ( + + ), + setCellProps: () => ({ + style: { width: '20%' }, + }), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + { + name: 'policyAdherenceSummary', + options: { display: false } + }, + { + name: 'severityBasedRuleViolationSummary', + options: { display: false } + }, + { + name: 'policies', + label: intl.formatMessage({ + id: 'Governance.Overview.APICompliance.column.policies', + defaultMessage: 'Policies', + }), + options: { + customBodyRender: (value, tableMeta) => { + const followed = tableMeta.rowData[4]?.followed || 0; + const violated = tableMeta.rowData[4]?.violated || 0; + const total = followed + violated; + return renderProgress(followed, total); + }, + setCellProps: () => ({ + style: { width: '40%' }, + }), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + { + name: 'compliance', + label: ' ', + options: { + customBodyRender: (value, tableMeta) => { + const severityBasedRuleViolationSummary = tableMeta.rowData[5] || []; + return renderComplianceIcons(severityBasedRuleViolationSummary); + }, + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + ]; + + const emptyStateContent = ( + + + + {intl.formatMessage({ + id: 'Governance.Overview.APICompliance.empty.content', + defaultMessage: 'No APIs Available', + })} + + + {intl.formatMessage({ + id: 'Governance.Overview.APICompliance.empty.helper', + defaultMessage: 'Create APIs to start evaluating their compliance.', + })} + + + ); + + return ( + value, + setTableProps: () => ({ + style: { + '& .MuiTableCell-root': { + border: 'none' + } + } + }) + }} + /> + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/PolicyAdherenceTable.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/PolicyAdherenceTable.jsx new file mode 100644 index 00000000000..49ac1987f07 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/PolicyAdherenceTable.jsx @@ -0,0 +1,319 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Box, Chip, Typography, TableRow, TableCell, LinearProgress } from '@mui/material'; +import ListBase from 'AppComponents/AdminPages/Addons/ListBase'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import { Link as RouterLink } from 'react-router-dom'; +import Stack from '@mui/material/Stack'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CancelIcon from '@mui/icons-material/Cancel'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import { useIntl } from 'react-intl'; +import PolicyIcon from '@mui/icons-material/Policy'; +import Utils from 'AppData/Utils'; + +/** + * API call to get Policies + * @returns {Promise}. + */ +function apiCall() { + const restApi = new GovernanceAPI(); + return restApi + .getPolicyAdherenceForAllPolicies() + .then((result) => { + const policies = result.body.list; + + // Fetch policy adherence details for each policy + // TODO: optimize + return Promise.all( + policies.map(async (policy) => { + try { + const adherenceDetails = await restApi.getPolicyAdherenceByPolicyId(policy.id); + return { + ...policy, + evaluatedArtifacts: adherenceDetails.body.evaluatedArtifacts || [] + }; + } catch (error) { + console.error(`Error fetching adherence for policy ${policy.id}:`, error); + return { + ...policy, + evaluatedArtifacts: [] + }; + } + }) + ); + }) + .catch((error) => { + throw error; + }); +} + +export default function PolicyAdherenceTable() { + const intl = useIntl(); + + // TODO: reuse this function in other components + const renderProgress = (followed, total) => { + if (total === 0) { + return ( + + {intl.formatMessage({ + id: 'Governance.Overview.PolicyAdherence.no.apis', + defaultMessage: 'N/A - No APIs to evaluate', + })} + + ); + } + + const percentage = (followed / total) * 100; + const isComplete = followed === total; + + return ( + + + + {intl.formatMessage({ + id: 'Governance.Overview.PolicyAdherence.compliant.count', + defaultMessage: '{followed}/{total} Compliant', + }, { followed, total })} + + + + + ); + }; + + const policyColumProps = [ + { + name: 'id', + options: { display: false } + }, + { + name: 'name', + label: intl.formatMessage({ + id: 'Governance.Overview.PolicyAdherence.column.policy', + defaultMessage: 'Policy', + }), + options: { + customBodyRender: (value, tableMeta) => ( + + + {value} + + + + ), + setCellProps: () => ({ + style: { + width: '30%' + }, + }), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + { + name: 'status', + label: intl.formatMessage({ + id: 'Governance.Overview.PolicyAdherence.column.status', + defaultMessage: 'Status', + }), + options: { + customBodyRender: (value) => ( + + ), + setCellProps: () => ({ + sx: { width: '20%' }, + }), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + { + name: 'artifactComplianceSummary', + options: { display: false } + }, + { + name: 'evaluatedArtifacts', + options: { display: false } + }, + { + name: 'progress', + label: intl.formatMessage({ + id: 'Governance.Overview.PolicyAdherence.column.apis', + defaultMessage: 'APIs', + }), + options: { + customBodyRender: (value, tableMeta) => { + const followed = tableMeta.rowData[3]?.compliant || 0; + const total = (tableMeta.rowData[3]?.nonCompliant + followed) || 0; + return renderProgress(followed, total); + }, + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + ]; + + const renderExpandableRow = (rowData) => { + return ( + + + + + {/* TODO: Find a better way to display all those */} + {rowData[4].map((artifact) => ( + + {artifact.status === 'COMPLIANT' ? + : + + } + + {artifact.info.name} + + + + ))} + + + + ); + }; + + const emptyStateContent = ( + + + + {intl.formatMessage({ + id: 'Governance.Overview.PolicyAdherence.empty.content', + defaultMessage: 'No Governance Policies Available', + })} + + + {intl.formatMessage({ + id: 'Governance.Overview.PolicyAdherence.empty.helper', + defaultMessage: 'Create a new governance policy to start governing the APIs.', + })} + + + ); + + return ( + + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/Summary.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/Summary.jsx new file mode 100644 index 00000000000..132773484dc --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/Summary.jsx @@ -0,0 +1,210 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { useIntl } from 'react-intl'; +import ContentBase from 'AppComponents/AdminPages/Addons/ContentBase'; +import { Grid, Card, CardContent, Typography } from '@mui/material'; +import DonutChart from 'AppComponents/Shared/DonutChart'; +import ApiComplianceTable from './ApiComplianceTable'; +import PolicyAdherenceTable from './PolicyAdherenceTable'; +import GovernanceAPI from 'AppData/GovernanceAPI'; + +export default function Summary() { + const intl = useIntl(); + const [policyAdherence, setPolicyAdherence] = useState({ + followedPolicies: 0, + violatedPolicies: 0, + unAppliedPolicies: 0 + }); + const [apiCompliance, setApiCompliance] = useState({ + compliantArtifacts: 0, + nonCompliantArtifacts: 0, + notApplicableArtifacts: 0 + }); + + useEffect(() => { + const restApi = new GovernanceAPI(); + + Promise.all([ + restApi.getPolicyAdherenceSummary(), + restApi.getComplianceSummaryForAPIs() + ]) + .then(([policyResponse, artifactResponse]) => { + // Set Policy Adherence + setPolicyAdherence({ + followedPolicies: policyResponse.body.followed || 0, + violatedPolicies: policyResponse.body.violated || 0, + unAppliedPolicies: policyResponse.body.unApplied || 0 + }); + + // Set API compliance + setApiCompliance({ + compliantArtifacts: artifactResponse.body.compliant || 0, + nonCompliantArtifacts: artifactResponse.body.nonCompliant || 0, + notApplicableArtifacts: artifactResponse.body.notApplicable || 0 + }); + }) + .catch((error) => { + console.error('Error fetching compliance data:', error); + }); + }, []); + + return ( + + + + + + + {intl.formatMessage({ + id: 'Governance.Overview.Summary.policy.adherence', + defaultMessage: 'Policy Adherence', + })} + + + + + + + + + + {intl.formatMessage({ + id: 'Governance.Overview.Summary.api.compliance', + defaultMessage: 'API Compliance', + })} + + + + + + + + + + {intl.formatMessage({ + id: 'Governance.Overview.Summary.api.compliance.details', + defaultMessage: 'API Compliance Details', + })} + + + + + + + + + + {intl.formatMessage({ + id: 'Governance.Overview.Summary.policy.adherence.details', + defaultMessage: 'Policy Adherence Details', + })} + + + + + + + + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/index.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/index.jsx new file mode 100755 index 00000000000..7f5e108e2d4 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Overview/index.jsx @@ -0,0 +1,40 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Route, Switch, withRouter } from 'react-router-dom'; +import ResourceNotFound from 'AppComponents/Base/Errors/ResourceNotFound'; +import Summary from './Summary'; +import Compliance from './APICompliance/Compliance'; + +/** + * Render a list + * @returns {JSX} Header AppBar components. + */ +function Overview() { + return ( + + + + + + ); +} + +export default withRouter(Overview); diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/ActionConfigDialog.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/ActionConfigDialog.jsx new file mode 100644 index 00000000000..089ad218a14 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/ActionConfigDialog.jsx @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Select, + MenuItem, + FormControl, + InputLabel, + Box, + Typography, + RadioGroup, + FormControlLabel, + Radio, + Grid, +} from '@mui/material'; +import { FormattedMessage, useIntl } from 'react-intl'; +import CONSTS from 'AppData/Constants'; + +export default function ActionConfigDialog({ + open, onClose, onSave, editAction, +}) { + const intl = useIntl(); + const [formState, setFormState] = useState(editAction || { + governedState: '', + actions: { + error: CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + warn: CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + info: CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + }, + }); + + useEffect(() => { + setFormState(editAction || { + governedState: '', + actions: { + error: CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + warn: CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + info: CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + }, + }); + }, [editAction]); + + const handleClose = () => { + setFormState({ + governedState: '', + actions: { + error: CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + warn: CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + info: CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + }, + }); + onClose(); + }; + + const handleSave = () => { + onSave(formState); + handleClose(); // Reset the form after saving + }; + + const isValid = () => { + return formState.governedState && Object.values(formState.actions).every((action) => action !== ''); + }; + + return ( + + + + + + + + + + + + + + + {formState.governedState && ( + + + + + + + + + + + + + + {CONSTS.SEVERITY_LEVELS.map((level) => ( + + + + + {level.label} + + + + setFormState({ + ...formState, + actions: { + ...formState.actions, + [level.value.toLowerCase()]: e.target.value, + }, + })} + > + } + label={( + + {intl.formatMessage({ + id: 'Governance.Policies.AddEdit.action.notify', + defaultMessage: 'Notify', + })} + + )} + /> + } + disabled={formState.governedState === 'API_CREATE' + || formState.governedState === 'API_UPDATE'} + label={( + + {intl.formatMessage({ + id: 'Governance.Policies.AddEdit.action.block', + defaultMessage: 'Block', + })} + + )} + /> + + + + + ))} + + )} + + + + + + + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/AddEditPolicy.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/AddEditPolicy.jsx new file mode 100644 index 00000000000..307f15d3137 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/AddEditPolicy.jsx @@ -0,0 +1,944 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useReducer, useState, useEffect } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Link as RouterLink } from 'react-router-dom'; +import Alert from 'AppComponents/Shared/Alert'; +import ContentBase from 'AppComponents/AdminPages/Addons/ContentBase'; +import { + Box, + Button, + Grid, + TextField, + Typography, + Chip, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + CircularProgress, + Autocomplete, + RadioGroup, + FormControlLabel, + Radio, +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import { styled } from '@mui/material/styles'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import PropTypes from 'prop-types'; +import cloneDeep from 'lodash.clonedeep'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import API from 'AppData/api'; +import Utils from 'AppData/Utils'; +import CONSTS from 'AppData/Constants'; +import ActionConfigDialog from './ActionConfigDialog'; +import RulesetSelector from './RulesetSelector'; + +// Keep these styled components +const StyledSpan = styled('span')(({ theme }) => ({ + color: theme.palette.error.dark, +})); + +const StyledHr = styled('hr')({ + border: 'solid 1px #efefef', +}); + +const PREFIX = 'AddEditPolicy'; + +const classes = { + root: `${PREFIX}-root`, + formContainer: `${PREFIX}-formContainer`, + helperText: `${PREFIX}-helperText`, + divider: `${PREFIX}-divider`, + actionButton: `${PREFIX}-actionButton`, + actionButtonContainer: `${PREFIX}-actionButtonContainer`, + tableContainer: `${PREFIX}-tableContainer`, + actionChip: `${PREFIX}-actionChip`, + buttonWrapper: `${PREFIX}-buttonWrapper`, + requiredStar: `${PREFIX}-requiredStar`, +}; + +const StyledContentBase = styled(ContentBase)(({ theme }) => ({ + [`& .${classes.formContainer}`]: { + margin: theme.spacing(1), + }, + [`& .${classes.helperText}`]: { + position: 'absolute', + marginTop: '10px', + }, + [`& .${classes.divider}`]: { + margin: `${theme.spacing(2)} 0`, + borderTop: `1px solid ${theme.palette.divider}`, + }, + [`& .${classes.actionButton}`]: { + margin: theme.spacing(1), + }, + [`& .${classes.actionButtonContainer}`]: { + margin: theme.spacing(1), + }, + [`& .${classes.tableContainer}`]: { + margin: theme.spacing(1), + }, + [`& .${classes.actionChip}`]: { + borderColor: (props) => (props.isBlock ? theme.palette.error.main : theme.palette.primary.main), + backgroundColor: (props) => (props.isBlock ? theme.palette.error.lighter : theme.palette.primary.lighter), + }, + [`& .${classes.buttonWrapper}`]: { + margin: theme.spacing(2), + }, + [`& .${classes.requiredStar}`]: { + color: theme.palette.error.dark, + }, +})); + +function reducer(state, { field, value }) { + const nextState = cloneDeep(state); + switch (field) { + case 'all': // We set initial state with this. + return value; + case 'name': + case 'description': + nextState[field] = value; + return nextState; + case 'labels': + if (Array.isArray(value)) { + nextState.labels = value; + } + return nextState; + case 'rulesets': + if (Array.isArray(value)) { + nextState.rulesets = value; + } + return nextState; + case 'actions': + if (Array.isArray(value)) { + nextState.actions = value; + } else if (typeof value === 'object') { + const { actionType, actionData, indexToRemove } = value; + + if (actionType === 'ADD') { + nextState.actions.push(actionData); + } else if (actionType === 'REMOVE') { + nextState.actions = nextState.actions.filter((_, index) => index !== indexToRemove); + } else if (actionType === 'UPDATE') { + nextState.actions[indexToRemove] = actionData; + } + } + return nextState; + default: + return nextState; + } +} + +function AddEditPolicy(props) { + const [validating, setValidating] = useState(false); + const [saving, setSaving] = useState(false); + const [availableRulesets, setAvailableRulesets] = useState([]); + const [selectedRulesets, setSelectedRulesets] = useState([]); // Store full ruleset objects for UI + const [availableLabels, setAvailableLabels] = useState([]); + const [labelMode, setLabelMode] = useState('all'); + const intl = useIntl(); + const { match: { params: { id: policyId } }, history } = props; + const editMode = policyId !== undefined; + + const initialState = { + name: '', + description: '', + labels: ['GLOBAL'], + actions: [], + rulesets: [], // Store only IDs + }; + const [state, dispatch] = useReducer(reducer, initialState); + + const { + name, + description, + labels, + actions, + rulesets, + } = state; + + const [dialogConfig, setDialogConfig] = useState({ + open: false, + editAction: null, + }); + + useEffect(() => { + const restApi = new GovernanceAPI(); + const adminApi = new API(); + + // Fetch available labels + adminApi.labelsListGet() + .then((response) => { + const labelList = response.body.list || []; + setAvailableLabels(labelList.map((label) => label.name)); + }) + .catch((error) => { + console.error('Error loading labels:', error); + Alert.error(intl.formatMessage({ + id: 'Governance.Policies.AddEdit.error.loading.labels', + defaultMessage: 'Error loading labels', + })); + }); + + restApi.getRulesetsList() + .then((response) => { + const rulesetList = response.body.list; + setAvailableRulesets(rulesetList); + + if (policyId) { + return restApi.getPolicy(policyId) + .then((policyResponse) => { + const { body } = policyResponse; + const fullRulesets = body.rulesets.map((rulesetId) => { + const foundRuleset = rulesetList.find((r) => r.id === rulesetId); + return foundRuleset || { id: rulesetId, name: 'Unknown Ruleset' }; + }); + setSelectedRulesets(fullRulesets); + + // Set the correct label mode based on the policy data + if (body.labels.length === 1 && body.labels[0] === 'GLOBAL') { + setLabelMode('all'); + } else if (body.labels.length === 0) { + setLabelMode('none'); + } else { + setLabelMode('specific'); + } + + return dispatch({ + field: 'all', + value: { + ...body, + rulesets: body.rulesets.map( + (ruleset) => (typeof ruleset === 'object' ? ruleset.id : ruleset), + ), + }, + }); + }); + } + return null; + }) + .catch((error) => { + console.error('Error loading rulesets:', error); + Alert.error(intl.formatMessage({ + id: 'Governance.Policies.AddEdit.error.loading.rulesets', + defaultMessage: 'Error loading rulesets', + })); + }); + }, [policyId]); + + const onChange = (e) => { + dispatch({ field: e.target.name, value: e.target.value }); + }; + + const handleLabelModeChange = (e) => { + setLabelMode(e.target.value); + switch (e.target.value) { + case 'all': + dispatch({ field: 'labels', value: ['GLOBAL'] }); + break; + case 'none': + dispatch({ field: 'labels', value: [] }); + break; + case 'specific': + // TODO: should load the saved labels instead of clearing + dispatch({ field: 'labels', value: [] }); + break; + default: + break; + } + }; + + const hasErrors = (fieldName, fieldValue, validatingActive) => { + let error = false; + if (!validatingActive) { + return false; + } + switch (fieldName) { + case 'name': + if (!fieldValue) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.name.required', + defaultMessage: 'Policy name is required', + }); + } else if (fieldValue.length < 1) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.name.too.short', + defaultMessage: 'Policy name cannot be empty', + }); + } else if (fieldValue.length > 255) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.name.too.long', + defaultMessage: 'Policy name cannot exceed 255 characters', + }); + } else if (!/^[a-zA-Z0-9-_ ]+$/.test(fieldValue)) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.name.invalid', + defaultMessage: 'Policy name can only contain alphanumeric characters,' + + ' hyphens, underscores, and spaces', + }); + } + break; + case 'description': + if (fieldValue && fieldValue.length > 1024) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.description.too.long', + defaultMessage: 'Description cannot exceed 1024 characters', + }); + } + break; + case 'rulesets': + if (!fieldValue || !Array.isArray(fieldValue) || fieldValue.length === 0) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.rulesets.required', + defaultMessage: 'At least one ruleset is required', + }); + } else if (new Set(fieldValue).size !== fieldValue.length) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.rulesets.duplicate', + defaultMessage: 'Duplicate rulesets are not allowed', + }); + } + break; + case 'actions': { + if (!fieldValue || !Array.isArray(fieldValue) || fieldValue.length === 0) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.actions.invalid', + defaultMessage: 'Actions must be properly configured', + }); + break; + } + + // Check for invalid BLOCK actions + const invalidBlockActions = fieldValue.filter((action) => ( + action.state === 'API_CREATE' || action.state === 'API_UPDATE') + && action.type === CONSTS.GOVERNANCE_ACTIONS.BLOCK); + if (invalidBlockActions.length > 0) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.actions.invalid.block', + defaultMessage: 'BLOCK action is not allowed for API_CREATE and API_UPDATE states', + }); + break; + } + break; + } + case 'labels': + if (labelMode === 'specific' && (!fieldValue || fieldValue.length === 0)) { + error = intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.labels.required', + defaultMessage: 'At least one label is required when applying to specific APIs', + }); + } + break; + default: + break; + } + return error; + }; + + const formHasErrors = (validatingActive = false) => { + if (hasErrors('name', name, validatingActive) + || hasErrors('description', description, validatingActive) + || hasErrors('labels', labels, validatingActive) + || hasErrors('rulesets', rulesets, validatingActive) + || hasErrors('actions', actions, validatingActive)) { + return true; + } + return false; + }; + + const formSave = () => { + setValidating(true); + if (formHasErrors(true)) { + Alert.error(intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.has.errors', + defaultMessage: 'One or more fields contain errors.', + })); + return false; + } + + setSaving(true); + const body = { + ...state, + governableStates: [...new Set(actions.map((action) => action.state))], + }; + + // Do the API call + const restApi = new GovernanceAPI(); + let promiseAPICall = null; + + if (policyId) { + promiseAPICall = restApi + .updatePolicy(body).then(() => { + return intl.formatMessage({ + id: 'Governance.Policies.AddEdit.edit.success', + defaultMessage: 'Policy Updated Successfully', + }); + }); + } else { + promiseAPICall = restApi + .addPolicy(body).then(() => { + return intl.formatMessage({ + id: 'Governance.Policies.AddEdit.add.success', + defaultMessage: 'Policy Added Successfully', + }); + }); + } + + promiseAPICall.then((msg) => { + Alert.success(`${name} ${msg}`); + history.push('/governance/policies/'); + }).catch((error) => { + const { response, message } = error; + if (response && response.body) { + Alert.error(response.body.description); + } else if (message) { + Alert.error(message); + } + return null; + }).finally(() => { + setSaving(false); + }); + return true; + }; + + const groupActionsByState = (actionsList) => { + return actionsList.reduce((acc, action) => { + const existingStateIndex = acc.findIndex((item) => item.state === action.state); + if (existingStateIndex === -1) { + acc.push({ + state: action.state, + error: action.ruleSeverity === 'ERROR' ? action.type : null, + warn: action.ruleSeverity === 'WARN' ? action.type : null, + info: action.ruleSeverity === 'INFO' ? action.type : null, + }); + } else { + switch (action.ruleSeverity) { + case 'ERROR': + acc[existingStateIndex].error = action.type; + break; + case 'WARN': + acc[existingStateIndex].warn = action.type; + break; + case 'INFO': + acc[existingStateIndex].info = action.type; + break; + default: + break; + } + } + return acc; + }, []); + }; + + const handleActionSave = (actionConfig) => { + const newActions = []; + const { governedState, actions: configuredActions } = actionConfig; + + Object.entries(configuredActions).forEach(([severity, action]) => { + if (action) { + newActions.push({ + state: governedState, + ruleSeverity: severity.toUpperCase(), + type: action, + }); + } + }); + + // Remove existing actions for this governedState + const filteredActions = actions.filter((action) => action.state !== governedState); + dispatch({ field: 'actions', value: [...filteredActions, ...newActions] }); + setDialogConfig({ + open: false, + editAction: null, + }); + }; + + const handleAddAction = () => { + setDialogConfig({ + open: true, + editAction: null, + }); + }; + + const handleEditAction = (groupedAction) => { + const actionConfig = { + governedState: groupedAction.state, + actions: { + error: groupedAction.error || CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + warn: groupedAction.warn || CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + info: groupedAction.info || CONSTS.GOVERNANCE_ACTIONS.NOTIFY, + }, + }; + setDialogConfig({ + open: true, + editAction: actionConfig, + }); + }; + + const handleCloseDialog = () => { + setDialogConfig({ + open: false, + editAction: null, + }); + }; + + const handleRulesetSelect = (ruleset) => { + dispatch({ + field: 'rulesets', + value: [...rulesets, ruleset.id], + }); + setSelectedRulesets([...selectedRulesets, ruleset]); + }; + + const handleRulesetDeselect = (ruleset) => { + dispatch({ + field: 'rulesets', + value: rulesets.filter((id) => id !== ruleset.id), + }); + setSelectedRulesets(selectedRulesets.filter((r) => r.id !== ruleset.id)); + }; + + return ( + TODO: Link Doc} + > + + + + + + + + + + + + + + + * + + )} + fullWidth + error={hasErrors('name', name, validating)} + helperText={hasErrors('name', name, validating) || intl.formatMessage({ + id: 'Governance.Policies.AddEdit.form.name.help', + defaultMessage: 'Name of the governance policy.', + })} + variant='outlined' + /> + + + + + + + + + + + + + + + + + + + + + + + } + label={intl.formatMessage({ + id: 'Governance.Policies.AddEdit.labels.applyAll', + defaultMessage: 'Apply to all APIs', + })} + /> + } + label={intl.formatMessage({ + id: 'Governance.Policies.AddEdit.labels.applySpecific', + defaultMessage: 'Apply to APIs with specific labels', + })} + /> + } + label={intl.formatMessage({ + id: 'Governance.Policies.AddEdit.labels.applyNone', + defaultMessage: 'Apply to none', + })} + /> + + + {labelMode === 'specific' && ( + { + dispatch({ field: 'labels', value: newValue }); + }} + renderInput={(params) => ( + + )} + renderTags={(value, getTagProps) => value.map((option, index) => ( + + ))} + /> + )} + + + + + + + + + + + + + + + + + + + + + + + {actions && actions.length > 0 && ( + + + + + + {intl.formatMessage({ + id: 'Governance.Policies.AddEdit.action.table.state', + defaultMessage: 'State', + })} + + + {intl.formatMessage({ + id: 'Governance.Policies.AddEdit.action.table.onError', + defaultMessage: 'On Error', + })} + + + {intl.formatMessage({ + id: 'Governance.Policies.AddEdit.action.table.onWarn', + defaultMessage: 'On Warn', + })} + + + {intl.formatMessage({ + id: 'Governance.Policies.AddEdit.action.table.onInfo', + defaultMessage: 'On Info', + })} + + + {intl.formatMessage({ + id: 'Governance.Policies.AddEdit.action.table.actions', + defaultMessage: 'Actions', + })} + + + + + {groupActionsByState(actions).map((groupedAction) => ( + + + {Utils.mapGovernableStateToLabel(groupedAction.state)} + + + + + + + + + + + + handleEditAction(groupedAction)} + size='small' + sx={{ mr: 1 }} + > + + + { + const newActions = actions.filter( + (a) => a.state !== groupedAction.state, + ); + dispatch({ field: 'actions', value: newActions }); + }} + size='small' + > + + + + + ))} + +
+
+ )} +
+ {validating && hasErrors('actions', actions, true) && ( + + {hasErrors('actions', actions, true)} + + )} +
+ + + + + + + + + + + + + + + + + + + + {validating && hasErrors('rulesets', rulesets, true) && ( + + {hasErrors('rulesets', rulesets, true)} + + )} + + + + + + + + + + + + + + + + +
+
+ + +
+ ); +} + +AddEditPolicy.propTypes = { + match: PropTypes.shape({}).isRequired, + history: PropTypes.shape({}).isRequired, +}; + +export default AddEditPolicy; diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/DeletePolicy.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/DeletePolicy.jsx new file mode 100644 index 00000000000..21272b41fd3 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/DeletePolicy.jsx @@ -0,0 +1,83 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import PropTypes from 'prop-types'; +import { FormattedMessage, useIntl } from 'react-intl'; +import DialogContentText from '@mui/material/DialogContentText'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import FormDialogBase from 'AppComponents/AdminPages/Addons/FormDialogBase'; + +/** + * Render delete dialog box. + * @param {JSON} props component props. + * @returns {JSX} Loading animation. + */ +function DeletePolicy({ updateList, dataRow }) { + const { id } = dataRow; + const intl = useIntl(); + const formSaveCallback = () => { + return new GovernanceAPI() + .deletePolicy(id) + .then(() => ( + + )) + .catch((error) => { + throw new Error(error.response.body.description); + }) + .finally(() => { + updateList(); + }); + }; + + return ( + } + formSaveCallback={formSaveCallback} + > + + + + + ); +} + +DeletePolicy.propTypes = { + updateList: PropTypes.func.isRequired, + dataRow: PropTypes.shape({ + id: PropTypes.string.isRequired, + }).isRequired, +}; + +export default DeletePolicy; diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/ListPolicies.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/ListPolicies.jsx new file mode 100644 index 00000000000..ed0a2473955 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/ListPolicies.jsx @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { useIntl, FormattedMessage } from 'react-intl'; +import Typography from '@mui/material/Typography'; +import { + Chip, Stack, Tooltip, Button, +} from '@mui/material'; +import { Link as RouterLink } from 'react-router-dom'; +import EditIcon from '@mui/icons-material/Edit'; +import ListBase from 'AppComponents/AdminPages/Addons/ListBase'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import Utils from 'AppData/Utils'; +import DeletePolicy from './DeletePolicy'; + +/** + * API call to get Policies + * @returns {Promise}. + */ +function apiCall() { + const restApi = new GovernanceAPI(); + return restApi + .getPoliciesList() + .then((result) => { + return result.body.list; + }) + .catch((error) => { + throw error; + }); +} + +/** + * Render a list of policies + * @returns {JSX} List component + */ +export default function ListPolicies() { + const intl = useIntl(); + + const columProps = [ + { + name: 'name', + label: intl.formatMessage({ + id: 'Governance.Policies.List.column.policy', + defaultMessage: 'Policy', + }), + options: { + sort: true, + customBodyRender: (value, tableMeta) => { + const dataRow = tableMeta.rowData; + return ( + <> + {/* TODO: Add text wrapping */} + {value} + + {dataRow[1]} + + + ); + }, + setCellProps: () => ({ + style: { + width: '35%', + }, + }), + }, + }, + { + name: 'description', + options: { display: false }, + }, + { + name: 'governableStates', + label: intl.formatMessage({ + id: 'Governance.Policies.List.column.appliesWhen', + defaultMessage: 'Applies when', + }), + options: { + sort: false, + customBodyRender: (value) => { + if (!value?.length) return 'Not set'; + const displayItems = value.slice(0, 2); + const remainingCount = value.length - 2; + + return ( + Utils.mapGovernableStateToLabel(label)).join(', ')} + arrow + > + + {displayItems.map((label) => ( + + ))} + {remainingCount > 0 && ( + + + + {remainingCount} + + )} + + + ); + }, + setCellProps: () => ({ + style: { + width: '25%', + }, + }), + }, + }, + { + name: 'labels', + label: intl.formatMessage({ + id: 'Governance.Policies.List.column.appliesTo', + defaultMessage: 'Applies to', + }), + options: { + sort: false, + customBodyRender: (value) => { + if (!value?.length) return 'None'; + const displayItems = value.slice(0, 2); + const remainingCount = value.length - 2; + + return ( + + + {displayItems.map((label) => ( + + ))} + {remainingCount > 0 && ( + + + + {remainingCount} + + )} + + + ); + }, + setCellProps: () => ({ + style: { + width: '25%', + }, + }), + }, + }, + { + name: 'id', + options: { display: false }, + }, // Id column has to be always the last since it is used in the actions. + ]; + + const pageProps = { + pageStyle: 'paperLess', + title: intl.formatMessage({ + id: 'Governance.Policies.List.title', + defaultMessage: 'Governance Policies', + }), + pageDescription: intl.formatMessage({ + id: 'Governance.Policies.List.description', + defaultMessage: 'Create governance policies using rulesets from the catalog' + + ' to standardize and regulate your APls effectively', + }), + }; + + const addButtonProps = { + triggerButtonText: intl.formatMessage({ + id: 'Governance.Policies.List.addPolicy.triggerButtonText', + defaultMessage: 'Create Governance Policy', + }), + title: intl.formatMessage({ + id: 'Governance.Policies.List.addPolicy.title', + defaultMessage: 'Create Governance Policy', + }), + }; + + const searchProps = { + searchPlaceholder: intl.formatMessage({ + id: 'Governance.Policies.List.search.default', + defaultMessage: 'Search policies by name or label', + }), + active: true, + }; + + const emptyBoxProps = { + content: ( + + + + ), + title: ( + + + + ), + }; + + const addButtonOverride = ( + + + + ); + + return ( + , + title: intl.formatMessage({ + id: 'Governance.Policies.List.edit.title', + defaultMessage: 'Edit Policy', + }), + routeTo: '/governance/policies/', + }} + addButtonOverride={addButtonOverride} + /> + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/RulesetSelector.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/RulesetSelector.jsx new file mode 100644 index 00000000000..262cdeece5d --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/RulesetSelector.jsx @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { + Box, + TextField, + Typography, + Chip, + Card, + CardContent, + Grid, + Checkbox, + Link, +} from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; +import InputAdornment from '@mui/material/InputAdornment'; +import Pagination from '@mui/material/Pagination'; +import BusinessIcon from '@mui/icons-material/Business'; +import LaunchIcon from '@mui/icons-material/Launch'; +import Utils from 'AppData/Utils'; +import { styled } from '@mui/material/styles'; + +// Move getChipStyles from AddEditPolicy +const getChipStyles = (type) => { + switch (type) { + case 'API_DEFINITION': + return { + color: '#0D47A1', + borderColor: '#0D47A1', + }; + case 'API_METADATA': + return { + color: '#4A148C', + borderColor: '#4A148C', + }; + case 'API_DOCUMENTATION': + return { + color: '#1B5E20', + borderColor: '#1B5E20', + }; + default: + return {}; + } +}; + +const PREFIX = 'RulesetSelector'; + +const classes = { + selectedRulesets: `${PREFIX}-selectedRulesets`, + searchField: `${PREFIX}-searchField`, + card: `${PREFIX}-card`, + cardContent: `${PREFIX}-cardContent`, + checkboxWrapper: `${PREFIX}-checkboxWrapper`, + chip: `${PREFIX}-chip`, + description: `${PREFIX}-description`, + providerWrapper: `${PREFIX}-providerWrapper`, + documentationDivider: `${PREFIX}-documentationDivider`, + documentationLink: `${PREFIX}-documentationLink`, + documentationIcon: `${PREFIX}-documentationIcon`, + pagination: `${PREFIX}-pagination`, +}; + +const StyledBox = styled(Box)(({ theme }) => ({ + [`& .${classes.selectedRulesets}`]: { + marginBottom: theme.spacing(2), + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(1), + }, + [`& .${classes.searchField}`]: { + marginBottom: theme.spacing(2), + }, + [`& .${classes.card}`]: { + position: 'relative', + cursor: 'pointer', + transition: 'all 0.3s', + height: '100%', + display: 'flex', + flexDirection: 'column', + '&:hover': { + boxShadow: theme.shadows[2], + }, + }, + [`& .${classes.cardContent}`]: { + flex: 1, + display: 'flex', + flexDirection: 'column', + position: 'relative', + padding: theme.spacing(2), + }, + [`& .${classes.checkboxWrapper}`]: { + position: 'absolute', + right: 8, + top: 8, + }, + [`& .${classes.chip}`]: { + height: '16px', + '& .MuiChip-label': { + padding: '0 6px', + fontSize: '0.625rem', + lineHeight: 1, + }, + }, + [`& .${classes.description}`]: { + marginBottom: theme.spacing(2), + display: '-webkit-box', + WebkitBoxOrient: 'vertical', + WebkitLineClamp: 3, + overflow: 'hidden', + minHeight: '4.5em', + }, + [`& .${classes.providerWrapper}`]: { + display: 'flex', + alignItems: 'center', + color: theme.palette.text.secondary, + }, + [`& .${classes.documentationDivider}`]: { + margin: `${theme.spacing(1)} 0`, + borderTop: `1px solid ${theme.palette.divider}`, + }, + [`& .${classes.documentationLink}`]: { + display: 'flex', + alignItems: 'center', + fontSize: '0.75rem', + color: theme.palette.text.secondary, + textDecoration: 'none', + '&:hover': { + color: theme.palette.primary.main, + }, + }, + [`& .${classes.documentationIcon}`]: { + marginRight: theme.spacing(0.5), + fontSize: '0.875rem', + }, + [`& .${classes.pagination}`]: { + marginTop: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +function RulesetSelector({ + availableRulesets, + selectedRulesets, + onRulesetSelect, + onRulesetDeselect, +}) { + const [searchQuery, setSearchQuery] = useState(''); + const [page, setPage] = useState(1); + const itemsPerPage = 6; + + const filteredRulesets = availableRulesets.filter( + (ruleset) => ( + ruleset.name.toLowerCase().includes(searchQuery.toLowerCase()) + || ruleset.description.toLowerCase().includes(searchQuery.toLowerCase()) + ), + ); + + const paginatedRulesets = filteredRulesets.slice( + (page - 1) * itemsPerPage, + page * itemsPerPage, + ); + + const handleRulesetToggle = (ruleset) => { + const isSelected = selectedRulesets.some((r) => r.id === ruleset.id); + if (isSelected) { + onRulesetDeselect(ruleset); + } else { + onRulesetSelect(ruleset); + } + }; + + return ( + + {/* Selected Rulesets */} + {selectedRulesets.length > 0 && ( + + {selectedRulesets.map((ruleset) => ( + onRulesetDeselect(ruleset)} + color='primary' + variant='outlined' + /> + ))} + + )} + + {/* Search Bar */} + { + setSearchQuery(e.target.value); + setPage(1); + }} + className={classes.searchField} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + {/* Rulesets Grid */} + + {paginatedRulesets.map((ruleset) => ( + + handleRulesetToggle(ruleset)} + sx={{ + border: (theme) => ( + selectedRulesets.some((r) => r.id === ruleset.id) + ? `2px solid ${theme.palette.primary.main}` + : '1px solid rgba(0, 0, 0, 0.12)' + ), + }} + > + + + r.id === ruleset.id)} + onClick={(e) => e.stopPropagation()} + onChange={(e) => { + if (e.target.checked) { + onRulesetSelect(ruleset); + } else { + onRulesetDeselect(ruleset); + } + }} + /> + + + {ruleset.name} + + + + + + + {ruleset.description} + + + + + + {ruleset.provider} + + + {ruleset.documentationLink && ( + <> + + e.stopPropagation()} + className={classes.documentationLink} + > + + Documentation + + + )} + + + + + ))} + + + {/* Pagination */} + {filteredRulesets.length > itemsPerPage && ( + + setPage(value)} + color='primary' + /> + + )} + + {filteredRulesets.length === 0 && ( + + {searchQuery ? ( + + ) : ( + + )} + + )} + + ); +} + +RulesetSelector.propTypes = { + availableRulesets: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + ruleType: PropTypes.string.isRequired, + artifactType: PropTypes.string.isRequired, + provider: PropTypes.string.isRequired, + documentationLink: PropTypes.string, + })).isRequired, + selectedRulesets: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + })).isRequired, + onRulesetSelect: PropTypes.func.isRequired, + onRulesetDeselect: PropTypes.func.isRequired, +}; + +export default RulesetSelector; diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/index.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/index.jsx new file mode 100755 index 00000000000..372086457f5 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/Policies/index.jsx @@ -0,0 +1,41 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Route, Switch, withRouter } from 'react-router-dom'; +import ResourceNotFound from 'AppComponents/Base/Errors/ResourceNotFound'; +import ListPolicies from './ListPolicies'; +import AddEditPolicy from './AddEditPolicy'; + +/** + * Render a list + * @returns {JSX} Header AppBar components. + */ +function Policies() { + return ( + + + + + + + ); +} + +export default withRouter(Policies); diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/AddEditRuleset.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/AddEditRuleset.jsx new file mode 100644 index 00000000000..57ef98d5132 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/AddEditRuleset.jsx @@ -0,0 +1,669 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useReducer, useState, useEffect } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Link as RouterLink } from 'react-router-dom'; +import Alert from 'AppComponents/Shared/Alert'; +import ContentBase from 'AppComponents/AdminPages/Addons/ContentBase'; +import { + Box, + Button, + Grid, + TextField, + Typography, + MenuItem, + Paper, + CircularProgress, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + DialogContentText, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import Editor from '@monaco-editor/react'; +import cloneDeep from 'lodash.clonedeep'; +import PropTypes from 'prop-types'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import CONSTS from 'AppData/Constants'; +import AuthManager from 'AppData/AuthManager'; + +const StyledSpan = styled('span')(({ theme }) => ({ color: theme.palette.error.dark })); +const StyledHr = styled('hr')({ border: 'solid 1px #efefef' }); + +const EditorToolbar = styled(Box)(({ theme }) => ({ + padding: theme.spacing(1), + borderTopLeftRadius: theme.shape.borderRadius, + borderTopRightRadius: theme.shape.borderRadius, + borderBottom: `1px solid ${theme.palette.divider}`, + display: 'flex', + justifyContent: 'flex-end', + backgroundColor: theme.palette.grey[50], +})); + +const EditorContainer = styled(Box)(({ theme }) => ({ + height: 400, + '& .monaco-editor': { + borderBottomLeftRadius: theme.shape.borderRadius, + borderBottomRightRadius: theme.shape.borderRadius, + overflow: 'hidden', + }, +})); + +function reducer(state, { field, value }) { + const nextState = cloneDeep(state); + switch (field) { + case 'all': + return value; + case 'name': + case 'description': + case 'ruleType': + case 'artifactType': + case 'provider': + case 'rulesetContent': + case 'documentationLink': + nextState[field] = value; + return nextState; + default: + return nextState; + } +} + +function AddEditRuleset(props) { + const [validating, setValidating] = useState(false); + const [saving, setSaving] = useState(false); + const [openConfirmDialog, setOpenConfirmDialog] = useState(false); + const [pendingFile, setPendingFile] = useState(null); + const intl = useIntl(); + const { match: { params: { id } }, history } = props; + const editMode = id !== undefined; + + const initialState = { + name: '', + description: '', + ruleType: '', + artifactType: '', + provider: '', + rulesetContent: '', + documentationLink: '', + }; + const [state, dispatch] = useReducer(reducer, initialState); + + const { + name, + description, + ruleType, + artifactType, + rulesetContent, + documentationLink, + } = state; + + useEffect(() => { + const restApi = new GovernanceAPI(); + if (id) { + // Get ruleset metadata + restApi + .getRuleset(id) + .then((result) => { + const { body } = result; + return body; + }) + .then((data) => { + dispatch({ field: 'all', value: data }); + // After getting metadata, fetch the ruleset content + return restApi.getRulesetContent(id); + }) + .then((contentResult) => { + const { text } = contentResult; + dispatch({ field: 'rulesetContent', value: text }); + }) + .catch((error) => { + const { response } = error; + if (response && response.body) { + Alert.error(response.body.description); + } else { + Alert.error(intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.error.loading', + defaultMessage: 'Error loading ruleset', + })); + } + }); + } + }, [id]); + + const onChange = (e) => { + dispatch({ field: e.target.name, value: e.target.value }); + }; + + const handleEditorChange = (value) => { + dispatch({ field: 'rulesetContent', value }); + }; + + const processFile = (file) => { + const reader = new FileReader(); + reader.onload = (e) => { + dispatch({ field: 'rulesetContent', value: e.target.result }); + }; + reader.onerror = () => { + Alert.error(intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.file.read.error', + defaultMessage: 'Error reading file', + })); + }; + reader.readAsText(file); + }; + + const handleFileUpload = (event) => { + const file = event.target.files[0]; + if (file) { + if (rulesetContent.trim()) { + setPendingFile(file); + setOpenConfirmDialog(true); + } else { + processFile(file); + } + } + // Clone the event target before modifying + const input = event.target; + setTimeout(() => { + input.value = ''; + }, 0); + }; + + const handleConfirmOverwrite = () => { + if (pendingFile) { + processFile(pendingFile); + setPendingFile(null); + } + setOpenConfirmDialog(false); + }; + + const handleCancelOverwrite = () => { + setPendingFile(null); + setOpenConfirmDialog(false); + }; + + const hasErrors = (fieldName, fieldValue, validatingActive) => { + let error = false; + if (!validatingActive) { + return false; + } + switch (fieldName) { + case 'name': + if (!fieldValue) { + error = intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.name.required', + defaultMessage: 'Ruleset name is required', + }); + } else if (fieldValue.length > 255) { + error = intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.name.too.long', + defaultMessage: 'Ruleset name cannot exceed 255 characters', + }); + } else if (!/^[a-zA-Z0-9-_ ]+$/.test(fieldValue)) { + error = intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.name.invalid', + defaultMessage: 'Ruleset name can only contain alphanumeric characters,' + + ' spaces, hyphens and underscores.', + }); + } + break; + + case 'description': + if (fieldValue && fieldValue.length > 1000) { + error = intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.description.too.long', + defaultMessage: 'Description cannot exceed 1000 characters', + }); + } + break; + + case 'ruleType': + if (!fieldValue) { + error = intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.ruletype.required', + defaultMessage: 'Rule type is required', + }); + } + break; + + case 'artifactType': + if (!fieldValue) { + error = intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.artifacttype.required', + defaultMessage: 'Artifact type is required', + }); + } + break; + + case 'documentationLink': + if (fieldValue) { + if (fieldValue.length > 500) { + error = intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.doclink.too.long', + defaultMessage: 'Documentation link cannot exceed 500 characters', + }); + } else if (!/^https?:\/\/.+/.test(fieldValue)) { + error = intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.doclink.invalid', + defaultMessage: 'Documentation link must be a valid HTTP/HTTPS URL', + }); + } + } + break; + + case 'rulesetContent': + if (!fieldValue || fieldValue.trim().length === 0) { + error = intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.rulesetcontent.required', + defaultMessage: 'Ruleset content is required', + }); + } + break; + + default: + break; + } + return error; + }; + + const formHasErrors = (validatingActive = false) => { + if (hasErrors('name', name, validatingActive) + || hasErrors('description', description, validatingActive) + || hasErrors('ruleType', ruleType, validatingActive) + || hasErrors('artifactType', artifactType, validatingActive) + || hasErrors('documentationLink', documentationLink, validatingActive) + || hasErrors('rulesetContent', rulesetContent, validatingActive)) { + return true; + } + return false; + }; + + const formSave = () => { + setValidating(true); + if (formHasErrors(true)) { + Alert.error(intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.form.has.errors', + defaultMessage: 'One or more fields contain errors.', + })); + return false; + } + + setSaving(true); + + const file = new File([rulesetContent], `${name}.yaml`); + const body = { + ...state, + provider: AuthManager.getUser().name, + rulesetContent: file, + }; + + // Do the API call + const restApi = new GovernanceAPI(); + let promiseAPICall = null; + + if (id) { + promiseAPICall = restApi.updateRuleset(id, body) + .then(() => { + return intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.edit.success', + defaultMessage: 'Ruleset Updated Successfully', + }); + }); + } else { + promiseAPICall = restApi.addRuleset(body) + .then(() => { + return intl.formatMessage({ + id: 'Governance.Rulesets.AddEdit.add.success', + defaultMessage: 'Ruleset Added Successfully', + }); + }); + } + + promiseAPICall.then((msg) => { + Alert.success(`${name} ${msg}`); + history.push('/governance/ruleset-catalog/'); + }).catch((error) => { + const { response, message } = error; + if (response && response.body) { + Alert.error(response.body.description); + } else if (message) { + Alert.error(message); + } + return null; + }).finally(() => { + setSaving(false); + }); + return true; + }; + + return ( + + ) : ( + + ) + } + help={
TODO: Link Doc
} + > + + + {/* General Details Section */} + + + + + + + + + + + + + + * + + )} + fullWidth + error={hasErrors('name', name, validating)} + helperText={hasErrors('name', name, validating)} + variant='outlined' + disabled={editMode} + /> + + )} + fullWidth + error={hasErrors('description', description, validating)} + helperText={hasErrors('description', description, validating)} + multiline + rows={3} + variant='outlined' + /> + + )} + fullWidth + error={hasErrors('documentationLink', documentationLink, validating)} + helperText={hasErrors('documentationLink', documentationLink, validating)} + variant='outlined' + /> + + + + )} + fullWidth + error={hasErrors('ruleType', ruleType, validating)} + helperText={hasErrors('ruleType', ruleType, validating)} + required + variant='outlined' + > + {CONSTS.RULESET_TYPES.map((option) => ( + + {option.label} + + ))} + + + + + )} + fullWidth + error={hasErrors('artifactType', artifactType, validating)} + helperText={hasErrors('artifactType', artifactType, validating)} + required + variant='outlined' + > + {CONSTS.ARTIFACT_TYPES.map((option) => ( + + {option.label} + + ))} + + + + + + + + + + + + + {/* Ruleset Content Section */} + + + + + + + + + + + + + + + + + + + + + {validating && hasErrors('rulesetContent', rulesetContent, true) && ( + + {hasErrors('rulesetContent', rulesetContent, true)} + + )} + + + {/* Action Buttons */} + + + + + + + + + + + + {/* Add the confirmation dialog */} + + + + + + + + + + + + + + +
+ ); +} + +AddEditRuleset.propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + id: PropTypes.string, + }), + }).isRequired, + history: PropTypes.shape({ + push: PropTypes.func, + }).isRequired, +}; + +export default AddEditRuleset; diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/DeleteRuleset.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/DeleteRuleset.jsx new file mode 100644 index 00000000000..d8de713677e --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/DeleteRuleset.jsx @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import PropTypes from 'prop-types'; +import { FormattedMessage, useIntl } from 'react-intl'; +import DialogContentText from '@mui/material/DialogContentText'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import FormDialogBase from 'AppComponents/AdminPages/Addons/FormDialogBase'; + +/** + * Renders delete dialog box + * @param {JSON} props component properties + * @returns {JSX} Delete dialog box + */ +function DeleteRuleset({ updateList, dataRow }) { + const { id } = dataRow; + const intl = useIntl(); + + const formSaveCallback = () => { + return new GovernanceAPI() + .deleteRuleset(id) + .then(() => ( + + )) + .catch((error) => { + throw new Error(error.response.body.description); + }) + .finally(() => { + updateList(); + }); + }; + + return ( + } + formSaveCallback={formSaveCallback} + > + + + + + ); +} + +DeleteRuleset.propTypes = { + updateList: PropTypes.func.isRequired, + dataRow: PropTypes.shape({ + id: PropTypes.string.isRequired, + }).isRequired, +}; + +export default DeleteRuleset; diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/ListRulesets.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/ListRulesets.jsx new file mode 100644 index 00000000000..017c6dea656 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/ListRulesets.jsx @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { useIntl, FormattedMessage } from 'react-intl'; +import Typography from '@mui/material/Typography'; +import ListBase from 'AppComponents/AdminPages/Addons/ListBase'; +import { Chip, Button } from '@mui/material'; +import { Link as RouterLink } from 'react-router-dom'; +import EditIcon from '@mui/icons-material/Edit'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import Utils from 'AppData/Utils'; +import DeleteRuleset from './DeleteRuleset'; + +/** + * API call to get Rulesets + * @returns {Promise}. + */ +function apiCall() { + const restApi = new GovernanceAPI(); + return restApi + .getRulesetsList() + .then((result) => { + return result.body.list; + }) + .catch((error) => { + throw error; + }); +} + +/** + * Render a list of rulesets + * @returns {JSX} List component + */ +export default function ListRulesets() { + const intl = useIntl(); + + const columProps = [ + { + name: 'name', + label: intl.formatMessage({ + id: 'Governance.Rulesets.List.column.ruleset', + defaultMessage: 'Ruleset', + }), + options: { + filter: true, + sort: true, + customBodyRender: (value, tableMeta) => { + const dataRow = tableMeta.rowData; + return ( + // TODO: Add text wrapping + tooltip for long descriptions + <> + {value} + + {dataRow[1]} + + + ); + }, + setCellProps: () => ({ + style: { + width: '40%', + }, + }), + }, + }, + { + name: 'description', + options: { display: false }, + }, + { + name: 'artifactType', + label: intl.formatMessage({ + id: 'Governance.Rulesets.List.column.artifactType', + defaultMessage: 'Artifact Type', + }), + options: { + filter: true, + sort: false, + customBodyRender: (value) => ( + + ), + setCellProps: () => ({ + style: { + width: '15%', + textAlign: 'center', + }, + }), + setCellHeaderProps: () => ({ + style: { + textAlign: 'center', + }, + }), + }, + }, + { + name: 'ruleType', + label: intl.formatMessage({ + id: 'Governance.Rulesets.List.column.rulesetType', + defaultMessage: 'Ruleset Type', + }), + options: { + filter: true, + sort: false, + customBodyRender: (value) => ( + + ), + setCellProps: () => ({ + style: { + width: '15%', + textAlign: 'center', + }, + }), + setCellHeaderProps: () => ({ + style: { + textAlign: 'center', + }, + }), + }, + }, + { + name: 'provider', + label: intl.formatMessage({ + id: 'Governance.Rulesets.List.column.provider', + defaultMessage: 'Provider', + }), + options: { + filter: true, + sort: false, + setCellProps: () => ({ + style: { + width: '15%', + textAlign: 'center', + }, + }), + setCellHeaderProps: () => ({ + style: { + textAlign: 'center', + }, + }), + }, + }, + { name: 'id', options: { display: false } }, + ]; + + const pageProps = { + pageStyle: 'paperLess', + title: intl.formatMessage({ + id: 'Governance.Rulesets.List.title', + defaultMessage: 'Ruleset Catalog', + }), + pageDescription: intl.formatMessage({ + id: 'Governance.Rulesets.List.description', + defaultMessage: + 'Find comprehensive governance rulesets designed to ensure' + + ' the consistency, security and reliability for your APls', + }), + }; + + const addButtonProps = { + triggerButtonText: intl.formatMessage({ + id: 'Governance.Rulesets.List.addRuleset.triggerButtonText', + defaultMessage: 'Create Ruleset', + }), + title: intl.formatMessage({ + id: 'Governance.Rulesets.List.addRuleset.title', + defaultMessage: 'Create Ruleset', + }), + }; + + const emptyBoxProps = { + content: ( + + + + ), + title: ( + + + + ), + }; + + const addButtonOverride = ( + + + + ); + + return ( + <> + , + title: intl.formatMessage({ + id: 'Governance.Rulesets.List.edit.title', + defaultMessage: 'Edit Ruleset', + }), + routeTo: '/governance/ruleset-catalog/', + }} + addButtonOverride={addButtonOverride} + /> + + ); +} diff --git a/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/index.jsx b/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/index.jsx new file mode 100644 index 00000000000..b742a229d68 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Governance/RulesetCatalog/index.jsx @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Route, Switch, withRouter } from 'react-router-dom'; +import ResourceNotFound from 'AppComponents/Base/Errors/ResourceNotFound'; +import ListRulesets from './ListRulesets'; +import AddEditRuleset from './AddEditRuleset'; + +/** + * Render ruleset routes + * @returns {JSX} Ruleset routing component + */ +function RulesetCatalog() { + return ( + + + + + + + ); +} + +export default withRouter(RulesetCatalog); diff --git a/portals/admin/src/main/webapp/source/src/app/components/Shared/DonutChart.jsx b/portals/admin/src/main/webapp/source/src/app/components/Shared/DonutChart.jsx new file mode 100644 index 00000000000..808fedf4b49 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/Shared/DonutChart.jsx @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { PieChart } from '@mui/x-charts/PieChart'; +import { Box, Typography } from '@mui/material'; + +const DonutChart = ({ + data, height, width, colors, +}) => { + const hasData = data.some((item) => item.value > 0); + + const renderEmptyChart = (message) => ( + + + {message} + + + ); + + if (!hasData) { + return renderEmptyChart('No data available'); + } + + return ( + + + + ); +}; + +DonutChart.propTypes = { + data: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + value: PropTypes.number.isRequired, + label: PropTypes.string.isRequired, + })).isRequired, + height: PropTypes.number, + width: PropTypes.number, + colors: PropTypes.arrayOf(PropTypes.string), +}; + +DonutChart.defaultProps = { + height: 200, + width: 400, + colors: ['#2E96FF', '#FF5252', 'grey'], +}; + +export default DonutChart; diff --git a/portals/admin/src/main/webapp/source/src/app/data/APIClientFactory.js b/portals/admin/src/main/webapp/source/src/app/data/APIClientFactory.js index a11a67ac4e5..c96b2d116ef 100644 --- a/portals/admin/src/main/webapp/source/src/app/data/APIClientFactory.js +++ b/portals/admin/src/main/webapp/source/src/app/data/APIClientFactory.js @@ -17,6 +17,7 @@ */ import APIClient from './APIClient'; +import GovernanceAPIClient from './GovernanceAPIClient'; import Utils from './Utils'; /** @@ -40,28 +41,36 @@ class APIClientFactory { } /** - * - * @param {Object} environment - * @returns {APIClient} APIClient object for the environment + * Get an API Client for the given environment and client type + * @param {Object} environment - Environment object for the client + * @param {String} clientType - Type of client (API_CLIENT or GOVERNANCE_CLIENT) + * @returns {APIClient|GovernanceAPIClient} Client instance */ - getAPIClient(environment = Utils.getDefaultEnvironment()) { - let apiClient = this._APIClientMap.get(environment.label); + getAPIClient(environment = Utils.getDefaultEnvironment(), clientType = Utils.CONST.API_CLIENT) { + const key = environment.label + '_' + clientType; + let client = this._APIClientMap.get(key); - if (apiClient) { - return apiClient; + if (client) { + return client; } - apiClient = new APIClient(environment); - this._APIClientMap.set(environment.label, apiClient); - return apiClient; + if (clientType === Utils.CONST.API_CLIENT) { + client = new APIClient(environment); + } else if (clientType === Utils.CONST.GOVERNANCE_CLIENT) { + client = new GovernanceAPIClient(environment); + } + + this._APIClientMap.set(key, client); + return client; } /** - * Remove an APIClient object from the environment + * Remove client from map * @param {String} environmentLabel name of the environment + * @param {String} clientType - Type of client (API_CLIENT or GOVERNANCE_CLIENT) */ - destroyAPIClient(environmentLabel) { - this._APIClientMap.delete(environmentLabel); + destroyAPIClient(environmentLabel, clientType = Utils.CONST.API_CLIENT) { + this._APIClientMap.delete(environmentLabel + '_' + clientType); } } diff --git a/portals/admin/src/main/webapp/source/src/app/data/Constants.js b/portals/admin/src/main/webapp/source/src/app/data/Constants.js index cd8c9e9808d..e8a834a941d 100644 --- a/portals/admin/src/main/webapp/source/src/app/data/Constants.js +++ b/portals/admin/src/main/webapp/source/src/app/data/Constants.js @@ -44,6 +44,44 @@ const CONSTS = { }, DEFAULT_SUBSCRIPTIONLESS_PLAN: 'DefaultSubscriptionless', DEFAULT_ASYNC_SUBSCRIPTIONLESS_PLAN: 'AsyncDefaultSubscriptionless', + GOVERNABLE_STATES: [ + { value: 'API_CREATE', label: 'API Create' }, + { value: 'API_UPDATE', label: 'API Update' }, + { value: 'API_DEPLOY', label: 'API Deploy' }, + { value: 'API_PUBLISH', label: 'API Publish' }, + ], + RULESET_TYPES: [ + { value: 'API_DEFINITION', label: 'API Definition' }, + { value: 'API_METADATA', label: 'API Metadata' }, + { value: 'API_DOCUMENTATION', label: 'Documentation' }, + ], + ARTIFACT_TYPES: [ + { value: 'REST_API', label: 'REST API' }, + { value: 'ASYNC_API', label: 'Async API' }, + ], + SEVERITY_LEVELS: [ + { value: 'ERROR', label: 'Error' }, + { value: 'WARN', label: 'Warn' }, + { value: 'INFO', label: 'Info' }, + ], + COMPLIANCE_STATES: [ + { value: 'NOT_APPLICABLE', label: 'Not Applicable' }, + { value: 'COMPLIANT', label: 'Compliant' }, + { value: 'NON_COMPLIANT', label: 'Non Compliant' }, + ], + POLICY_ADHERENCE_STATES: [ + { value: 'FOLLOWED', label: 'Followed' }, + { value: 'VIOLATED', label: 'Violated' }, + { value: 'UNAPPLIED', label: 'Unapplied' }, + ], + RULESET_VALIDATION_STATES: [ + { value: 'PASSED', label: 'Passed' }, + { value: 'FAILED', label: 'Failed' }, + ], + GOVERNANCE_ACTIONS: { + BLOCK: 'BLOCK', + NOTIFY: 'NOTIFY', + }, }; export default CONSTS; diff --git a/portals/admin/src/main/webapp/source/src/app/data/GovernanceAPI.js b/portals/admin/src/main/webapp/source/src/app/data/GovernanceAPI.js new file mode 100644 index 00000000000..65d1473a490 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/data/GovernanceAPI.js @@ -0,0 +1,332 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Utils from './Utils'; +import Resource from './Resource'; +import APIClientFactory from './APIClientFactory'; + +/** + * An abstract representation of GovernanceAPI + */ +class GovernanceAPI extends Resource { + constructor(kwargs) { + super(); + this.client = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), + Utils.CONST.GOVERNANCE_CLIENT).client; + const properties = kwargs; + Utils.deepFreeze(properties); + this._data = properties; + for (const key in properties) { + if (Object.prototype.hasOwnProperty.call(properties, key)) { + this[key] = properties[key]; + } + } + } + + /** + * @param data + * @returns {object} Metadata for API request + * @private + */ + _requestMetaData(data = {}) { + return { + requestContentType: data['Content-Type'] || 'application/json', + }; + } + + /** + * + * Instance method of the API class to provide raw JSON object + * which is API body friendly to use with REST api requests + * Use this method instead of accessing the private _data object for + * converting to a JSON representation of an API object. + * Note: This is deep coping, Use sparingly, Else will have a bad impact on performance + * Basically this is the revers operation in constructor. + * This method simply iterate through all the object properties (excluding the properties in `excludes` list) + * and copy their values to new object. + * So use this method with care!! + * @memberof API + * @param {Array} [userExcludes=[]] List of properties that are need to be excluded from the generated JSON object + * @returns {JSON} JSON representation of the API + */ + toJSON(userExcludes = []) { + var copy = {}, + excludes = ['_data', 'client', 'apiType', ...userExcludes]; + for (var prop in this) { + if (!excludes.includes(prop)) { + copy[prop] = cloneDeep(this[prop]); + } + } + return copy; + } + + /** + * Get list of governance policies + * @returns {Promise} Promised policies response + */ + getPoliciesList() { + return this.client.then((client) => { + return client.apis['Governance Policies'].getGovernancePolicies( + this._requestMetaData(), + ); + }); + } + + /** + * Get governance policy by id + * @param {string} policyId Policy id + * @returns {Promise} Promised policy response + */ + getPolicy(policyId) { + return this.client.then((client) => { + return client.apis['Governance Policies'].getGovernancePolicyById( + { policyId: policyId }, + this._requestMetaData(), + ); + }); + } + + /** + * Add a new governance policy + * @param {Object} policy - Policy object containing the policy configuration + * @returns {Promise} Promise resolving to API response + */ + addPolicy(policy) { + return this.client.then((client) => { + return client.apis['Governance Policies'].createGovernancePolicy( + { 'Content-Type': 'application/json' }, + { requestBody: policy }, + this._requestMetaData(), + ); + }); + } + + /** + * Update an existing governance policy + * @param {Object} policy - Policy object containing the updated policy configuration and ID + * @param {string} policy.id - ID of the policy to update + * @returns {Promise} Promise resolving to API response + */ + updatePolicy(policy) { + return this.client.then((client) => { + return client.apis['Governance Policies'].updateGovernancePolicyById( + { + policyId: policy.id, + 'Content-Type': 'application/json' + }, + { requestBody: policy }, + this._requestMetaData(), + ); + }); + } + + /** + * Delete a governance policy + * @param {string} policyId Policy id + * @returns {Promise} Promised response + */ + deletePolicy(policyId) { + return this.client.then((client) => { + return client.apis['Governance Policies'].deleteGovernancePolicy( + { policyId: policyId }, + this._requestMetaData(), + ); + }); + } + + // rulesets + /** + * Get list of rulesets + * @returns {Promise} Promised rulesets response + */ + getRulesetsList() { + return this.client.then((client) => { + return client.apis['Rulesets'].getRulesets( + this._requestMetaData(), + ); + }); + } + + /** + * Get ruleset by id + * @param {string} rulesetId Ruleset id + * @returns {Promise} Promised ruleset response + */ + getRuleset(rulesetId) { + return this.client.then((client) => { + return client.apis['Rulesets'].getRulesetById( + { rulesetId: rulesetId }, + this._requestMetaData(), + ); + }); + } + + /** + * Get ruleset content by id + * @param {string} rulesetId Ruleset id + * @returns {Promise} Promised ruleset content response + */ + getRulesetContent(rulesetId) { + return this.client.then((client) => { + return client.apis['Rulesets'].getRulesetContent( + { rulesetId: rulesetId }, + this._requestMetaData(), + ); + }); + } + + /** + * Add a new ruleset + * @param {FormData} ruleset Ruleset data including the file + * @returns {Promise} Promise resolving to response + */ + addRuleset(ruleset) { + return this.client.then((client) => { + return client.apis['Rulesets'].createRuleset( + { 'Content-Type': 'multipart/form-data' }, + { requestBody: ruleset }, + ); + }); + } + + /** + * Update a ruleset + * @param {string} id Ruleset ID + * @param {FormData} ruleset Updated ruleset data including the file + * @returns {Promise} Promise resolving to response + */ + updateRuleset(id, ruleset) { + return this.client.then((client) => { + const payload = ruleset; + return client.apis['Rulesets'].updateRulesetById( + { rulesetId: id }, + { requestBody: payload }, + { 'Content-Type': 'multipart/form-data' }, + ); + }); + } + + /** + * Delete a ruleset + * @param {string} rulesetId Ruleset id + * @returns {Promise} Promised response + */ + deleteRuleset(rulesetId) { + return this.client.then((client) => { + return client.apis['Rulesets'].deleteRuleset( + { rulesetId: rulesetId }, + this._requestMetaData(), + ); + }); + } + + /** + * Get policy adherence for all policies + * @returns {Promise} Promised policy adherence response + */ + getPolicyAdherenceForAllPolicies() { + return this.client.then((client) => { + return client.apis['Policy Adherence'].getPolicyAdherenceForAllPolicies( + this._requestMetaData(), + ); + }); + } + + /** + * Get artifact compliance for all artifacts + * @returns {Promise} Promised artifact compliance response + */ + getComplianceStatusListOfAPIs() { + return this.client.then((client) => { + return client.apis['Artifact Compliance'].getComplianceStatusListOfAPIs( + this._requestMetaData(), + ); + }); + } + + /** + * Get artifact compliance by id + * @param {string} artifactId Artifact id + * @param {Object} options Optional parameters including signal for AbortController + * @returns {Promise} Promised artifact compliance response + */ + getComplianceByAPIId(artifactId, options = {}) { + return this.client.then((client) => { + return client.apis['Artifact Compliance'].getComplianceByAPIId( + { apiId: artifactId }, + { ...this._requestMetaData(), signal: options.signal } + ); + }); + } + + /** + * Get policy adherence summary + * @returns {Promise} Promised policy adherence summary response + */ + getPolicyAdherenceSummary() { + return this.client.then((client) => { + return client.apis['Policy Adherence'].getPolicyAdherenceSummary( + this._requestMetaData(), + ); + }); + } + + /** + * Get artifact compliance summary + * @returns {Promise} Promised artifact compliance summary response + */ + getComplianceSummaryForAPIs() { + return this.client.then((client) => { + return client.apis['Artifact Compliance'].getComplianceSummaryForAPIs( + this._requestMetaData(), + ); + }); + } + + /** + * Get policy adherence by id + * @param {string} policyId Policy id + * @returns {Promise} Promised policy adherence response + */ + getPolicyAdherenceByPolicyId(policyId) { + return this.client.then((client) => { + return client.apis['Policy Adherence'].getPolicyAdherenceByPolicyId( + { policyId: policyId }, + this._requestMetaData(), + ); + }); + } + + /** + * Get ruleset validation results by artifact id + * @param {string} artifactId Artifact id + * @param {string} rulesetId Ruleset id + * @returns {Promise} Promised validation results response + */ + getRulesetValidationResultsByAPIId(artifactId, rulesetId) { + return this.client.then((client) => { + return client.apis['Artifact Compliance'].getRulesetValidationResultsByAPIId( + { apiId: artifactId, rulesetId: rulesetId }, + this._requestMetaData(), + ); + }); + } +} + +export default GovernanceAPI; diff --git a/portals/admin/src/main/webapp/source/src/app/data/GovernanceAPIClient.js b/portals/admin/src/main/webapp/source/src/app/data/GovernanceAPIClient.js new file mode 100644 index 00000000000..28298806b8d --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/data/GovernanceAPIClient.js @@ -0,0 +1,213 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import SwaggerClient from 'swagger-client'; +import { Mutex } from 'async-mutex'; +import Configurations from 'Config'; +import AuthManager from 'AppData/AuthManager'; +import Utils from './Utils'; + +/** + * This class expose single swaggerClient instance created using the given swagger URL (Publisher, Store, ect ..) + * it's highly unlikely to change the REST API Swagger definition (swagger.json) file on the fly, + * Hence this singleton class help to preserve consecutive swagger client object creations saving redundant IO + * operations. + */ +class GovernanceAPIClient { + /** + * @param {Object} environment - Environment to get host for the swagger-client's spec property. + * @param {{}} args - Accept as an optional argument for GovernanceAPIClient constructor.Merge the given args with + * default args. + * @returns {GovernanceAPIClient} + */ + constructor(environment, args = {}) { + this.environment = environment || Utils.getCurrentEnvironment(); + SwaggerClient.http.withCredentials = true; + const promisedResolve = SwaggerClient.resolve({ + url: Utils.getGovernanceSwaggerURL(), + requestInterceptor: (request) => { + request.headers.Accept = 'text/yaml'; + }, + }); + GovernanceAPIClient.spec = promisedResolve; + this._client = promisedResolve.then((resolved) => { + const argsv = Object.assign(args, { + spec: this._fixSpec(resolved.spec), + requestInterceptor: this._getRequestInterceptor(), + responseInterceptor: this._getResponseInterceptor(), + }); + SwaggerClient.http.withCredentials = true; + return new SwaggerClient(argsv); + }); + this._client.catch(AuthManager.unauthorizedErrorHandler); + this.mutex = new Mutex(); + } + + /** + * Expose the private _client property to public + * @returns {GovernanceAPIClient} an instance of GovernanceAPIClient class + */ + get client() { + return this._client; + } + + /** + * Get the ETag of a given resource key from the session storage + * @param {String} key - key of resource. + * @returns {String} ETag value for the given key + */ + static getETag(key) { + return sessionStorage.getItem('etag_' + key); + } + + /** + * Add an ETag to a given resource key into the session storage + * @param key {string} key of resource. + * @param etag {string} etag value to be stored against the key + */ + static addETag(key, etag) { + sessionStorage.setItem('etag_' + key, etag); + } + + /** + * Get Scope for a particular resource path + * + * @param resourcePath resource path of the action + * @param resourceMethod resource method of the action + */ + static getScopeForResource(resourcePath, resourceMethod) { + if (!GovernanceAPIClient.spec) { + SwaggerClient.http.withCredentials = true; + GovernanceAPIClient.spec = SwaggerClient.resolve({ url: Utils.getGovernanceSwaggerURL() }); + } + return GovernanceAPIClient.spec.then((resolved) => { + return ( + resolved.spec.paths[resourcePath] + && resolved.spec.paths[resourcePath][resourceMethod] + && resolved.spec.paths[resourcePath][resourceMethod].security[0].OAuth2Security[0] + ); + }); + } + + /** + * Temporary method to fix the hostname attribute Till following issues get fixed ~tmkb + * https://github.com/swagger-api/swagger-js/issues/1081 + * https://github.com/swagger-api/swagger-js/issues/1045 + * @param spec {JSON} : Json object of the specification + * @returns {JSON} : Fixed specification + * @private + */ + _fixSpec(spec) { + const updatedSpec = spec; + const url = new URL(spec.servers[0].url); + if (this.environment.host !== url.host) { + url.host = this.environment.host; + if (Configurations.app.proxy_context_path && Configurations.app.proxy_context_path !== '') { + url.pathname = Configurations.app.proxy_context_path + url.pathname; + } + updatedSpec.servers[0].url = String(url); + } + return updatedSpec; + } + + _getResponseInterceptor() { + return (data) => { + if (data.headers.etag) { + GovernanceAPIClient.addETag(data.url, data.headers.etag); + } + + // If an unauthenticated response is received, we check whether the token is valid by introspecting it. + // If it is not valid, we need to clear the stored tokens (in cookies etc) in the browser by redirecting the + // user to logout. + if (data.status === 401 && data.body != null && data.body.description === 'Unauthenticated request') { + const userData = AuthManager.getUserFromToken(); + userData.catch((error) => { + console.error('Error occurred while checking token status. Hence redirecting to login', error); + window.location = Configurations.app.context + Utils.CONST.LOGOUT_CALLBACK; + }); + } + return data; + }; + } + + /** + * + * + * @returns + * @memberof GovernanceAPIClient + */ + _getRequestInterceptor() { + return (request) => { + const existingUser = AuthManager.getUser(this.environment.label); + if (!existingUser) { + console.log('User not found. Token refreshing failed.'); + return request; + } + let existingToken = AuthManager.getUser(this.environment.label).getPartialToken(); + const refToken = AuthManager.getUser(this.environment.label).getRefreshPartialToken(); + if (existingToken) { + request.headers.authorization = 'Bearer ' + existingToken; + return request; + } else { + console.log('Access token is expired. Trying to refresh.'); + if (!refToken) { + console.log('Refresh token not found. Token refreshing failed.'); + return request; + } + } + + const env = this.environment; + const promise = new Promise((resolve, reject) => { + this.mutex.acquire().then((release) => { + existingToken = AuthManager.getUser(env.label).getPartialToken(); + if (existingToken) { + request.headers.authorization = 'Bearer ' + existingToken; + release(); + resolve(request); + } else { + AuthManager.refresh(env).then((res) => res.json()) + .then(() => { + request.headers.authorization = 'Bearer ' + + AuthManager.getUser(env.label).getPartialToken(); + release(); + resolve(request); + }).catch((error) => { + console.error('Error:', error); + release(); + reject(); + }) + .finally(() => { + release(); + }); + } + }); + }); + + if (GovernanceAPIClient.getETag(request.url) + && (request.method === 'PUT' || request.method === 'DELETE' || request.method === 'POST')) { + request.headers['If-Match'] = GovernanceAPIClient.getETag(request.url); + } + return promise; + }; + } +} + +GovernanceAPIClient.spec = null; + +export default GovernanceAPIClient; diff --git a/portals/admin/src/main/webapp/source/src/app/data/Utils.js b/portals/admin/src/main/webapp/source/src/app/data/Utils.js index ad9a918d86e..f801140eb2e 100644 --- a/portals/admin/src/main/webapp/source/src/app/data/Utils.js +++ b/portals/admin/src/main/webapp/source/src/app/data/Utils.js @@ -16,6 +16,7 @@ * under the License. */ import Configurations from 'Config'; +import CONSTS from 'AppData/Constants'; /** * Utility class for Admin Portal application */ @@ -193,6 +194,25 @@ class Utils { } } + /** + * Get governance swagger definition URL + * @static + * @returns + * @memberof Utils + */ + static getGovernanceSwaggerURL() { + if (Configurations.app.proxy_context_path) { + return 'https://' + + Utils.getCurrentEnvironment().host + + Configurations.app.proxy_context_path + + Utils.CONST.GOVERNANCE_SWAGGER_JSON; + } else { + return 'https://' + + Utils.getCurrentEnvironment().host + + Utils.CONST.GOVERNANCE_SWAGGER_JSON; + } + } + /** * Return the time difference between the current time and the given time in the Date object in seconds * @param targetTime {Date|Integer} Date object which needs to be compared with current time @@ -292,6 +312,69 @@ class Utils { } } + /** + * Maps a governable state value to its label + * @param {string} stateValue The value of the governable state + * @returns {string} The label of the governable state + * @memberof Utils + */ + static mapGovernableStateToLabel(value) { + const state = CONSTS.GOVERNABLE_STATES.find((t) => t.value === value); + return state?.label || value; + } + + /** + * Maps rule type value to its label + * @param {String} value - The value to be mapped + * @returns {String} The corresponding label + */ + static mapRuleTypeToLabel(value) { + const ruleType = CONSTS.RULESET_TYPES.find((t) => t.value === value); + return ruleType?.label || value; + } + + /** + * Maps artifact type value to its label + * @param {String} value - The value to be mapped + * @returns {String} The corresponding label + */ + static mapArtifactTypeToLabel(value) { + const artifactType = CONSTS.ARTIFACT_TYPES.find((t) => t.value === value); + return artifactType?.label || value; + } + + /** + * Maps a compliance state to its display label + * @param {string} state The compliance state value + * @returns {string} The display label + */ + static mapComplianceStateToLabel(state) { + const complianceState = CONSTS.COMPLIANCE_STATES.find((t) => t.value === state); + return complianceState?.label || state; + } + + /** + * Maps a policy adherence state value to its label + * @param {string} state The value of the policy adherence state + * @returns {string} The label of the policy adherence state + * @memberof Utils + */ + static mapPolicyAdherenceStateToLabel(state) { + const policyState = CONSTS.POLICY_ADHERENCE_STATES.find((t) => t.value === state); + return policyState?.label || state; + } + + /** + * Maps a ruleset validation state value to its label + * @param {string} state The value of the ruleset validation state + * @returns {string} The label of the ruleset validation state + * @memberof Utils + */ + static mapRulesetValidationStateToLabel(state) { + const validationState = CONSTS.RULESET_VALIDATION_STATES.find((t) => t.value === state); + return validationState?.label || state; + } + /** * Force file download in browser * @@ -346,7 +429,10 @@ Utils.CONST = { LOGOUT_CALLBACK: '/services/auth/callback/logout', INTROSPECT: '/services/auth/introspect', SWAGGER_JSON: '/api/am/admin/v4/swagger.yaml', + GOVERNANCE_SWAGGER_JSON: '/api/am/governance/v1/swagger.yaml', PROTOCOL: 'https://', + API_CLIENT: 'API_CLIENT', + GOVERNANCE_CLIENT: 'GOVERNANCE_CLIENT', }; /** diff --git a/portals/admin/src/main/webapp/source/src/app/data/api.js b/portals/admin/src/main/webapp/source/src/app/data/api.js index a6ab253308b..cb4a1a47001 100644 --- a/portals/admin/src/main/webapp/source/src/app/data/api.js +++ b/portals/admin/src/main/webapp/source/src/app/data/api.js @@ -284,6 +284,17 @@ class API extends Resource { }); } + /** + * Get list of labels + */ + labelsListGet() { + return this.client.then((client) => { + return client.apis['Labels (Collection)'].getAllLabels( + this._requestMetaData(), + ); + }); + } + /** * Get Application Throttling Policies */ diff --git a/portals/devportal/src/main/webapp/site/public/locales/en.json b/portals/devportal/src/main/webapp/site/public/locales/en.json index 0381a7987b2..e498d55323a 100644 --- a/portals/devportal/src/main/webapp/site/public/locales/en.json +++ b/portals/devportal/src/main/webapp/site/public/locales/en.json @@ -161,6 +161,7 @@ "Apis.Details.Credentials.Credentials.subscribe.to.application.sign.in": "Sign In to Subscribe", "Apis.Details.Credentials.Credentials.subscribed.successfully": "Subscribed successfully", "Apis.Details.Credentials.Credentials.subscription.deleted.successfully": "Subscription deleted successfully!", + "Apis.Details.Credentials.Credentials.subscription.request.created": "Subscription Deletion Request Created!", "Apis.Details.Credentials.Credentials.visit.original.developer.portal": "Visit Original Developer Portal", "Apis.Details.Credentials.OriginalDevportalDetails.original.developer.portal.title": "Original Developer Portal", "Apis.Details.Credentials.OriginalDevportalDetails.visit.original.developer.portal": "Visit Original Developer Portal", diff --git a/portals/publisher/src/main/webapp/package-lock.json b/portals/publisher/src/main/webapp/package-lock.json index 7c5ffee87fb..b40d62ee0e1 100644 --- a/portals/publisher/src/main/webapp/package-lock.json +++ b/portals/publisher/src/main/webapp/package-lock.json @@ -23,6 +23,7 @@ "@mui/icons-material": "^5.15.6", "@mui/lab": "^5.0.0-alpha.162", "@mui/material": "^5.15.6", + "@mui/x-charts": "^7.24.0", "@stoplight/elements": "^8.3.3", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-rulesets": "^1.19.1", @@ -2203,9 +2204,10 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -4222,11 +4224,12 @@ } }, "node_modules/@mui/types": { - "version": "7.2.14", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", - "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", + "version": "7.2.21", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz", + "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==", + "license": "MIT", "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -4261,6 +4264,156 @@ } } }, + "node_modules/@mui/x-charts": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.24.1.tgz", + "integrity": "sha512-OdTS/nXaANPe4AoUFIDD4LlID8kK/00q+uqVOCkVClEvFQeAkj3pBaghdS4hY7rVqsCgsm+yOStQVJa9G2MR+Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-charts-vendor": "7.20.0", + "@mui/x-internals": "7.24.1", + "@react-spring/rafz": "^9.7.5", + "@react-spring/web": "^9.7.5", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts-vendor": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz", + "integrity": "sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg==", + "license": "MIT AND ISC", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@types/d3-color": "^3.1.3", + "@types/d3-delaunay": "^6.0.4", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-scale": "^4.0.8", + "@types/d3-shape": "^3.1.6", + "@types/d3-time": "^3.0.3", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "delaunator": "^5.0.1", + "robust-predicates": "^3.0.2" + } + }, + "node_modules/@mui/x-charts/node_modules/@mui/utils": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.2.tgz", + "integrity": "sha512-5NkhzlJkmR5+5RSs/Irqin1GPy2Z8vbLk/UzQrH9FEAnm6OA9SvuXjzgklxUs7N65VwEkGpKK1jMZ5K84hRdzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/types": "^7.2.21", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts/node_modules/react-is": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", + "license": "MIT" + }, + "node_modules/@mui/x-internals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.1.tgz", + "integrity": "sha512-9BvJzpLJnS9BDphvkiv6v0QOLxbnu8jhwcexFjtCQ2ZyxtVuVsWzGZ2npT9sGOil7+eaFDmWnJtea/tgrPvSwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-internals/node_modules/@mui/utils": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.2.tgz", + "integrity": "sha512-5NkhzlJkmR5+5RSs/Irqin1GPy2Z8vbLk/UzQrH9FEAnm6OA9SvuXjzgklxUs7N65VwEkGpKK1jMZ5K84hRdzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/types": "^7.2.21", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals/node_modules/react-is": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", + "license": "MIT" + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -4497,6 +4650,78 @@ "react": ">=16.8" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@react-types/checkbox": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.8.1.tgz", @@ -6954,6 +7179,57 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -7306,9 +7582,10 @@ "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" }, "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" }, "node_modules/@types/protocol-buffers-schema": { "version": "3.4.3", @@ -10614,6 +10891,121 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -10973,6 +11365,15 @@ "rimraf": "bin.js" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -14410,6 +14811,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -23463,6 +23873,12 @@ "inherits": "^2.0.1" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rtl-css-js": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", diff --git a/portals/publisher/src/main/webapp/package.json b/portals/publisher/src/main/webapp/package.json index 6cba691d6e8..645ac2c7a51 100644 --- a/portals/publisher/src/main/webapp/package.json +++ b/portals/publisher/src/main/webapp/package.json @@ -45,6 +45,7 @@ "@mui/icons-material": "^5.15.6", "@mui/lab": "^5.0.0-alpha.162", "@mui/material": "^5.15.6", + "@mui/x-charts": "^7.24.0", "@stoplight/elements": "^8.3.3", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-rulesets": "^1.19.1", diff --git a/portals/publisher/src/main/webapp/services/dev_proxy/auth_login.js b/portals/publisher/src/main/webapp/services/dev_proxy/auth_login.js index 7e6ba1cb848..82d5ce44bef 100644 --- a/portals/publisher/src/main/webapp/services/dev_proxy/auth_login.js +++ b/portals/publisher/src/main/webapp/services/dev_proxy/auth_login.js @@ -132,6 +132,13 @@ const setResponseSessionCookies = (res, accessToken, refreshToken, idToken, sess maxAge, }); + res.cookie('AM_REF_TOKEN_DEFAULT_P2', refreshTokenPart2, { + path: '/api/am/governance/', + httpOnly: true, + secure: true, + maxAge, + }); + res.cookie('AM_REF_TOKEN_DEFAULT_P2', refreshTokenPart2, { path: '/publisher', httpOnly: true, diff --git a/portals/publisher/src/main/webapp/services/login/login_callback.jsp b/portals/publisher/src/main/webapp/services/login/login_callback.jsp index 8c529fd735b..667db29e860 100644 --- a/portals/publisher/src/main/webapp/services/login/login_callback.jsp +++ b/portals/publisher/src/main/webapp/services/login/login_callback.jsp @@ -173,6 +173,13 @@ cookie.setMaxAge((int) expiresIn); response.addCookie(cookie); + cookie = new Cookie("AM_PUBLISHER_ACC_TOKEN_DEFAULT_P2", accessTokenPart2); + cookie.setPath(proxyContext != null ? proxyContext + "/api/am/governance/" : "/api/am/governance/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setMaxAge((int) expiresIn); + response.addCookie(cookie); + cookie = new Cookie("AM_REF_TOKEN_DEFAULT_P2", refreshTokenPart2); cookie.setPath(context + "/"); cookie.setHttpOnly(true); diff --git a/portals/publisher/src/main/webapp/services/logout/logout_callback.jsp b/portals/publisher/src/main/webapp/services/logout/logout_callback.jsp index 780250f28da..8b4b491a101 100644 --- a/portals/publisher/src/main/webapp/services/logout/logout_callback.jsp +++ b/portals/publisher/src/main/webapp/services/logout/logout_callback.jsp @@ -41,6 +41,13 @@ cookie.setMaxAge(2); response.addCookie(cookie); + cookie = new Cookie("AM_PUBLISHER_ACC_TOKEN_DEFAULT_P2", ""); + cookie.setPath("/api/am/governance/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setMaxAge(2); + response.addCookie(cookie); + cookie = new Cookie("AM_REF_TOKEN_DEFAULT_P2", ""); cookie.setPath(context + "/"); cookie.setHttpOnly(true); diff --git a/portals/publisher/src/main/webapp/services/refresh/refresh.jsp b/portals/publisher/src/main/webapp/services/refresh/refresh.jsp index 85b6a8fd956..eff36eaa8ae 100644 --- a/portals/publisher/src/main/webapp/services/refresh/refresh.jsp +++ b/portals/publisher/src/main/webapp/services/refresh/refresh.jsp @@ -142,6 +142,13 @@ cookie.setMaxAge((int) expiresIn); response.addCookie(cookie); + cookie = new Cookie("AM_PUBLISHER_ACC_TOKEN_DEFAULT_P2", accessTokenPart2); + cookie.setPath("/api/am/governance/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setMaxAge((int) expiresIn); + response.addCookie(cookie); + cookie = new Cookie("AM_ACC_TOKEN_DEFAULT_P2", accessTokenPart2); cookie.setPath("/api/am/service-catalog/v1/"); cookie.setHttpOnly(true); diff --git a/portals/publisher/src/main/webapp/site/public/locales/en.json b/portals/publisher/src/main/webapp/site/public/locales/en.json index c26cc8fae93..9358b5863c5 100644 --- a/portals/publisher/src/main/webapp/site/public/locales/en.json +++ b/portals/publisher/src/main/webapp/site/public/locales/en.json @@ -5,6 +5,9 @@ "Active.Deployment.Available": "An active deployment is available", "Active.Deployments.Available": "Active deployments are available", "Adding.Policy.Mapping.Error": "Error occurred while adding the policy mapping", + "AdminPages.Addons.ListBase.noDataError": "Error while retrieving data.", + "AdminPages.Addons.ListBase.nodata.message": "No items yet", + "AdminPages.Addons.ListBase.reload": "Reload", "Api.category.dropdown.tooltip": "Allow to group APIs that have similar attributes. There has to be pre-defined API categories in the environment in order to be attached to an API.", "Api.login.page.readonly.user": "Read only", "Apis.APIProductCreateWrapper.error.errorMessage.create.api.product": "Something went wrong while adding the API Product", @@ -352,6 +355,37 @@ "Apis.Details.Comments.reply.delete.success": "Reply comment has been successfully deleted", "Apis.Details.Comments.retrieve.error": "Something went wrong while retrieving comments", "Apis.Details.Comments.title": "Comments", + "Apis.Details.Compliance.PolicyAdherence.column.policy": "Policy", + "Apis.Details.Compliance.PolicyAdherence.column.rulesets": "Rulesets", + "Apis.Details.Compliance.PolicyAdherence.column.status": "Status", + "Apis.Details.Compliance.PolicyAdherence.empty.helper": "No governance policies have been applied to this API.", + "Apis.Details.Compliance.PolicyAdherence.empty.title": "No Policies Applied", + "Apis.Details.Compliance.PolicyAdherence.followed.count": "{followed}/{total} Followed", + "Apis.Details.Compliance.RuleViolation.column.description": "Description", + "Apis.Details.Compliance.RuleViolation.column.message": "Message", + "Apis.Details.Compliance.RuleViolation.column.path": "Path", + "Apis.Details.Compliance.RuleViolation.column.rule": "Rule", + "Apis.Details.Compliance.RuleViolation.empty.errors": "No Error violations found", + "Apis.Details.Compliance.RuleViolation.empty.info": "No Info violations found", + "Apis.Details.Compliance.RuleViolation.empty.passed": "No Passed rules found", + "Apis.Details.Compliance.RuleViolation.empty.warnings": "No Warning violations found", + "Apis.Details.Compliance.RuleViolation.tab.errors": "Errors ({count})", + "Apis.Details.Compliance.RuleViolation.tab.info": "Info ({count})", + "Apis.Details.Compliance.RuleViolation.tab.passed": "Passed ({count})", + "Apis.Details.Compliance.RuleViolation.tab.warnings": "Warnings ({count})", + "Apis.Details.Compliance.RulesetAdherence.column.ruleset": "Ruleset", + "Apis.Details.Compliance.RulesetAdherence.column.status": "Status", + "Apis.Details.Compliance.RulesetAdherence.column.violations": "Violations", + "Apis.Details.Compliance.RulesetAdherence.empty.helper": "No governance rulesets have been applied for this API.", + "Apis.Details.Compliance.RulesetAdherence.empty.title": "No Rulesets Found", + "Apis.Details.Compliance.RulesetAdherence.violations.tooltip": "Errors: {error}, Warnings: {warn}, Info: {info}", + "Apis.Details.Compliance.failed": "Failed", + "Apis.Details.Compliance.passed": "Passed", + "Apis.Details.Compliance.policy.adherence.summary": "Policy Adherence Summary", + "Apis.Details.Compliance.revision.message": "Compliance summary is not available for API revisions. Please navigate to the current API version to view the compliance summary.", + "Apis.Details.Compliance.ruleset.adherence": "Ruleset Adherence", + "Apis.Details.Compliance.ruleset.adherence.summary": "Ruleset Adherence Summary", + "Apis.Details.Compliance.topic.header": "Compliance Summary", "Apis.Details.Components.SOAP.To.REST.edit.btn": "Edit", "Apis.Details.Components.SOAP.To.REST.tabs.In.text": "In", "Apis.Details.Components.SOAP.To.REST.tabs.Out.text": "Out", @@ -914,6 +948,7 @@ "Apis.Details.Environments.Environments.pending.chip": "Pending", "Apis.Details.Environments.Environments.revision\n .description.deploy": "Add a description to the revision", "Apis.Details.Environments.Environments.revision.create.error": "Something went wrong while creating the revision", + "Apis.Details.Environments.Environments.revision.create.error.governance": "Revision Creation failed. Governance policy violations found", "Apis.Details.Environments.Environments.revision.create.heading": "Create New Revision (From Current API)", "Apis.Details.Environments.Environments.revision.create.success": "Revision Created Successfully", "Apis.Details.Environments.Environments.revision.delete": "Delete", @@ -953,6 +988,7 @@ "Apis.Details.Environments.Environments.undeploy.btn": "Undeploy", "Apis.Details.Environments.Environments.visibility.in.devportal": "Gateway URL Visibility", "Apis.Details.Environments.Environments.visibility.permission": "Gateway Environment Visibility in Developer Portal.", + "Apis.Details.Environments.GovernanceViolations.title": "Governance Rule Violations", "Apis.Details.Environments.deploy.api.gateways.text": "API Gateways", "Apis.Details.Environments.deploy.env.text": "Deploy API to the Gateway Environment", "Apis.Details.Environments.deploy.text": "Deploy the API", @@ -1003,6 +1039,7 @@ "Apis.Details.LifeCycle.LifeCycleUpdate.approve.approveStatus": "Lifecycle state change action approved successfully", "Apis.Details.LifeCycle.LifeCycleUpdate.error": "Something went wrong while updating the lifecycle", "Apis.Details.LifeCycle.LifeCycleUpdate.error.certs": "Error while retrieving certificates", + "Apis.Details.LifeCycle.LifeCycleUpdate.error.governance": "Lifecycle update failed. Governance policy violations found", "Apis.Details.LifeCycle.LifeCycleUpdate.reject.rejectStatus": "Lifecycle state change action rejected due to validation failure", "Apis.Details.LifeCycle.LifeCycleUpdate.success.createStatus": "Lifecycle state change request has been sent", "Apis.Details.LifeCycle.LifeCycleUpdate.success.otherStatus": "Lifecycle state updated successfully", @@ -1611,6 +1648,7 @@ "Apis.Details.index.asyncApi.definition": "AsyncAPI Definition", "Apis.Details.index.business.info": "business info", "Apis.Details.index.comments": "Comments", + "Apis.Details.index.compliance": "compliance", "Apis.Details.index.deploy.title": "Deploy", "Apis.Details.index.design.api.configs.title": "API Configurations", "Apis.Details.index.design.api.configs.title.tooltip": "If you make any changes to the API configuration, you need to redeploy the API to see updates in the API Gateway.", @@ -1875,6 +1913,7 @@ "Mui.data.table.pagination.display.tool.print": "Print", "Mui.data.table.pagination.display.tool.view.columns": "\"View Columns\"", "Mui.data.table.pagination.rows.per.page": "Rows per page:", + "Mui.data.table.search.no.records.found": "Sorry, no matching records found", "Polcies.TextField.Description": "Description", "Polcies.TextField.Name": "Name", "Policy.Delete.Error": "Error while deleting the policy", @@ -2043,6 +2082,7 @@ "Task.SubscriptionCreation.table.button.reject": "Reject", "Task.SubscriptionUpdate.table.button.reject": "Reject", "Task.title": "Tasks", + "Throttling.Advanced.AddEdit.form.actions.label": "Actions", "Undeploy": "Undeploy", "UnexpectedError.logout": "Logout", "UnexpectedError.message": "Error occurred due to invalid request", diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Addons/Addons/ListBase.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Addons/Addons/ListBase.jsx new file mode 100644 index 00000000000..328c2ca05af --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Addons/Addons/ListBase.jsx @@ -0,0 +1,443 @@ +/* eslint-disable react/jsx-props-no-spreading */ +/* + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useLayoutEffect, useState } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import PropTypes from 'prop-types'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Grid'; +import TextField from '@mui/material/TextField'; +import Tooltip from '@mui/material/Tooltip'; +import IconButton from '@mui/material/IconButton'; +import SearchIcon from '@mui/icons-material/Search'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import MUIDataTable from 'mui-datatables'; +import ContentBase from 'AppComponents/Addons/Addons/ContentBase'; +import InlineProgress from 'AppComponents/Addons/Addons/InlineProgress'; +import { Link as RouterLink } from 'react-router-dom'; +import EditIcon from '@mui/icons-material/Edit'; +import Alert from '@mui/material/Alert'; + +/** + * Render a list + * @param {JSON} props props passed from parent + * @returns {JSX} Header AppBar components. + */ +function ListBase(props) { + const { + EditComponent, editComponentProps, DeleteComponent, showActionColumn, + columnProps, pageProps, addButtonProps, addButtonOverride, + searchProps: { active: searchActive, searchPlaceholder }, apiCall, emptyBoxProps: { + title: emptyBoxTitle, + content: emptyBoxContent, + }, + noDataMessage, + addedActions, + enableCollapsable, + renderExpandableRow, + useContentBase, + } = props; + + const [searchText, setSearchText] = useState(''); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const intl = useIntl(); + + const filterData = (event) => { + setSearchText(event.target.value); + }; + + const sortBy = (field, reverse, primer) => { + const key = primer + ? (x) => { + return primer(x[field]); + } + : (x) => { + return x[field]; + }; + + // eslint-disable-next-line no-param-reassign + reverse = !reverse ? 1 : -1; + + return (a, b) => { + const aValue = key(a); + const bValue = key(b); + return reverse * ((aValue > bValue) - (bValue > aValue)); + }; + }; + const onColumnSortChange = (changedColumn, direction) => { + const sorted = [...data].sort(sortBy(changedColumn, direction === 'descending')); + setData(sorted); + }; + + const fetchData = () => { + // Fetch data from backend when an apiCall is provided + setData(null); + if (apiCall) { + const promiseAPICall = apiCall(); + promiseAPICall.then((LocalData) => { + if (LocalData) { + setData(LocalData); + setError(null); + } else { + setError(intl.formatMessage({ + id: 'AdminPages.Addons.ListBase.noDataError', + defaultMessage: 'Error while retrieving data.', + })); + } + }) + .catch((e) => { + setError(e.message); + }); + } + setSearchText(''); + }; + + useEffect(() => { + fetchData(); + }, []); + + useLayoutEffect(() => { + let i; + const sortButtonList = document.getElementsByClassName('MuiTableSortLabel-root'); + const footerList = document.getElementsByClassName('MuiTable-root'); + + for (i = 0; i < sortButtonList.length; i++) { + sortButtonList[i].setAttribute('aria-label', `sort-icon-button-${i}`); + } + + if (footerList.length > 1) footerList[1].setAttribute('role', 'presentation'); + }); + + let columns = []; + if (columnProps) { + columns = [ + ...columnProps, + ]; + } + if (showActionColumn) { + columns.push( + { + name: '', + label: , + options: { + filter: false, + sort: false, + customBodyRender: (value, tableMeta) => { + const dataRow = data[tableMeta.rowIndex]; + const itemName = (typeof tableMeta.rowData === 'object') ? tableMeta.rowData[0] : ''; + if (editComponentProps && editComponentProps.routeTo) { + if (typeof tableMeta.rowData === 'object') { + const artifactId = tableMeta.rowData[tableMeta.rowData.length - 2]; + const isAI = tableMeta.rowData[1] === 'AI API Quota'; + return ( +
+ + + + + + {DeleteComponent && ( + + )} + {addedActions && addedActions.map((action) => { + const AddedComponent = action; + return ( + + ); + })} +
+ ); + } else { + return (
); + } + } + return ( +
+ {EditComponent && ( + + )} + {DeleteComponent && ()} + {addedActions && addedActions.map((action) => { + const AddedComponent = action; + return ( + + ); + })} +
+ + ); + }, + setCellProps: () => { + return { + style: { width: 150 }, + }; + }, + }, + }, + ); + } + const options = { + filterType: 'checkbox', + selectableRows: 'none', + filter: false, + search: false, + print: false, + download: false, + viewColumns: false, + customToolbar: null, + responsive: 'vertical', + searchText, + rowsPerPageOptions: [5, 10, 25, 50, 100], + onColumnSortChange, + textLabels: { + body: { + noMatch: intl.formatMessage({ + id: 'Mui.data.table.search.no.records.found', + defaultMessage: 'Sorry, no matching records found', + }), + }, + pagination: { + rowsPerPage: intl.formatMessage({ + id: 'Mui.data.table.pagination.rows.per.page', + defaultMessage: 'Rows per page:', + }), + displayRows: intl.formatMessage({ + id: 'Mui.data.table.pagination.display.rows', + defaultMessage: 'of', + }), + }, + }, + expandableRows: enableCollapsable, + renderExpandableRow, + ...props.options, + }; + + // If no apiCall is provided OR, + // retrieved data is empty, display an information card. + if (!apiCall || (data && data.length === 0)) { + const content = ( + + + {emptyBoxTitle} + {emptyBoxContent} + + + {addButtonOverride || ( + EditComponent && () + )} + + + ); + + return useContentBase ? ( + {content} + ) : content; + } + + // If apiCall is provided and data is not retrieved yet, display progress component + if (!error && apiCall && !data) { + const content = ; + return useContentBase ? ( + {content} + ) : content; + } + + if (error) { + const content = {error}; + return useContentBase ? ( + {content} + ) : content; + } + + const mainContent = ( + <> + {(searchActive || addButtonProps) && ( + + + + + + {searchActive && ()} + + + {searchActive && ( + ({ + '& .search-input': { + fontSize: theme.typography.fontSize, + }, + })} + InputProps={{ + disableUnderline: true, + className: 'search-input', + }} + // eslint-disable-next-line react/jsx-no-duplicate-props + inputProps={{ + 'aria-label': 'search-by-policy', + }} + onChange={filterData} + value={searchText} + /> + )} + + + {addButtonOverride || ( + EditComponent && ( + + ) + )} + + )} + > + + + + + + + + + )} +
+ {data && data.length > 0 && ( + + )} +
+ {data && data.length === 0 && ( +
+ + {noDataMessage} + +
+ )} + + ); + + return useContentBase ? ( + {mainContent} + ) : mainContent; +} + +ListBase.defaultProps = { + addButtonProps: {}, + addButtonOverride: null, + searchProps: { + searchPlaceholder: '', + active: true, + }, + actionColumnProps: { + editIconShow: true, + editIconOverride: null, + deleteIconShow: true, + }, + addedActions: null, + noDataMessage: ( + + ), + showActionColumn: true, + apiCall: null, + EditComponent: null, + DeleteComponent: null, + editComponentProps: {}, + columnProps: null, + enableCollapsable: false, + renderExpandableRow: null, + useContentBase: true, + options: {}, +}; + +ListBase.propTypes = { + EditComponent: PropTypes.element, + editComponentProps: PropTypes.shape({}), + DeleteComponent: PropTypes.element, + showActionColumn: PropTypes.bool, + columnProps: PropTypes.element, + pageProps: PropTypes.shape({}).isRequired, + addButtonProps: PropTypes.shape({}), + searchProps: PropTypes.shape({ + searchPlaceholder: PropTypes.string.isRequired, + active: PropTypes.bool.isRequired, + }), + apiCall: PropTypes.func, + emptyBoxProps: PropTypes.shape({ + title: PropTypes.element.isRequired, + content: PropTypes.element.isRequired, + }).isRequired, + actionColumnProps: PropTypes.shape({ + editIconShow: PropTypes.bool, + editIconOverride: PropTypes.element, + deleteIconShow: PropTypes.bool, + }), + noDataMessage: PropTypes.element, + addButtonOverride: PropTypes.element, + addedActions: PropTypes.shape([]), + enableCollapsable: PropTypes.bool, + renderExpandableRow: PropTypes.func, + useContentBase: PropTypes.bool, + options: PropTypes.shape({}), +}; +export default ListBase; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/Compliance.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/Compliance.jsx new file mode 100644 index 00000000000..a74d6042132 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/Compliance.jsx @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useState } from 'react'; +import { styled } from '@mui/material/styles'; +import { Grid, Card, CardContent, Typography } from '@mui/material'; +import DonutChart from 'AppComponents/Shared/DonutChart'; +import { FormattedMessage, useIntl } from 'react-intl'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import { useAPI } from 'AppComponents/Apis/Details/components/ApiContext'; +import PolicyAdherenceSummaryTable from './PolicyAdherenceSummaryTable'; +import RulesetAdherenceSummaryTable from './RulesetAdherenceSummaryTable'; +import RuleViolationSummary from './RuleViolationSummary'; + +const PREFIX = 'Compliance'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.root}`]: { + ...theme.mixins.gutters(), + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + }, +})); + +export default function Compliance() { + const intl = useIntl(); + const [api] = useAPI(); + const artifactId = api.id; + const [statusCounts, setStatusCounts] = useState({ passed: 0, failed: 0 }); + + useEffect(() => { + // Skip the API call if this is a revision + if (api.isRevision) { + return undefined; + } + + const abortController = new AbortController(); + const restApi = new GovernanceAPI(); + + restApi.getComplianceByAPIId(artifactId, { signal: abortController.signal }) + .then((response) => { + const rulesetMap = new Map(); + + response.body.governedPolicies.forEach(policy => { + policy.rulesetValidationResults.forEach(result => { + // If ruleset not in map or if existing result is older, update the map + if (!rulesetMap.has(result.id)) { + rulesetMap.set(result.id, result); + } + }); + }); + + // Count statuses from unique rulesets + const counts = Array.from(rulesetMap.values()).reduce((acc, result) => { + if (result.status === 'PASSED') acc.passed += 1; + if (result.status === 'FAILED') acc.failed += 1; + return acc; + }, { passed: 0, failed: 0 }); + + setStatusCounts(counts); + }) + .catch((error) => { + if (!abortController.signal.aborted) { + console.error('Error fetching ruleset adherence data:', error); + setStatusCounts({ passed: 0, failed: 0 }); + } + }); + + return () => { + abortController.abort(); + }; + }, [artifactId, api.isRevision]); + + if (api.isRevision) { + return ( + + + + + + {/* Rule Violation Summary section */} + + + + + + + + + + ); + } + + return ( + + + + + + {/* Rule Violation Summary section */} + + + + + + + + + {/* Policy Adherence Summary section */} + + + + + + + + + + + + {/* Ruleset Adherence Summary section */} + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/PolicyAdherenceSummaryTable.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/PolicyAdherenceSummaryTable.jsx new file mode 100644 index 00000000000..c05f6902c76 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/PolicyAdherenceSummaryTable.jsx @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Typography, Chip, Box, LinearProgress , TableRow, TableCell } from '@mui/material'; +import ListBase from 'AppComponents/Addons/Addons/ListBase'; +import Stack from '@mui/material/Stack'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CancelIcon from '@mui/icons-material/Cancel'; +import { useIntl } from 'react-intl'; +import PolicyIcon from '@mui/icons-material/Policy'; + +import GovernanceAPI from 'AppData/GovernanceAPI'; +import Utils from 'AppData/Utils'; + +export default function PolicyAdherenceSummaryTable({ artifactId }) { + const intl = useIntl(); + + /** + * API call to get Policies + * @returns {Promise}. + */ + function apiCall() { + const restApi = new GovernanceAPI(); + return restApi + .getComplianceByAPIId(artifactId) + .then((result) => { + return result.body.governedPolicies; + }) + .catch((error) => { + if (error.status === 404) { + return []; + } + throw error; + }); + } + + const renderProgress = (followed, total) => { + const percentage = (followed / total) * 100; + const isComplete = followed === total; + + return ( + + + + {intl.formatMessage({ + id: 'Apis.Details.Compliance.PolicyAdherence.followed.count', + defaultMessage: '{followed}/{total} Followed', + }, { followed, total })} + + + + + ); + }; + + const renderExpandableRow = (rowData) => { + const rulesets = rowData[3]; + return ( + + + + + {rulesets.map((ruleset) => ( + + {ruleset.status === 'PASSED' ? + : + + } + + {ruleset.name} + + + ))} + + + + ); + }; + + const policyColumnProps = [ + { + name: 'id', + options: { display: false } + }, + { + name: 'name', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.PolicyAdherence.column.policy', + defaultMessage: 'Policy', + }), + options: { + width: '30%', + customBodyRender: (value) => ( + {value} + ), + setCellProps: () => ({ + style: { width: '30%' }, + }), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + { + name: 'status', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.PolicyAdherence.column.status', + defaultMessage: 'Status', + }), + options: { + setCellProps: () => ({ + style: { width: '20%' }, + }), + customBodyRender: (value) => { + const getChipColor = (status) => { + if (status === 'FOLLOWED') return 'success'; + if (status === 'VIOLATED') return 'error'; + return 'default'; + }; + return ( + + ); + }, + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + { + name: 'rulesetValidationResults', + options: { display: false } + }, + { + name: 'rulesetsList', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.PolicyAdherence.column.rulesets', + defaultMessage: 'Rulesets', + }), + options: { + customBodyRender: (value, tableMeta) => { + const rulesets = tableMeta.rowData[3]; + const total = rulesets.length; + const followed = rulesets.filter((ruleset) => ruleset.status === 'PASSED').length; + return renderProgress(followed, total); + }, + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + } + }), + } + }, + ]; + + const emptyStateContent = ( + + + + {intl.formatMessage({ + id: 'Apis.Details.Compliance.PolicyAdherence.empty.title', + defaultMessage: 'No Policies Applied', + })} + + + {intl.formatMessage({ + id: 'Apis.Details.Compliance.PolicyAdherence.empty.helper', + defaultMessage: 'No governance policies have been applied to this API.', + })} + + + ); + + return ( + + ); +} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/RuleViolationSummary.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/RuleViolationSummary.jsx new file mode 100644 index 00000000000..a53e0f2639e --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/RuleViolationSummary.jsx @@ -0,0 +1,469 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Grid, Card, CardContent, Typography, Box, Tabs, Tab, Collapse, IconButton } from '@mui/material'; +import ReportIcon from '@mui/icons-material/Report'; +import WarningIcon from '@mui/icons-material/Warning'; +import InfoIcon from '@mui/icons-material/Info'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import LabelIcon from '@mui/icons-material/Label'; +import RuleIcon from '@mui/icons-material/Rule'; +import ListBase from 'AppComponents/Addons/Addons/ListBase'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import { useIntl } from 'react-intl'; + +// TODO: Improve the component +export default function RuleViolationSummary({ artifactId }) { + const intl = useIntl(); + const [selectedTab, setSelectedTab] = React.useState(0); + const [expandedItems, setExpandedItems] = React.useState([]); + + // TODO: Optimize + simplify + const apiCall = () => { + const restApi = new GovernanceAPI(); + return restApi.getComplianceByAPIId(artifactId) + .then((response) => { + // Get unique ruleset IDs from all policies + const rulesetIds = [...new Set( + response.body.governedPolicies.flatMap(policy => + policy.rulesetValidationResults.map(result => result.id) + ) + )]; + + // Get validation results for each ruleset + return Promise.all( + rulesetIds.map(rulesetId => + restApi.getRulesetValidationResultsByAPIId(artifactId, rulesetId) + .then((result) => result.body) + ) + ).then((rulesets) => { + // Create rulesets array with severities catagorized + const rulesetCategories = rulesets.map(ruleset => ({ + id: ruleset.id, + rulesetName: ruleset.name, + error: ruleset.violatedRules.filter(rule => rule.severity === 'ERROR'), + warn: ruleset.violatedRules.filter(rule => rule.severity === 'WARN'), + info: ruleset.violatedRules.filter(rule => rule.severity === 'INFO'), + passed: ruleset.followedRules + })); + + // Group by severity level + const severityGroups = { + errors: [], + warnings: [], + info: [], + passed: [] + }; + + rulesetCategories.forEach(ruleset => { + if (ruleset.error.length > 0) { + severityGroups.errors.push({ + id: ruleset.id, + rulesetName: ruleset.rulesetName, + // tag: ruleset.tag, + rules: ruleset.error + }); + } + if (ruleset.warn.length > 0) { + severityGroups.warnings.push({ + id: ruleset.id, + rulesetName: ruleset.rulesetName, + // tag: ruleset.tag, + rules: ruleset.warn + }); + } + if (ruleset.info.length > 0) { + severityGroups.info.push({ + id: ruleset.id, + rulesetName: ruleset.rulesetName, + // tag: ruleset.tag, + rules: ruleset.info + }); + } + if (ruleset.passed.length > 0) { + severityGroups.passed.push({ + id: ruleset.id, + rulesetName: ruleset.rulesetName, + // tag: ruleset.tag, + rules: ruleset.passed + }); + } + }); + + return severityGroups; + }); + }) + .catch((error) => { + console.error('Error fetching ruleset adherence data:', error); + return { + errors: [], + warnings: [], + info: [], + passed: [] + }; + }); + }; + + // Remove the mock complianceData and use state instead + const [complianceData, setComplianceData] = React.useState({ + errors: [], + warnings: [], + info: [], + passed: [] + }); + + React.useEffect(() => { + apiCall().then(setComplianceData); + }, [artifactId]); + + const handleTabChange = (e, newValue) => { + setSelectedTab(newValue); + setExpandedItems([]); // Reset expanded items when tab changes + }; + + const handleExpandClick = (id) => { + setExpandedItems(prev => { + const isExpanded = prev.includes(id); + return isExpanded + ? prev.filter(i => i !== id) + : [...prev, id]; + }); + }; + + const getRuleData = (rules) => { + return Promise.resolve( + rules.map(rule => [rule.name, rule.violatedPath, rule.message]) + ); + }; + + // Add new function for passed rules data + const getPassedRuleData = (rules) => { + return Promise.resolve( + rules.map(rule => [rule.name, rule.description]) + ); + }; + + const ruleColumnProps = [ + { + name: 'name', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.column.rule', + defaultMessage: 'Rule', + }), + options: { + customBodyRender: (value) => ( + {value} + ), + }, + }, + { + name: 'violatedPath', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.column.path', + defaultMessage: 'Path', + }), + options: { + customBodyRender: (value) => ( + {value.path} + ), + }, + }, + { + name: 'message', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.column.message', + defaultMessage: 'Message', + }), + options: { + customBodyRender: (value) => ( + {value} + ), + }, + }, + ]; + + // Add new column props for passed rules + const passedRuleColumnProps = [ + { + name: 'name', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.column.rule', + defaultMessage: 'Rule', + }), + options: { + customBodyRender: (value) => ( + {value} + ), + }, + }, + { + name: 'description', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.column.description', + defaultMessage: 'Description', + }), + options: { + customBodyRender: (value) => ( + {value} + ), + }, + }, + ]; + + const renderComplianceCards = (rulesets, isPassed = false) => { + return ( + <> + + {rulesets.map((item) => ( + + + + + + + + {/* {item.provider} / */} + {item.rulesetName} ({item.rules.length}) + + {/* */} + + handleExpandClick(item.id)} + aria-expanded={expandedItems.includes(item.id)} + aria-label='show more' + > + {expandedItems.includes(item.id) ? + : } + + + + + + isPassed ? + getPassedRuleData(item.rules) : getRuleData(item.rules)} + searchProps={false} + addButtonProps={false} + showActionColumn={false} + useContentBase={false} + emptyBoxProps={{ + content: 'There are no rules to display', + }} + options={{ + elevation: 0, + setTableProps: () => ({ + size: 'small', + }), + rowsPerPage: 5, + }} + /> + + + + + ))} + + + ); + }; + + // Add this new function to calculate total rules + const getTotalRuleCount = (rulesets) => { + return rulesets.reduce((sum, ruleset) => sum + ruleset.rules.length, 0); + }; + + const renderEmptyContent = (message) => ( + + + + {message} + + + ); + + const getEmptyMessage = (tabIndex) => { + switch (tabIndex) { + case 0: + return intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.empty.errors', + defaultMessage: 'No Error violations found', + }); + case 1: + return intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.empty.warnings', + defaultMessage: 'No Warning violations found', + }); + case 2: + return intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.empty.info', + defaultMessage: 'No Info violations found', + }); + case 3: + return intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.empty.passed', + defaultMessage: 'No Passed rules found', + }); + default: + return ''; + } + }; + + return ( + <> + { + switch (selectedTab) { + case 0: + return theme.palette.error.main; + case 1: + return theme.palette.warning.main; + case 2: + return theme.palette.info.main; + case 3: + return theme.palette.success.main; + default: + return theme.palette.primary.main; + } + } + } + } + }} + TabIndicatorProps={{ + sx: { + backgroundColor: (theme) => { + switch (selectedTab) { + case 0: + return theme.palette.error.main; + case 1: + return theme.palette.warning.main; + case 2: + return theme.palette.info.main; + case 3: + return theme.palette.success.main; + default: + return theme.palette.primary.main; + } + } + } + }} + > + } + iconPosition='start' + label={intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.tab.errors', + defaultMessage: 'Errors ({count})', + }, { count: getTotalRuleCount(complianceData.errors) })} + /> + } + iconPosition='start' + label={intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.tab.warnings', + defaultMessage: 'Warnings ({count})', + }, { count: getTotalRuleCount(complianceData.warnings) })} + /> + } + iconPosition='start' + label={intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.tab.info', + defaultMessage: 'Info ({count})', + }, { count: getTotalRuleCount(complianceData.info) })} + /> + } + iconPosition='start' + label={intl.formatMessage({ + id: 'Apis.Details.Compliance.RuleViolation.tab.passed', + defaultMessage: 'Passed ({count})', + }, { count: getTotalRuleCount(complianceData.passed) })} + /> + + {selectedTab === 0 && ( + complianceData.errors.length > 0 + ? renderComplianceCards(complianceData.errors) + : renderEmptyContent(getEmptyMessage(0)) + )} + {selectedTab === 1 && ( + complianceData.warnings.length > 0 + ? renderComplianceCards(complianceData.warnings) + : renderEmptyContent(getEmptyMessage(1)) + )} + {selectedTab === 2 && ( + complianceData.info.length > 0 + ? renderComplianceCards(complianceData.info) + : renderEmptyContent(getEmptyMessage(2)) + )} + {selectedTab === 3 && ( + complianceData.passed.length > 0 + ? renderComplianceCards(complianceData.passed, true) + : renderEmptyContent(getEmptyMessage(3)) + )} + + ); +} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/RulesetAdherenceSummaryTable.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/RulesetAdherenceSummaryTable.jsx new file mode 100644 index 00000000000..51fa0936290 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APICompliance/RulesetAdherenceSummaryTable.jsx @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Typography, Chip, Box, Tooltip } from '@mui/material'; +import ListBase from 'AppComponents/Addons/Addons/ListBase'; +import ErrorIcon from '@mui/icons-material/Error'; +import WarningIcon from '@mui/icons-material/Warning'; +import InfoIcon from '@mui/icons-material/Info'; +import GovernanceAPI from 'AppData/GovernanceAPI'; +import { useIntl } from 'react-intl'; +import AssignmentIcon from '@mui/icons-material/Assignment'; +import Utils from 'AppData/Utils'; + +export default function RulesetAdherenceSummaryTable({ artifactId }) { + const intl = useIntl(); + + const apiCall = () => { + const restApi = new GovernanceAPI(); + return restApi.getComplianceByAPIId(artifactId) + .then((response) => { + // Get unique ruleset IDs from all policies + const rulesetIds = [...new Set( + response.body.governedPolicies.flatMap(policy => + policy.rulesetValidationResults.map(result => result.id) + ) + )]; + + // Get validation results for each ruleset + return Promise.all( + rulesetIds.map(rulesetId => + restApi.getRulesetValidationResultsByAPIId(artifactId, rulesetId) + .then((result) => result.body) + ) + ); + }) + .catch((error) => { + console.error('Error fetching ruleset adherence data:', error); + return []; + }); + }; + + const renderComplianceIcons = (violations) => { + const { error, warn, info } = violations; + return ( + + + + + + {error} + + + + + + {warn} + + + + + + {info} + + + + + ); + }; + + const RulesetColumnProps = [ + { + name: 'id', + options: { display: false } + }, + { + name: 'name', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.RulesetAdherence.column.ruleset', + defaultMessage: 'Ruleset', + }), + options: { + width: '40%', + customBodyRender: (value) => ( + {value} + ), + setCellProps: () => ({ + style: { width: '30%' }, + }), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + { + name: 'status', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.RulesetAdherence.column.status', + defaultMessage: 'Status', + }), + options: { + setCellProps: () => ({ + style: { width: '30%' }, + }), + customBodyRender: (value) => ( + + ), + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + }, + }), + }, + }, + { + name: 'violatedRules', + options: { display: false } + }, + { + name: 'followedRules', + options: { display: false } + }, + { + name: 'violationsSummary', + label: intl.formatMessage({ + id: 'Apis.Details.Compliance.RulesetAdherence.column.violations', + defaultMessage: 'Violations', + }), + options: { + customBodyRender: (value, tableMeta) => { + // Count the number of errors, warnings, and info messages in the violations + const violations = tableMeta.rowData[3]; + const counts = violations.reduce((acc, { severity }) => { + acc[severity.toLowerCase()] += 1; + return acc; + }, { error: 0, warn: 0, info: 0 }); + + return renderComplianceIcons({ + error: counts.error, + warn: counts.warn, + info: counts.info + }); + }, + setCellHeaderProps: () => ({ + sx: { + paddingTop: 0, + paddingBottom: 0, + '& .MuiButton-root': { + fontWeight: 'bold', + fontSize: 'small' + }, + } + }), + } + }, + ]; + + const emptyStateContent = ( + + + + {intl.formatMessage({ + id: 'Apis.Details.Compliance.RulesetAdherence.empty.title', + defaultMessage: 'No Rulesets Found', + })} + + + {intl.formatMessage({ + id: 'Apis.Details.Compliance.RulesetAdherence.empty.helper', + defaultMessage: 'No governance rulesets have been applied for this API.', + })} + + + ); + + return ( + + ); +} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Environments/Environments.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Environments/Environments.jsx index 6f0434a87dd..4bc6cef50c0 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Environments/Environments.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Environments/Environments.jsx @@ -70,6 +70,7 @@ import { useRevisionContext } from 'AppComponents/Shared/RevisionContext'; import Utils from 'AppData/Utils'; import { Parser } from '@asyncapi/parser'; import { upperCaseString } from 'AppData/stringFormatter'; +import GovernanceViolations from 'AppComponents/Shared/Governance/GovernanceViolations'; import DisplayDevportal from './DisplayDevportal'; import DeploymentOnbording from './DeploymentOnbording'; import Permission from './Permission'; @@ -203,7 +204,7 @@ const Root = styled('div')(({ theme }) => ({ [`& .${classes.plusIconStyle}`]: { marginTop: 8, marginLeft: 8, - fontSize: 30, + fontSize: 30, }, [`& .${classes.shapeDottedStart1}`]: { @@ -257,7 +258,7 @@ const Root = styled('div')(({ theme }) => ({ color: '#415A85', }, - [`& .${classes.textShape4}`]: { + [`& .${classes.textShape4}`]: { marginTop: 55, }, @@ -492,7 +493,7 @@ export default function Environments() { const isEndpointAvailable = api.endpointConfig !== null; const isDeployButtonDisabled = (((api.type !== 'WEBSUB' && !isEndpointAvailable)) - || api.workflowStatus === 'CREATED'); + || api.workflowStatus === 'CREATED'); const history = useHistory(); const { data: settings, isLoading } = usePublisherSettings(); const { @@ -511,7 +512,7 @@ export default function Environments() { const externalGateways = settings && settings.environment.filter((p) => !p.provider.toLowerCase().includes('wso2')); const internalGatewaysFiltered = settings && settings.environment.filter((p) => p.provider.toLowerCase().includes('wso2')); - const internalGateways = internalGatewaysFiltered && internalGatewaysFiltered.filter((p) => + const internalGateways = internalGatewaysFiltered && internalGatewaysFiltered.filter((p) => p.gatewayType.toLowerCase() === assignGateway.toLowerCase() ); const [selectedVhosts, setVhosts] = useState(null); @@ -542,6 +543,8 @@ export default function Environments() { const [currentLength, setCurrentLength] = useState(0); const [openDeployPopup, setOpenDeployPopup] = useState(history.location.state === 'deploy'); const [externalEnvEndpoints, setExternalEnvEndpoints] = useState(null); + const [isGovernanceViolation, setIsGovernanceViolation] = useState(false); + const [governanceError, setGovernanceError] = useState(''); const allExternalGatewaysMap = []; const allExternalGateways = []; @@ -794,10 +797,26 @@ export default function Environments() { id: 'Apis.Details.Environments.Environments.revision.create.success', defaultMessage: 'Revision Created Successfully', })); + getRevision(); }) .catch((error) => { if (error.response) { - Alert.error(error.response.body.description); + // TODO: Use the error code to identify the errors thrown by governance violation + if (error.response.body.description.toLowerCase().includes('rule')) { + setGovernanceError( + JSON.parse(error.response.body.description) + ); + setIsGovernanceViolation(true); + Alert.error( + intl.formatMessage({ + id: 'Apis.Details.Environments.Environments.revision.create.error.governance', + defaultMessage: 'Revision Creation failed. Governance policy violations found', + }), + ); + return; + } else { + Alert.error(error.response.body.description); + } } else { Alert.error(intl.formatMessage({ id: 'Apis.Details.Environments.Environments.revision.create.error', @@ -805,7 +824,6 @@ export default function Environments() { })); } console.error(error); - }).finally(() => { getRevision(); }); } @@ -977,7 +995,7 @@ export default function Environments() { Alert.error(intl.formatMessage({ id: 'Apis.Details.Environments.Environments.revision.deploy.request.cancel.error', defaultMessage: 'Something went wrong while cancelling the revision' - + ' deployment request', + + ' deployment request', })); } console.error(error); @@ -1084,7 +1102,24 @@ export default function Environments() { }) .catch((error) => { if (error.response) { - Alert.error(error.response.body.description); + // TODO: Use the error code to identify the errors thrown by governance violation + if (error.response.body.description.toLowerCase().includes('rule')) { + setGovernanceError( + JSON.parse(error.response.body.description) + ); + setIsGovernanceViolation(true); + Alert.error( + intl.formatMessage({ + id: 'Apis.Details.Environments.Environments.' + + 'revision.create.error.governance', + defaultMessage: 'Revision Deployment failed.' + + ' Governance policy violations found', + }), + ); + return; + } else { + Alert.error(error.response.body.description); + } } else { Alert.error(intl.formatMessage({ id: 'Apis.Details.Environments.Environments.revision.deploy.error', @@ -1100,7 +1135,22 @@ export default function Environments() { }) .catch((error) => { if (error.response) { - Alert.error(error.response.body.description); + // TODO: Use the error code to identify the errors thrown by governance violation + if (error.response.body.description.toLowerCase().includes('rule')) { + setGovernanceError( + JSON.parse(error.response.body.description) + ); + setIsGovernanceViolation(true); + Alert.error( + intl.formatMessage({ + id: 'Apis.Details.Environments.Environments.revision.create.error.governance', + defaultMessage: 'Revision Creation failed. Governance policy violations found', + }), + ); + return; + } else { + Alert.error(error.response.body.description); + } } else { Alert.error(intl.formatMessage({ id: 'Apis.Details.Environments.Environments.revision.create.error', @@ -1313,7 +1363,7 @@ export default function Environments() { {revName} - + {revDescription} @@ -1331,7 +1381,7 @@ export default function Environments() { - + @@ -1367,7 +1417,7 @@ export default function Environments() { className={clsx(classes.shapeDottedStart, classes.shapeCircle)} style={{ cursor: 'pointer' }} > - + )} @@ -1449,7 +1499,7 @@ export default function Environments() { {revName} - + {revDescription} @@ -1537,7 +1587,7 @@ export default function Environments() { )} - + - + && ( + + + + {api.lifeCycleStatus === 'RETIRED' ? intl.formatMessage({ + id: 'Apis.Details.Environments.Environments.RetiredApi.ToolTip', + defaultMessage: 'Can not deploy new revisions for retired API', + }) : 'Deploy new revision'} + + + )} + placement='bottom' + > + + + - - - )} + + + )} - { allRevisions && allRevisions.length === revisionCount && ( + {allRevisions && allRevisions.length === revisionCount && ( )} - { allRevisions && allRevisions.length === revisionCount && ( + {allRevisions && allRevisions.length === revisionCount && ( ) : ( )} @@ -2166,9 +2216,9 @@ export default function Environments() { variant='outlined' disabled={api.isRevision || (settings && settings.portalConfigurationOnlyModeEnabled) || - allRevisions.filter( - (o1) => o1.deploymentInfo.length === 0, - ).length === 0} + allRevisions.filter( + (o1) => o1.deploymentInfo.length === 0, + ).length === 0} > {allRevisions && allRevisions.length !== 0 && allRevisions.filter( (o1) => o1.deploymentInfo.length === 0, @@ -2235,7 +2285,7 @@ export default function Environments() { }) ? '0.6' : '1' }} className={clsx(SelectedEnvironment - && SelectedEnvironment.includes(row.name) + && SelectedEnvironment.includes(row.name) ? (classes.changeCard) : (classes.noChangeCard), classes.cardHeight)} variant='outlined' @@ -2258,9 +2308,9 @@ export default function Environments() { ( } + icon={} checkedIcon={< - CheckCircleIcon color='primary'/>} + CheckCircleIcon color='primary' />} disabled /> ) : @@ -2272,9 +2322,9 @@ export default function Environments() { SelectedEnvironment.includes(row.name)} onChange={handleChange} color='primary' - icon={} + icon={} checkedIcon={< - CheckCircleIcon color='primary'/>} + CheckCircleIcon color='primary' />} inputProps={{ 'aria-label': 'secondary checkbox' }} @@ -2413,7 +2463,7 @@ export default function Environments() { pending.chip' defaultMessage='Pending' /> -
+
{o3.displayName}
} @@ -2425,7 +2475,7 @@ export default function Environments() { ))} - + @@ -2451,7 +2501,7 @@ export default function Environments() { - { allRevisions && allRevisions.length === revisionCount && ( + {allRevisions && allRevisions.length === revisionCount && ( )} - { allRevisions && allRevisions.length === revisionCount && ( + {allRevisions && allRevisions.length === revisionCount && ( ) : ( )} @@ -2661,9 +2711,9 @@ export default function Environments() { variant='outlined' disabled={api.isRevision || (settings && settings.portalConfigurationOnlyModeEnabled) || - allRevisions.filter( - (o1) => o1.deploymentInfo.length === 0, - ).length === 0} + allRevisions.filter( + (o1) => o1.deploymentInfo.length === 0, + ).length === 0} > {allRevisions && allRevisions.length !== 0 && allRevisions.filter( (o1) => o1.deploymentInfo.length === 0, @@ -2731,8 +2781,11 @@ export default function Environments() { - {api.lifeCycleStatus !== 'RETIRED' - && allRevisions && allRevisions.length !== 0 && api.gatewayVendor === 'wso2' && ( + {isGovernanceViolation && ( + + )} + {api.lifeCycleStatus !== 'RETIRED' + && allRevisions && allRevisions.length !== 0 && api.gatewayVendor === 'wso2' && ( {api.isGraphql() - && ( - <> -
- {getGatewayAccessUrl(allEnvDeployments[row.name] - .vhost, 'WS') - .primary} -
-
- {getGatewayAccessUrl(allEnvDeployments[row.name] - .vhost, 'WS') - .secondary} -
- - - )} + && ( + <> +
+ {getGatewayAccessUrl(allEnvDeployments[row.name] + .vhost, 'WS') + .primary} +
+
+ {getGatewayAccessUrl(allEnvDeployments[row.name] + .vhost, 'WS') + .secondary} +
+ + + )} ) : ( @@ -2901,17 +2954,20 @@ export default function Environments() { variant='outlined' className={classes.vhostSelect} fullWidth - disabled={api.isRevision - || (settings && settings.portalConfigurationOnlyModeEnabled) - || !allRevisions || allRevisions.length === 0} + disabled={ + api.isRevision + || (settings + && settings.portalConfigurationOnlyModeEnabled) + || !allRevisions || allRevisions.length === 0 + } helperText={getVhostHelperText(row.name, selectedVhosts, true, 100)} > {row.vhosts.map( (vhost) => ( - - {api.isWebSocket() + {api.isWebSocket() ? vhost.wsHost : vhost.host} ), @@ -2948,7 +3004,7 @@ export default function Environments() {
)} {allRevisions && allRevisions.length !== 0 && (api.gatewayVendor === 'solace') - && (allExternalGateways.length > 0) && ( + && (allExternalGateways.length > 0) && ( {externalEnvEndpoints[row.name] && - externalEnvEndpoints[row.name].map((e) => { - return ( - - - - - - {e.uri} + externalEnvEndpoints[row.name].map((e) => { + return ( + + + + + + {e.uri} + - - ); - }) + ); + }) } )} @@ -3076,9 +3132,9 @@ export default function Environments() { className={classes.button1} variant='outlined' disabled={api.isRevision || - (settings && - settings.portalConfigurationOnlyModeEnabled - )} + (settings && + settings.portalConfigurationOnlyModeEnabled + )} onClick={() => undeployRevision( allExternalGatewaysMap[row.name] .revision.id, row.name, @@ -3092,7 +3148,7 @@ export default function Environments() { ) : ( -
+
{allRevisions && allRevisions.length !== 0 - && allRevisions.map( - (number) => ( - - {number.displayName} - - ), - )} + && allRevisions.map( + (number) => ( + + {number.displayName} + + ), + )}
@@ -1143,6 +1159,7 @@ Details.subPaths = { TOPICS: '/apis/:api_uuid/topics', ASYNCAPI_DEFINITION: '/apis/:api_uuid/asyncApi-definition', POLICIES: '/apis/:api_uuid/policies', + COMPLIANCE: '/apis/:api_uuid/compliance', }; // To make sure that paths will not change by outsiders, Basically an enum diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Shared/DonutChart.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Shared/DonutChart.jsx new file mode 100644 index 00000000000..808fedf4b49 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Shared/DonutChart.jsx @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { PieChart } from '@mui/x-charts/PieChart'; +import { Box, Typography } from '@mui/material'; + +const DonutChart = ({ + data, height, width, colors, +}) => { + const hasData = data.some((item) => item.value > 0); + + const renderEmptyChart = (message) => ( + + + {message} + + + ); + + if (!hasData) { + return renderEmptyChart('No data available'); + } + + return ( + + + + ); +}; + +DonutChart.propTypes = { + data: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + value: PropTypes.number.isRequired, + label: PropTypes.string.isRequired, + })).isRequired, + height: PropTypes.number, + width: PropTypes.number, + colors: PropTypes.arrayOf(PropTypes.string), +}; + +DonutChart.defaultProps = { + height: 200, + width: 400, + colors: ['#2E96FF', '#FF5252', 'grey'], +}; + +export default DonutChart; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Shared/Governance/GovernanceViolations.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Shared/Governance/GovernanceViolations.jsx new file mode 100644 index 00000000000..f6268cd6b4b --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Shared/Governance/GovernanceViolations.jsx @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Accordion, AccordionDetails, AccordionSummary, Grid, Typography } from '@mui/material'; +import { ExpandMore } from '@mui/icons-material'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TablePagination from '@mui/material/TablePagination'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import GovernanceViolationsSummary, { violationSeverityMap } from './GovernanceViolationsSummary'; + +// Severity priority mapping (higher number = higher priority) +const severityPriority = { + ERROR: 3, + WARN: 2, + INFO: 1, +}; + +function GovernanceViolations({ violations }) { + const [expandViolations, setExpandViolations] = useState(true); + const [selectedSeverity, setSelectedSeverity] = useState(null); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const columns = [ + { id: 'severity', label: 'Severity' }, + { id: 'ruleCode', label: 'Rule' }, + { id: 'violatedPath', label: 'Path' }, + ]; + + const filteredViolations = (selectedSeverity + ? violations.filter(v => v.severity === selectedSeverity) + : violations + ).sort((a, b) => severityPriority[b.severity] - severityPriority[a.severity]); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(+event.target.value); + setPage(0); + }; + + return ( + + { setExpandViolations(!expandViolations) }} + > + } + aria-controls='panel1bh-content' + id='panel1bh-header' + > + + + + + { + event.stopPropagation(); + setSelectedSeverity(value); + setExpandViolations(true); + }} + /> + + + + + + + + + {columns.map((column) => ( + + {column.label} + + ))} + + + + {filteredViolations + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((violation, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {violationSeverityMap[violation.severity]} + {violation.ruleCode} + {violation.violatedPath} + + ))} + +
+
+ +
+
+
+
+ ); +} + +export default GovernanceViolations; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Shared/Governance/GovernanceViolationsSummary.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Shared/Governance/GovernanceViolationsSummary.jsx new file mode 100644 index 00000000000..8e226e10cf3 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Shared/Governance/GovernanceViolationsSummary.jsx @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import Box from '@mui/material/Box'; +import { Grid, Tooltip, Typography } from '@mui/material'; +import { ToggleButton, ToggleButtonGroup } from '@mui/lab'; +import ErrorIcon from '@mui/icons-material/Error'; +import WarningIcon from '@mui/icons-material/Warning'; +import InfoIcon from '@mui/icons-material/Info'; + +export const violationSeverityMap = { + 'ERROR': , + 'WARN': , + 'INFO': , +}; + +const GovernanceViolationsSummary = ({ violations, handleChange }) => { + const [selectedSeverity, setSelectedSeverity] = useState(null); + const severityCounts = {}; + + if (violations) { + violations.forEach(({ severity }) => { + severityCounts[severity] = severityCounts[severity] + 1 || 1; + }); + } + + return ( + + + { + setSelectedSeverity(value); + handleChange(event, value); + }} + > + {Object.entries(violationSeverityMap).map(([severity, component]) => ( + + + {component} + + + +  {severityCounts[severity] || 0} + + + + + + ))} + + + + ); +}; + +export default GovernanceViolationsSummary; diff --git a/portals/publisher/src/main/webapp/source/src/app/data/APIClientFactory.js b/portals/publisher/src/main/webapp/source/src/app/data/APIClientFactory.js index e6ab3626067..ffceed1f46e 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/APIClientFactory.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/APIClientFactory.js @@ -18,6 +18,7 @@ import APIClient from './APIClient'; import ServiceCatalogClient from './ServiceCatalogClient'; +import GovernanceAPIClient from './GovernanceAPIClient'; import Utils from './Utils'; /** @@ -55,6 +56,7 @@ class APIClientFactory { } const apiClientEnvLabel = environment.label + Utils.CONST.API_CLIENT; const catalogClientEnvLabel = environment.label + Utils.CONST.SERVICE_CATALOG_CLIENT; + const governanceClientEnvLabel = environment.label + Utils.CONST.GOVERNANCE_CLIENT; let apiClient; if (clientType === Utils.CONST.API_CLIENT) { apiClient = this._APIClientMap.get(apiClientEnvLabel); @@ -72,6 +74,14 @@ class APIClientFactory { apiClient = new ServiceCatalogClient(environment); this._APIClientMap.set(catalogClientEnvLabel); } + } else if (clientType === Utils.CONST.GOVERNANCE_CLIENT) { + apiClient = this._APIClientMap.get(governanceClientEnvLabel); + if (apiClient) { + return apiClient; + } else { + apiClient = new GovernanceAPIClient(environment); + this._APIClientMap.set(governanceClientEnvLabel); + } } return apiClient; } diff --git a/portals/publisher/src/main/webapp/source/src/app/data/Constants.js b/portals/publisher/src/main/webapp/source/src/app/data/Constants.js index ecea41b61c5..a2f0d5104e5 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/Constants.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/Constants.js @@ -74,6 +74,15 @@ const CONSTS = { }, DEFAULT_SUBSCRIPTIONLESS_PLAN: 'DefaultSubscriptionless', DEFAULT_ASYNC_SUBSCRIPTIONLESS_PLAN: 'AsyncDefaultSubscriptionless', + POLICY_ADHERENCE_STATES: [ + { value: 'FOLLOWED', label: 'Followed' }, + { value: 'VIOLATED', label: 'Violated' }, + { value: 'UNAPPLIED', label: 'Unapplied' }, + ], + RULESET_VALIDATION_STATES: [ + { value: 'PASSED', label: 'Passed' }, + { value: 'FAILED', label: 'Failed' }, + ], }; export default CONSTS; diff --git a/portals/publisher/src/main/webapp/source/src/app/data/GovernanceAPI.js b/portals/publisher/src/main/webapp/source/src/app/data/GovernanceAPI.js new file mode 100644 index 00000000000..7a3b55a5f16 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/data/GovernanceAPI.js @@ -0,0 +1,112 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Utils from './Utils'; +import Resource from './Resource'; +import APIClientFactory from './APIClientFactory'; + +/** + * An abstract representation of GovernanceAPI + */ +class GovernanceAPI extends Resource { + constructor(kwargs) { + super(); + this.client = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), + Utils.CONST.GOVERNANCE_CLIENT).client; + const properties = kwargs; + Utils.deepFreeze(properties); + this._data = properties; + for (const key in properties) { + if (Object.prototype.hasOwnProperty.call(properties, key)) { + this[key] = properties[key]; + } + } + } + + /** + * @param data + * @returns {object} Metadata for API request + * @private + */ + _requestMetaData(data = {}) { + return { + requestContentType: data['Content-Type'] || 'application/json', + signal: data.signal, + }; + } + + /** + * + * Instance method of the API class to provide raw JSON object + * which is API body friendly to use with REST api requests + * Use this method instead of accessing the private _data object for + * converting to a JSON representation of an API object. + * Note: This is deep coping, Use sparingly, Else will have a bad impact on performance + * Basically this is the revers operation in constructor. + * This method simply iterate through all the object properties (excluding the properties in `excludes` list) + * and copy their values to new object. + * So use this method with care!! + * @memberof API + * @param {Array} [userExcludes=[]] List of properties that are need to be excluded from the generated JSON object + * @returns {JSON} JSON representation of the API + */ + toJSON(userExcludes = []) { + var copy = {}, + excludes = ['_data', 'client', 'apiType', ...userExcludes]; + for (var prop in this) { + if (!excludes.includes(prop)) { + copy[prop] = cloneDeep(this[prop]); + } + } + return copy; + } + + /** + * Get artifact compliance by id + * @param {string} artifactId Artifact id + * @param {Object} options Optional parameters including signal for AbortController + * @returns {Promise} Promised artifact compliance response + */ + getComplianceByAPIId(artifactId, options = {}) { + return this.client.then((client) => { + return client.apis['Artifact Compliance'].getComplianceByAPIId( + { apiId: artifactId }, + { ...this._requestMetaData(), signal: options.signal } + ); + }); + } + + /** + * Get ruleset validation results by artifact id + * @param {string} artifactId Artifact id + * @param {string} rulesetId Ruleset id + * @param {Object} options Optional parameters including signal for AbortController + * @returns {Promise} Promised validation results response + */ + getRulesetValidationResultsByAPIId(artifactId, rulesetId) { + return this.client.then((client) => { + return client.apis['Artifact Compliance'].getRulesetValidationResultsByAPIId( + { apiId: artifactId, rulesetId: rulesetId }, + this._requestMetaData(), + ); + }); + } +} + +export default GovernanceAPI; diff --git a/portals/publisher/src/main/webapp/source/src/app/data/GovernanceAPIClient.js b/portals/publisher/src/main/webapp/source/src/app/data/GovernanceAPIClient.js new file mode 100644 index 00000000000..566edd2675e --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/data/GovernanceAPIClient.js @@ -0,0 +1,213 @@ +/* eslint-disable */ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import SwaggerClient from 'swagger-client'; +import { Mutex } from 'async-mutex'; +import Configurations from 'Config'; +import AuthManager from 'AppData/AuthManager'; +import Utils from './Utils'; + +/** + * This class expose single swaggerClient instance created using the given swagger URL (Publisher, Store, ect ..) + * it's highly unlikely to change the REST API Swagger definition (swagger.json) file on the fly, + * Hence this singleton class help to preserve consecutive swagger client object creations saving redundant IO + * operations. + */ +class GovernanceAPIClient { + /** + * @param {Object} environment - Environment to get host for the swagger-client's spec property. + * @param {{}} args - Accept as an optional argument for GovernanceAPIClient constructor.Merge the given args with + * default args. + * @returns {GovernanceAPIClient} + */ + constructor(environment, args = {}) { + this.environment = environment || Utils.getCurrentEnvironment(); + SwaggerClient.http.withCredentials = true; + const promisedResolve = SwaggerClient.resolve({ + url: Utils.getGovernanceSwaggerURL(), + requestInterceptor: (request) => { + request.headers.Accept = 'text/yaml'; + }, + }); + GovernanceAPIClient.spec = promisedResolve; + this._client = promisedResolve.then((resolved) => { + const argsv = Object.assign(args, { + spec: this._fixSpec(resolved.spec), + requestInterceptor: this._getRequestInterceptor(), + responseInterceptor: this._getResponseInterceptor(), + }); + SwaggerClient.http.withCredentials = true; + return new SwaggerClient(argsv); + }); + this._client.catch(AuthManager.unauthorizedErrorHandler); + this.mutex = new Mutex(); + } + + /** + * Expose the private _client property to public + * @returns {GovernanceAPIClient} an instance of GovernanceAPIClient class + */ + get client() { + return this._client; + } + + /** + * Get the ETag of a given resource key from the session storage + * @param {String} key - key of resource. + * @returns {String} ETag value for the given key + */ + static getETag(key) { + return sessionStorage.getItem('etag_' + key); + } + + /** + * Add an ETag to a given resource key into the session storage + * @param key {string} key of resource. + * @param etag {string} etag value to be stored against the key + */ + static addETag(key, etag) { + sessionStorage.setItem('etag_' + key, etag); + } + + /** + * Get Scope for a particular resource path + * + * @param resourcePath resource path of the action + * @param resourceMethod resource method of the action + */ + static getScopeForResource(resourcePath, resourceMethod) { + if (!GovernanceAPIClient.spec) { + SwaggerClient.http.withCredentials = true; + GovernanceAPIClient.spec = SwaggerClient.resolve({ url: Utils.getSwaggerURL() }); + } + return GovernanceAPIClient.spec.then((resolved) => { + return ( + resolved.spec.paths[resourcePath] + && resolved.spec.paths[resourcePath][resourceMethod] + && resolved.spec.paths[resourcePath][resourceMethod].security[0].OAuth2Security[0] + ); + }); + } + + /** + * Temporary method to fix the hostname attribute Till following issues get fixed ~tmkb + * https://github.com/swagger-api/swagger-js/issues/1081 + * https://github.com/swagger-api/swagger-js/issues/1045 + * @param spec {JSON} : Json object of the specification + * @returns {JSON} : Fixed specification + * @private + */ + _fixSpec(spec) { + const updatedSpec = spec; + const url = new URL(spec.servers[0].url); + if (this.environment.host !== url.host) { + url.host = this.environment.host; + if (Configurations.app.proxy_context_path && Configurations.app.proxy_context_path !== '') { + url.pathname = Configurations.app.proxy_context_path + url.pathname; + } + updatedSpec.servers[0].url = String(url); + } + return updatedSpec; + } + + _getResponseInterceptor() { + return (data) => { + if (data.headers.etag) { + GovernanceAPIClient.addETag(data.url, data.headers.etag); + } + + // If an unauthenticated response is received, we check whether the token is valid by introspecting it. + // If it is not valid, we need to clear the stored tokens (in cookies etc) in the browser by redirecting the + // user to logout. + if (data.status === 401 && data.body != null && data.body.description === 'Unauthenticated request') { + const userData = AuthManager.getUserFromToken(); + userData.catch((error) => { + console.error('Error occurred while checking token status. Hence redirecting to login', error); + window.location = Configurations.app.context + Utils.CONST.LOGOUT_CALLBACK; + }); + } + return data; + }; + } + + /** + * + * + * @returns + * @memberof GovernanceAPIClient + */ + _getRequestInterceptor() { + return (request) => { + const existingUser = AuthManager.getUser(this.environment.label); + if (!existingUser) { + console.log('User not found. Token refreshing failed.'); + return request; + } + let existingToken = AuthManager.getUser(this.environment.label).getPartialToken(); + const refToken = AuthManager.getUser(this.environment.label).getRefreshPartialToken(); + if (existingToken) { + request.headers.authorization = 'Bearer ' + existingToken; + return request; + } else { + console.log('Access token is expired. Trying to refresh.'); + if (!refToken) { + console.log('Refresh token not found. Token refreshing failed.'); + return request; + } + } + + const env = this.environment; + const promise = new Promise((resolve, reject) => { + this.mutex.acquire().then((release) => { + existingToken = AuthManager.getUser(env.label).getPartialToken(); + if (existingToken) { + request.headers.authorization = 'Bearer ' + existingToken; + release(); + resolve(request); + } else { + AuthManager.refresh(env).then((res) => res.json()) + .then(() => { + request.headers.authorization = 'Bearer ' + + AuthManager.getUser(env.label).getPartialToken(); + release(); + resolve(request); + }).catch((error) => { + console.error('Error:', error); + release(); + reject(); + }) + .finally(() => { + release(); + }); + } + }); + }); + + if (GovernanceAPIClient.getETag(request.url) + && (request.method === 'PUT' || request.method === 'DELETE' || request.method === 'POST')) { + request.headers['If-Match'] = GovernanceAPIClient.getETag(request.url); + } + return promise; + }; + } +} + +GovernanceAPIClient.spec = null; + +export default GovernanceAPIClient; diff --git a/portals/publisher/src/main/webapp/source/src/app/data/Utils.js b/portals/publisher/src/main/webapp/source/src/app/data/Utils.js index f4ac2eb56be..cc37dfa0d21 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/Utils.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/Utils.js @@ -216,6 +216,25 @@ class Utils { } } + /** + * Get governance swagger definition URL + * @static + * @returns + * @memberof Utils + */ + static getGovernanceSwaggerURL() { + if (Configurations.app.proxy_context_path) { + return 'https://' + + Utils.getCurrentEnvironment().host + + Configurations.app.proxy_context_path + + Utils.CONST.GOVERNANCE_SWAGGER_JSON; + } else { + return 'https://' + + Utils.getCurrentEnvironment().host + + Utils.CONST.GOVERNANCE_SWAGGER_JSON; + } + } + /** * Generate UUID V4 Source https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid */ @@ -640,6 +659,28 @@ class Utils { ) })); } + + /** + * Maps a policy adherence state value to its label + * @param {string} state The value of the policy adherence state + * @returns {string} The label of the policy adherence state + * @memberof Utils + */ + static mapPolicyAdherenceStateToLabel(state) { + const policyState = CONSTS.POLICY_ADHERENCE_STATES.find((t) => t.value === state); + return policyState?.label || state; + } + + /** + * Maps a ruleset validation state value to its label + * @param {string} state The value of the ruleset validation state + * @returns {string} The label of the ruleset validation state + * @memberof Utils + */ + static mapRulesetValidationStateToLabel(state) { + const validationState = CONSTS.RULESET_VALIDATION_STATES.find((t) => t.value === state); + return validationState?.label || state; + } } Utils.CONST = { @@ -651,6 +692,7 @@ Utils.CONST = { INTROSPECT: '/services/auth/introspect', SERVICE_CATALOG_SWAGGER_YAML: '/api/am/service-catalog/v1/oas.yaml', SWAGGER_YAML: '/api/am/publisher/v4/swagger.yaml', + GOVERNANCE_SWAGGER_JSON: '/api/am/governance/v1/swagger.yaml', PROTOCOL: 'https://', API_CLIENT: 'apiClient', SERVICE_CATALOG_CLIENT: 'serviceCatalogClient',