Compare commits
13 Commits
commit-12b
...
commit-09a
Author | SHA1 | Date | |
---|---|---|---|
09ab4474e9 | |||
177406df41 | |||
e2367c2a33 | |||
33524ce3cc | |||
e037c6c577 | |||
c268991495 | |||
0edfe0f73a | |||
f4a6030d93 | |||
9358539e0c | |||
5bf7d5652c | |||
12f9229320 | |||
df19fa811c | |||
6cda89c710 |
2
.github/workflows/end-to-end-test.yml
vendored
2
.github/workflows/end-to-end-test.yml
vendored
@ -81,7 +81,7 @@ jobs:
|
|||||||
- name: Install go
|
- name: Install go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: 1.20.x
|
go-version: 1.21.x
|
||||||
|
|
||||||
- name: Checkout zot repo
|
- name: Checkout zot repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
210
package-lock.json
generated
210
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"@mui/lab": "^5.0.0-alpha.89",
|
"@mui/lab": "^5.0.0-alpha.89",
|
||||||
"@mui/material": "^5.8.6",
|
"@mui/material": "^5.8.6",
|
||||||
"@mui/styles": "^5.8.6",
|
"@mui/styles": "^5.8.6",
|
||||||
|
"@mui/x-date-pickers": "^6.18.4",
|
||||||
"@testing-library/jest-dom": "^5.16.1",
|
"@testing-library/jest-dom": "^5.16.1",
|
||||||
"@testing-library/react": "^12.1.2",
|
"@testing-library/react": "^12.1.2",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
@ -21,7 +22,7 @@
|
|||||||
"downshift": "^6.1.12",
|
"downshift": "^6.1.12",
|
||||||
"export-from-json": "^1.7.3",
|
"export-from-json": "^1.7.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^2.5.2",
|
"luxon": "^3.4.4",
|
||||||
"markdown-to-jsx": "^7.1.7",
|
"markdown-to-jsx": "^7.1.7",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
@ -2130,16 +2131,21 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.22.5",
|
"version": "7.23.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz",
|
||||||
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
|
"integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.13.11"
|
"regenerator-runtime": "^0.14.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||||
|
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.22.5",
|
"version": "7.22.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
|
||||||
@ -2678,6 +2684,40 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.4.2",
|
||||||
|
"@floating-ui/utils": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.5.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
|
||||||
|
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.10",
|
"version": "0.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
||||||
@ -3935,11 +3975,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/types": {
|
"node_modules/@mui/types": {
|
||||||
"version": "7.2.4",
|
"version": "7.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz",
|
||||||
"integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
|
"integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
@ -3948,25 +3988,134 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/utils": {
|
"node_modules/@mui/utils": {
|
||||||
"version": "5.13.6",
|
"version": "5.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.6.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.0.tgz",
|
||||||
"integrity": "sha512-ggNlxl5NPSbp+kNcQLmSig6WVB0Id+4gOxhx644987v4fsji+CSXc+MFYLocFB/x4oHtzCUlSzbVHlJfP/fXoQ==",
|
"integrity": "sha512-XSmTKStpKYamewxyJ256+srwEnsT3/6eNo6G7+WC1tj2Iq9GfUJ/6yUoB7YXjOD2jTZ3XobToZm4pVz1LBt6GA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.5",
|
"@babel/runtime": "^7.23.5",
|
||||||
"@types/prop-types": "^15.7.5",
|
"@types/prop-types": "^15.7.11",
|
||||||
"@types/react-is": "^18.2.0",
|
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-is": "^18.2.0"
|
"react-is": "^18.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
},
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mui/x-date-pickers": {
|
||||||
|
"version": "6.18.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.4.tgz",
|
||||||
|
"integrity": "sha512-YqJ6lxZHBIt344B3bvRAVbdYSQz4dcmJQXGcfvJTn26VdKjpgzjAqwhlbQhbAt55audJOWzGB99ImuQuljDROA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.23.2",
|
||||||
|
"@mui/base": "^5.0.0-beta.22",
|
||||||
|
"@mui/utils": "^5.14.16",
|
||||||
|
"@types/react-transition-group": "^4.4.8",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-transition-group": "^4.4.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/mui"
|
"url": "https://opencollective.com/mui"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^17.0.0 || ^18.0.0"
|
"@emotion/react": "^11.9.0",
|
||||||
|
"@emotion/styled": "^11.8.1",
|
||||||
|
"@mui/material": "^5.8.6",
|
||||||
|
"@mui/system": "^5.8.0",
|
||||||
|
"date-fns": "^2.25.0",
|
||||||
|
"date-fns-jalali": "^2.13.0-0",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
|
"luxon": "^3.0.2",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"moment-hijri": "^2.1.2",
|
||||||
|
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@emotion/styled": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"date-fns-jalali": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"dayjs": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"luxon": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"moment": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"moment-hijri": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"moment-jalaali": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mui/x-date-pickers/node_modules/@mui/base": {
|
||||||
|
"version": "5.0.0-beta.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.27.tgz",
|
||||||
|
"integrity": "sha512-duL37qxihT1N0pW/gyXVezP7SttLkF+cLAs/y6g6ubEFmVadjbnZ45SeF12/vAiKzqwf5M0uFH1cczIPXFZygA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.23.5",
|
||||||
|
"@floating-ui/react-dom": "^2.0.4",
|
||||||
|
"@mui/types": "^7.2.11",
|
||||||
|
"@mui/utils": "^5.15.0",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mui/x-date-pickers/node_modules/clsx": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||||
@ -4844,9 +4993,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||||
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
|
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/q": {
|
"node_modules/@types/q": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
@ -4884,18 +5033,10 @@
|
|||||||
"@types/react": "^17"
|
"@types/react": "^17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-is": {
|
|
||||||
"version": "18.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz",
|
|
||||||
"integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/react": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/react-transition-group": {
|
"node_modules/@types/react-transition-group": {
|
||||||
"version": "4.4.6",
|
"version": "4.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
|
||||||
"integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
|
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
@ -13250,9 +13391,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/luxon": {
|
"node_modules/luxon": {
|
||||||
"version": "2.5.2",
|
"version": "3.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||||
"integrity": "sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA==",
|
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@ -16202,7 +16343,8 @@
|
|||||||
"node_modules/regenerator-runtime": {
|
"node_modules/regenerator-runtime": {
|
||||||
"version": "0.13.11",
|
"version": "0.13.11",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/regenerator-transform": {
|
"node_modules/regenerator-transform": {
|
||||||
"version": "0.15.1",
|
"version": "0.15.1",
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"@mui/lab": "^5.0.0-alpha.89",
|
"@mui/lab": "^5.0.0-alpha.89",
|
||||||
"@mui/material": "^5.8.6",
|
"@mui/material": "^5.8.6",
|
||||||
"@mui/styles": "^5.8.6",
|
"@mui/styles": "^5.8.6",
|
||||||
|
"@mui/x-date-pickers": "^6.18.4",
|
||||||
"@testing-library/jest-dom": "^5.16.1",
|
"@testing-library/jest-dom": "^5.16.1",
|
||||||
"@testing-library/react": "^12.1.2",
|
"@testing-library/react": "^12.1.2",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
@ -16,7 +17,7 @@
|
|||||||
"downshift": "^6.1.12",
|
"downshift": "^6.1.12",
|
||||||
"export-from-json": "^1.7.3",
|
"export-from-json": "^1.7.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^2.5.2",
|
"luxon": "^3.4.4",
|
||||||
"markdown-to-jsx": "^7.1.7",
|
"markdown-to-jsx": "^7.1.7",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
@ -42,7 +42,8 @@ const config = {
|
|||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
ignoreHTTPSErrors: true
|
ignoreHTTPSErrors: true,
|
||||||
|
screenshot: 'only-on-failure'
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
@ -101,7 +102,7 @@ const config = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||||
// outputDir: 'test-results/',
|
outputDir: 'test-results/',
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
// webServer: {
|
// webServer: {
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { isAuthenticated } from 'utilities/authUtilities';
|
import { isAuthenticated, isApiKeyEnabled } from 'utilities/authUtilities';
|
||||||
|
import { AuthWrapper } from 'utilities/AuthWrapper';
|
||||||
|
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
import LoginPage from './pages/LoginPage';
|
import LoginPage from './pages/LoginPage';
|
||||||
import { AuthWrapper } from 'utilities/AuthWrapper';
|
|
||||||
import RepoPage from 'pages/RepoPage';
|
import RepoPage from 'pages/RepoPage';
|
||||||
import TagPage from 'pages/TagPage';
|
import TagPage from 'pages/TagPage';
|
||||||
import ExplorePage from 'pages/ExplorePage';
|
import ExplorePage from 'pages/ExplorePage';
|
||||||
|
import UserManagementPage from 'pages/UserManagementPage';
|
||||||
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ function App() {
|
|||||||
<Route path="/explore" element={<ExplorePage />} />
|
<Route path="/explore" element={<ExplorePage />} />
|
||||||
<Route path="/image/:name" element={<RepoPage />} />
|
<Route path="/image/:name" element={<RepoPage />} />
|
||||||
<Route path="/image/:reponame/tag/:tag" element={<TagPage />} />
|
<Route path="/image/:reponame/tag/:tag" element={<TagPage />} />
|
||||||
|
{isApiKeyEnabled() && <Route path="/user/apikey" element={<UserManagementPage />} />}
|
||||||
<Route path="*" element={<Navigate to="/home" />} />
|
<Route path="*" element={<Navigate to="/home" />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<AuthWrapper isLoggedIn={!isLoggedIn} redirect="/" />}>
|
<Route element={<AuthWrapper isLoggedIn={!isLoggedIn} redirect="/" />}>
|
||||||
|
@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event';
|
|||||||
const mockMgmtResponse = {
|
const mockMgmtResponse = {
|
||||||
distSpecVersion: '1.1.0-dev',
|
distSpecVersion: '1.1.0-dev',
|
||||||
binaryType: '-apikey-lint-metrics-mgmt-scrub-search-sync-ui-userprefs',
|
binaryType: '-apikey-lint-metrics-mgmt-scrub-search-sync-ui-userprefs',
|
||||||
http: { auth: { htpasswd: {} } }
|
http: { auth: { htpasswd: {}, openid: { providers: { github: {} } } } }
|
||||||
};
|
};
|
||||||
|
|
||||||
// useNavigate mock
|
// useNavigate mock
|
||||||
@ -55,6 +55,7 @@ describe('Sign in form', () => {
|
|||||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||||
expect(usernameInput).toHaveValue('test');
|
expect(usernameInput).toHaveValue('test');
|
||||||
expect(passwordInput).toHaveValue('test');
|
expect(passwordInput).toHaveValue('test');
|
||||||
|
expect(screen.getByTestId('openid-divider')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display error if username and password values are empty after change', async () => {
|
it('should display error if username and password values are empty after change', async () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
import { render, screen, waitFor, within, fireEvent } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||||
import { api } from 'api';
|
import { api } from 'api';
|
||||||
@ -18,10 +18,18 @@ const StateVulnerabilitiesWrapper = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCVEList = {
|
const simpleMockCVEList = {
|
||||||
CVEListForImage: {
|
CVEListForImage: {
|
||||||
Tag: '',
|
Tag: '',
|
||||||
Page: { ItemCount: 20, TotalCount: 20 },
|
Page: { ItemCount: 2, TotalCount: 2 },
|
||||||
|
Summary: {
|
||||||
|
Count: 2,
|
||||||
|
UnknownCount: 0,
|
||||||
|
LowCount: 0,
|
||||||
|
MediumCount: 1,
|
||||||
|
HighCount: 0,
|
||||||
|
CriticalCount: 1,
|
||||||
|
},
|
||||||
CVEList: [
|
CVEList: [
|
||||||
{
|
{
|
||||||
Id: 'CVE-2020-16156',
|
Id: 'CVE-2020-16156',
|
||||||
@ -31,6 +39,53 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'perl-base',
|
Name: 'perl-base',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
|
InstalledVersion: '5.30.0-9ubuntu0.2',
|
||||||
|
FixedVersion: 'Not Specified'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: 'CVE-2016-1000027',
|
||||||
|
Title: 'spring: HttpInvokerServiceExporter readRemoteInvocation method untrusted java deserialization',
|
||||||
|
Description: "Pivotal Spring Framework through 5.3.16 suffers from a potential remote code execution (RCE) issue if used for Java deserialization of untrusted data. Depending on how the library is implemented within a product, this issue may or not occur, and authentication may be required. NOTE: the vendor's position is that untrusted data is not an intended use case. The product's behavior will not be changed because some users rely on deserialization of trusted data.",
|
||||||
|
Severity: 'CRITICAL',
|
||||||
|
Reference: 'https://avd.aquasec.com/nvd/cve-2016-1000027',
|
||||||
|
PackageList: [
|
||||||
|
{
|
||||||
|
Name: 'org.springframework:spring-web',
|
||||||
|
PackagePath: 'usr/local/tomcat/webapps/spring4shell.war/WEB-INF/lib/spring-web-5.3.15.jar',
|
||||||
|
InstalledVersion: '5.3.15',
|
||||||
|
FixedVersion: '6.0.0'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockCVEList = {
|
||||||
|
CVEListForImage: {
|
||||||
|
Tag: '',
|
||||||
|
Page: { ItemCount: 20, TotalCount: 20 },
|
||||||
|
Summary: {
|
||||||
|
Count: 5,
|
||||||
|
UnknownCount: 1,
|
||||||
|
LowCount: 1,
|
||||||
|
MediumCount: 1,
|
||||||
|
HighCount: 1,
|
||||||
|
CriticalCount: 1,
|
||||||
|
},
|
||||||
|
CVEList: [
|
||||||
|
{
|
||||||
|
Id: 'CVE-2020-16156',
|
||||||
|
Title: 'perl-CPAN: Bypass of verification of signatures in CHECKSUMS files',
|
||||||
|
Description: 'CPAN 2.28 allows Signature Verification Bypass.',
|
||||||
|
Severity: 'MEDIUM',
|
||||||
|
PackageList: [
|
||||||
|
{
|
||||||
|
Name: 'perl-base',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '5.30.0-9ubuntu0.2',
|
InstalledVersion: '5.30.0-9ubuntu0.2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -46,26 +101,31 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'krb5-locales',
|
Name: 'krb5-locales',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libgssapi-krb5-2',
|
Name: 'libgssapi-krb5-2',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libk5crypto3',
|
Name: 'libk5crypto3',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libkrb5-3',
|
Name: 'libkrb5-3',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libkrb5support0',
|
Name: 'libkrb5support0',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -80,6 +140,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libgnutls30',
|
Name: 'libgnutls30',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '3.6.13-2ubuntu1.6',
|
InstalledVersion: '3.6.13-2ubuntu1.6',
|
||||||
FixedVersion: '3.6.13-2ubuntu1.7'
|
FixedVersion: '3.6.13-2ubuntu1.7'
|
||||||
}
|
}
|
||||||
@ -94,6 +155,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libpcre2-8-0',
|
Name: 'libpcre2-8-0',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '10.34-7',
|
InstalledVersion: '10.34-7',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -108,6 +170,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libsqlite3-0',
|
Name: 'libsqlite3-0',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||||
}
|
}
|
||||||
@ -122,6 +185,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libpcre3',
|
Name: 'libpcre3',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '2:8.39-12ubuntu0.1',
|
InstalledVersion: '2:8.39-12ubuntu0.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -136,6 +200,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libsqlite3-0',
|
Name: 'libsqlite3-0',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||||
}
|
}
|
||||||
@ -150,11 +215,13 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'login',
|
Name: 'login',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1:4.8.1-1ubuntu5.20.04.2',
|
InstalledVersion: '1:4.8.1-1ubuntu5.20.04.2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'passwd',
|
Name: 'passwd',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1:4.8.1-1ubuntu5.20.04.2',
|
InstalledVersion: '1:4.8.1-1ubuntu5.20.04.2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -169,6 +236,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libgmp10',
|
Name: 'libgmp10',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '2:6.2.0+dfsg-4',
|
InstalledVersion: '2:6.2.0+dfsg-4',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -183,6 +251,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libgnutls30',
|
Name: 'libgnutls30',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '3.6.13-2ubuntu1.6',
|
InstalledVersion: '3.6.13-2ubuntu1.6',
|
||||||
FixedVersion: '3.6.13-2ubuntu1.7'
|
FixedVersion: '3.6.13-2ubuntu1.7'
|
||||||
}
|
}
|
||||||
@ -197,26 +266,31 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libncurses6',
|
Name: 'libncurses6',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libncursesw6',
|
Name: 'libncursesw6',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libtinfo6',
|
Name: 'libtinfo6',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'ncurses-base',
|
Name: 'ncurses-base',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'ncurses-bin',
|
Name: 'ncurses-bin',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -231,6 +305,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libpcre2-8-0',
|
Name: 'libpcre2-8-0',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '10.34-7',
|
InstalledVersion: '10.34-7',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -245,26 +320,31 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libncurses6',
|
Name: 'libncurses6',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libncursesw6',
|
Name: 'libncursesw6',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libtinfo6',
|
Name: 'libtinfo6',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'ncurses-base',
|
Name: 'ncurses-base',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'ncurses-bin',
|
Name: 'ncurses-bin',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '6.2-0ubuntu2',
|
InstalledVersion: '6.2-0ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -279,6 +359,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'coreutils',
|
Name: 'coreutils',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '8.30-3ubuntu2',
|
InstalledVersion: '8.30-3ubuntu2',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -293,46 +374,55 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libasn1-8-heimdal',
|
Name: 'libasn1-8-heimdal',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libgssapi3-heimdal',
|
Name: 'libgssapi3-heimdal',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libhcrypto4-heimdal',
|
Name: 'libhcrypto4-heimdal',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libheimbase1-heimdal',
|
Name: 'libheimbase1-heimdal',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libheimntlm0-heimdal',
|
Name: 'libheimntlm0-heimdal',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libhx509-5-heimdal',
|
Name: 'libhx509-5-heimdal',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libkrb5-26-heimdal',
|
Name: 'libkrb5-26-heimdal',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libroken18-heimdal',
|
Name: 'libroken18-heimdal',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libwind0-heimdal',
|
Name: 'libwind0-heimdal',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -347,11 +437,13 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libc-bin',
|
Name: 'libc-bin',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '2.31-0ubuntu9.9',
|
InstalledVersion: '2.31-0ubuntu9.9',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libc6',
|
Name: 'libc6',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '2.31-0ubuntu9.9',
|
InstalledVersion: '2.31-0ubuntu9.9',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -365,6 +457,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libcurl4',
|
Name: 'libcurl4',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '7.68.0-1ubuntu2.12',
|
InstalledVersion: '7.68.0-1ubuntu2.12',
|
||||||
FixedVersion: '7.68.0-1ubuntu2.13'
|
FixedVersion: '7.68.0-1ubuntu2.13'
|
||||||
}
|
}
|
||||||
@ -380,26 +473,31 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'krb5-locales',
|
Name: 'krb5-locales',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libgssapi-krb5-2',
|
Name: 'libgssapi-krb5-2',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libk5crypto3',
|
Name: 'libk5crypto3',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libkrb5-3',
|
Name: 'libkrb5-3',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: 'libkrb5support0',
|
Name: 'libkrb5support0',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1.17-6ubuntu4.1',
|
InstalledVersion: '1.17-6ubuntu4.1',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -414,6 +512,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'libsqlite3-0',
|
Name: 'libsqlite3-0',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||||
}
|
}
|
||||||
@ -429,6 +528,7 @@ const mockCVEList = {
|
|||||||
PackageList: [
|
PackageList: [
|
||||||
{
|
{
|
||||||
Name: 'zlib1g',
|
Name: 'zlib1g',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
InstalledVersion: '1:1.2.11.dfsg-2ubuntu1.3',
|
InstalledVersion: '1:1.2.11.dfsg-2ubuntu1.3',
|
||||||
FixedVersion: 'Not Specified'
|
FixedVersion: 'Not Specified'
|
||||||
}
|
}
|
||||||
@ -442,10 +542,52 @@ const mockCVEListFiltered = {
|
|||||||
CVEListForImage: {
|
CVEListForImage: {
|
||||||
Tag: '',
|
Tag: '',
|
||||||
Page: { ItemCount: 20, TotalCount: 20 },
|
Page: { ItemCount: 20, TotalCount: 20 },
|
||||||
|
Summary: {
|
||||||
|
Count: 5,
|
||||||
|
UnknownCount: 1,
|
||||||
|
LowCount: 1,
|
||||||
|
MediumCount: 1,
|
||||||
|
HighCount: 1,
|
||||||
|
CriticalCount: 1,
|
||||||
|
},
|
||||||
CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Id.includes('2022'))
|
CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Id.includes('2022'))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockCVEListFilteredBySeverity = (severity) => {
|
||||||
|
return {
|
||||||
|
CVEListForImage: {
|
||||||
|
Tag: '',
|
||||||
|
Page: { ItemCount: 20, TotalCount: 20 },
|
||||||
|
Summary: {
|
||||||
|
Count: 5,
|
||||||
|
UnknownCount: 1,
|
||||||
|
LowCount: 1,
|
||||||
|
MediumCount: 1,
|
||||||
|
HighCount: 1,
|
||||||
|
CriticalCount: 1,
|
||||||
|
},
|
||||||
|
CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Severity.includes(severity))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCVEListFilteredExclude = {
|
||||||
|
CVEListForImage: {
|
||||||
|
Tag: '',
|
||||||
|
Page: { ItemCount: 20, TotalCount: 20 },
|
||||||
|
Summary: {
|
||||||
|
Count: 5,
|
||||||
|
UnknownCount: 1,
|
||||||
|
LowCount: 1,
|
||||||
|
MediumCount: 1,
|
||||||
|
HighCount: 1,
|
||||||
|
CriticalCount: 1,
|
||||||
|
},
|
||||||
|
CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => !e.Id.includes('2022'))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const mockCVEFixed = {
|
const mockCVEFixed = {
|
||||||
pageOne: {
|
pageOne: {
|
||||||
ImageListWithCVEFixed: {
|
ImageListWithCVEFixed: {
|
||||||
@ -499,41 +641,97 @@ describe('Vulnerabilties page', () => {
|
|||||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
||||||
render(<StateVulnerabilitiesWrapper />);
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||||
await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20));
|
await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1));
|
||||||
|
await waitFor(() => expect(screen.getAllByText(/CVE/)).toHaveLength(20));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the vulnerabilities by severity', async () => {
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
|
||||||
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1));
|
||||||
|
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(20));
|
||||||
|
expect(screen.getByLabelText('Medium')).toBeInTheDocument();
|
||||||
|
const mediumSeverity = await screen.getByLabelText('Medium');
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('MEDIUM') } });
|
||||||
|
fireEvent.click(mediumSeverity);
|
||||||
|
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(6));
|
||||||
|
expect(screen.getByLabelText('High')).toBeInTheDocument();
|
||||||
|
const highSeverity = await screen.getByLabelText('High');
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('HIGH') } });
|
||||||
|
fireEvent.click(highSeverity);
|
||||||
|
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1));
|
||||||
|
expect(screen.getByLabelText('Critical')).toBeInTheDocument();
|
||||||
|
const criticalSeverity = await screen.getByLabelText('Critical');
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('CRITICAL') } });
|
||||||
|
fireEvent.click(criticalSeverity);
|
||||||
|
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1));
|
||||||
|
expect(screen.getByLabelText('Low')).toBeInTheDocument();
|
||||||
|
const lowSeverity = await screen.getByLabelText('Low');
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('LOW') } });
|
||||||
|
fireEvent.click(lowSeverity);
|
||||||
|
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(10));
|
||||||
|
expect(screen.getByLabelText('Unknown')).toBeInTheDocument();
|
||||||
|
const unknownSeverity = await screen.getByLabelText('Unknown');
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('UNKNOWN') } });
|
||||||
|
fireEvent.click(unknownSeverity);
|
||||||
|
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1));
|
||||||
|
expect(screen.getByText('Total 5')).toBeInTheDocument();
|
||||||
|
const totalSeverity = await screen.getByText('Total 5');
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('') } });
|
||||||
|
fireEvent.click(totalSeverity);
|
||||||
|
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(20));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends filtered query if user types in the search bar', async () => {
|
it('sends filtered query if user types in the search bar', async () => {
|
||||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
|
||||||
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
|
await waitFor(() => expect(screen.getAllByText(/CVE/)).toHaveLength(20));
|
||||||
|
const cveSearchInput = screen.getByPlaceholderText(/search/i);
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFiltered } });
|
||||||
|
await userEvent.type(cveSearchInput, '2022');
|
||||||
|
expect(cveSearchInput).toHaveValue('2022')
|
||||||
|
await waitFor(() => expect(screen.queryAllByText(/2022/i)).toHaveLength(7));
|
||||||
|
await waitFor(() => expect(screen.queryAllByText(/2021/i)).toHaveLength(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a collapsable search bar', async () => {
|
||||||
|
jest.spyOn(api, 'get').
|
||||||
|
mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }).
|
||||||
|
mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredExclude } });
|
||||||
render(<StateVulnerabilitiesWrapper />);
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
const cveSearchInput = screen.getByPlaceholderText(/search/i);
|
const cveSearchInput = screen.getByPlaceholderText(/search/i);
|
||||||
jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } });
|
const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0];
|
||||||
await userEvent.type(cveSearchInput, '2022');
|
await fireEvent.click(expandSearch);
|
||||||
expect((await screen.queryAllByText(/2023/i).length) === 0);
|
await waitFor(() =>
|
||||||
expect((await screen.findAllByText(/2022/i)).length === 6);
|
expect(screen.getAllByPlaceholderText("Exclude")).toHaveLength(1)
|
||||||
});
|
);
|
||||||
|
const excludeInput = screen.getByPlaceholderText("Exclude");
|
||||||
|
userEvent.type(excludeInput, '2022');
|
||||||
|
expect(excludeInput).toHaveValue('2022')
|
||||||
|
await waitFor(() => expect(screen.queryAllByText(/2022/i)).toHaveLength(0));
|
||||||
|
await waitFor(() => expect(screen.queryAllByText(/2021/i)).toHaveLength(6));
|
||||||
|
})
|
||||||
|
|
||||||
it('renders no vulnerabilities if there are not any', async () => {
|
it('renders no vulnerabilities if there are not any', async () => {
|
||||||
jest.spyOn(api, 'get').mockResolvedValue({
|
jest.spyOn(api, 'get').mockResolvedValue({
|
||||||
status: 200,
|
status: 200,
|
||||||
data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [] } } }
|
data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [], Summary: {} } } }
|
||||||
});
|
});
|
||||||
render(<StateVulnerabilitiesWrapper />);
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1));
|
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open and close description dropdown for vulnerabilities', async () => {
|
it('should show description for vulnerabilities', async () => {
|
||||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
|
||||||
|
.mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageOne } });
|
||||||
render(<StateVulnerabilitiesWrapper />);
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
await waitFor(() => expect(screen.getAllByText(/description/i)).toHaveLength(20));
|
const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon');
|
||||||
const openText = screen.getAllByText(/description/i);
|
fireEvent.click(expandListBtn[0]);
|
||||||
await fireEvent.click(openText[0]);
|
await waitFor(() => expect(screen.getAllByText(/Description/)).toHaveLength(20));
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1)
|
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1)
|
||||||
);
|
);
|
||||||
await fireEvent.click(openText[0]);
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(screen.queryByText(/CPAN 2.28 allows Signature Verification Bypass./i)).not.toBeInTheDocument()
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log an error when data can't be fetched", async () => {
|
it("should log an error when data can't be fetched", async () => {
|
||||||
@ -551,23 +749,65 @@ describe('Vulnerabilties page', () => {
|
|||||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } });
|
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } });
|
||||||
render(<StateVulnerabilitiesWrapper />);
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||||
await fireEvent.click(screen.getAllByText(/fixed in/i)[0]);
|
const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon');
|
||||||
|
fireEvent.click(expandListBtn[1]);
|
||||||
await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument());
|
||||||
const loadMoreBtn = screen.getByText(/load more/i);
|
await waitFor(() => expect(screen.getAllByText(/Load more/).length).toBe(1));
|
||||||
expect(loadMoreBtn).toBeInTheDocument();
|
const loadMoreBtn = screen.getAllByText(/Load more/)[0];
|
||||||
await fireEvent.click(loadMoreBtn);
|
await fireEvent.click(loadMoreBtn);
|
||||||
await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument());
|
await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument());
|
||||||
expect(await screen.findByText('latest')).toBeInTheDocument();
|
expect(await screen.findByText('latest')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show the list of vulnerable packages for the CVEs', async () => {
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: simpleMockCVEList } })
|
||||||
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
|
const expandListBtn = await screen.findByTestId('expand-list-view-toggle');
|
||||||
|
fireEvent.click(expandListBtn);
|
||||||
|
const packageLists = await screen.findAllByTestId('cve-package-list');
|
||||||
|
expect(packageLists.length).toEqual(2); // Data set has 2 CVEs, so 2 package lists
|
||||||
|
|
||||||
|
const expectedData = [
|
||||||
|
{
|
||||||
|
Name: 'perl-base',
|
||||||
|
PackagePath: 'Not Specified',
|
||||||
|
InstalledVersion: '5.30.0-9ubuntu0.2',
|
||||||
|
FixedVersion: 'Not Specified'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'org.springframework:spring-web',
|
||||||
|
PackagePath: 'usr/local/tomcat/webapps/spring4shell.war/WEB-INF/lib/spring-web-5.3.15.jar',
|
||||||
|
InstalledVersion: '5.3.15',
|
||||||
|
FixedVersion: '6.0.0'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let index = 0; index < 2; index++) {
|
||||||
|
const expectedPackageData = expectedData[index];
|
||||||
|
const container = packageLists[index];
|
||||||
|
const pkgName = await within(container).findAllByTestId('cve-info-pkg-name');
|
||||||
|
expect(pkgName).toHaveLength(1);
|
||||||
|
expect(pkgName[0]).toHaveTextContent(expectedPackageData.Name);
|
||||||
|
|
||||||
|
const pkgPath = await within(container).findAllByTestId('cve-info-pkg-path');
|
||||||
|
expect(pkgPath).toHaveLength(1);
|
||||||
|
expect(pkgPath[0]).toHaveTextContent(expectedPackageData.PackagePath);
|
||||||
|
|
||||||
|
const pkgInstalledVer = await within(container).findAllByTestId('cve-info-pkg-install-ver');
|
||||||
|
expect(pkgInstalledVer).toHaveLength(1);
|
||||||
|
expect(pkgInstalledVer[0]).toHaveTextContent(expectedPackageData.InstalledVersion);
|
||||||
|
|
||||||
|
const pkgFixedVer = await within(container).findAllByTestId('cve-info-pkg-fixed-ver');
|
||||||
|
expect(pkgFixedVer).toHaveLength(1);
|
||||||
|
expect(pkgFixedVer[0]).toHaveTextContent(expectedPackageData.FixedVersion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should allow export of vulnerabilities list', async () => {
|
it('should allow export of vulnerabilities list', async () => {
|
||||||
const xlsxMock = jest.createMockFromModule('xlsx');
|
const xlsxMock = jest.createMockFromModule('xlsx');
|
||||||
xlsxMock.writeFile = jest.fn();
|
xlsxMock.writeFile = jest.fn();
|
||||||
|
|
||||||
jest
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
||||||
.spyOn(api, 'get')
|
|
||||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
|
|
||||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
|
|
||||||
render(<StateVulnerabilitiesWrapper />);
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||||
const downloadBtn = await screen.findAllByTestId('DownloadIcon');
|
const downloadBtn = await screen.findAllByTestId('DownloadIcon');
|
||||||
@ -580,12 +820,42 @@ describe('Vulnerabilties page', () => {
|
|||||||
await fireEvent.click(exportAsCSVBtn);
|
await fireEvent.click(exportAsCSVBtn);
|
||||||
expect(await screen.findByTestId('export-csv-menuItem')).not.toBeInTheDocument();
|
expect(await screen.findByTestId('export-csv-menuItem')).not.toBeInTheDocument();
|
||||||
fireEvent.click(downloadBtn[0]);
|
fireEvent.click(downloadBtn[0]);
|
||||||
const exportAsExcelBtn = screen.getByText(/MS Excel/i);
|
const exportAsExcelBtn = screen.getByText(/xlsx/i);
|
||||||
expect(exportAsExcelBtn).toBeInTheDocument();
|
expect(exportAsExcelBtn).toBeInTheDocument();
|
||||||
await fireEvent.click(exportAsExcelBtn);
|
await fireEvent.click(exportAsExcelBtn);
|
||||||
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument();
|
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should log an error when data can't be fetched for downloading", async () => {
|
||||||
|
const xlsxMock = jest.createMockFromModule('xlsx');
|
||||||
|
xlsxMock.writeFile = jest.fn();
|
||||||
|
|
||||||
|
jest.spyOn(api, 'get').
|
||||||
|
mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }).
|
||||||
|
mockRejectedValue({ status: 500, data: {} });
|
||||||
|
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||||
|
const downloadBtn = await screen.findAllByTestId('DownloadIcon');
|
||||||
|
fireEvent.click(downloadBtn[0]);
|
||||||
|
expect(await screen.findByTestId('export-csv-menuItem')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByTestId('export-excel-menuItem')).toBeInTheDocument();
|
||||||
|
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expand/collapse the list of CVEs', async () => {
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
|
||||||
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageOne } });
|
||||||
|
const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon');
|
||||||
|
fireEvent.click(expandListBtn[0]);
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20));
|
||||||
|
const collapseListBtn = await screen.findAllByTestId('ViewHeadlineIcon');
|
||||||
|
fireEvent.click(collapseListBtn[0]);
|
||||||
|
expect(await screen.findByText('Fixed in')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle fixed CVE query errors', async () => {
|
it('should handle fixed CVE query errors', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(api, 'get')
|
.spyOn(api, 'get')
|
||||||
@ -594,7 +864,8 @@ describe('Vulnerabilties page', () => {
|
|||||||
render(<StateVulnerabilitiesWrapper />);
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
await fireEvent.click(screen.getAllByText(/fixed in/i)[0]);
|
const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon');
|
||||||
|
fireEvent.click(expandListBtn[1]);
|
||||||
await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument());
|
||||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||||
});
|
});
|
||||||
|
24
src/api.js
24
src/api.js
@ -67,11 +67,14 @@ const api = {
|
|||||||
return axios.put(urli, payload, config);
|
return axios.put(urli, payload, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
delete(urli, abortSignal, cfg) {
|
delete(urli, params, abortSignal, cfg) {
|
||||||
let config = isEmpty(cfg) ? this.getRequestCfg() : cfg;
|
let config = isEmpty(cfg) ? this.getRequestCfg() : cfg;
|
||||||
if (!isEmpty(abortSignal) && isEmpty(config.signal)) {
|
if (!isEmpty(abortSignal) && isEmpty(config.signal)) {
|
||||||
config = { ...config, signal: abortSignal };
|
config = { ...config, signal: abortSignal };
|
||||||
}
|
}
|
||||||
|
if (!isEmpty(params)) {
|
||||||
|
config = { ...config, params };
|
||||||
|
}
|
||||||
return axios.delete(urli, config);
|
return axios.delete(urli, config);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -81,6 +84,7 @@ const endpoints = {
|
|||||||
authConfig: `/v2/_zot/ext/mgmt`,
|
authConfig: `/v2/_zot/ext/mgmt`,
|
||||||
openidAuth: `/zot/auth/login`,
|
openidAuth: `/zot/auth/login`,
|
||||||
logout: `/zot/auth/logout`,
|
logout: `/zot/auth/logout`,
|
||||||
|
apiKeys: '/zot/auth/apikey',
|
||||||
deleteImage: (name, tag) => `/v2/${name}/manifests/${tag}`,
|
deleteImage: (name, tag) => `/v2/${name}/manifests/${tag}`,
|
||||||
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
||||||
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
||||||
@ -90,17 +94,29 @@ const endpoints = {
|
|||||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor IsDeletable } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
|
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor IsDeletable } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||||
detailedImageInfo: (name, tag) =>
|
detailedImageInfo: (name, tag) =>
|
||||||
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
||||||
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '') => {
|
vulnerabilitiesForRepo: (
|
||||||
|
name,
|
||||||
|
{ pageNumber = 1, pageSize = 15 },
|
||||||
|
searchTerm = '',
|
||||||
|
excludedTerm = '',
|
||||||
|
severity = ''
|
||||||
|
) => {
|
||||||
let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||||
(pageNumber - 1) * pageSize
|
(pageNumber - 1) * pageSize
|
||||||
}}`;
|
}}`;
|
||||||
if (!isEmpty(searchTerm)) {
|
if (!isEmpty(searchTerm)) {
|
||||||
query += `, searchedCVE: "${searchTerm}"`;
|
query += `, searchedCVE: "${searchTerm}"`;
|
||||||
}
|
}
|
||||||
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`;
|
if (!isEmpty(excludedTerm)) {
|
||||||
|
query += `, excludedCVE: "${excludedTerm}"`;
|
||||||
|
}
|
||||||
|
if (!isEmpty(severity)) {
|
||||||
|
query += `, severity: "${severity}"`;
|
||||||
|
}
|
||||||
|
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name PackagePath InstalledVersion FixedVersion}} Summary {Count UnknownCount LowCount MediumCount HighCount CriticalCount}}}`;
|
||||||
},
|
},
|
||||||
allVulnerabilitiesForRepo: (name) =>
|
allVulnerabilitiesForRepo: (name) =>
|
||||||
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name InstalledVersion FixedVersion}}}}`,
|
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name PackagePath InstalledVersion FixedVersion}}}}`,
|
||||||
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }, filter = {}) => {
|
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }, filter = {}) => {
|
||||||
let filterParam = '';
|
let filterParam = '';
|
||||||
if (filter.Os || filter.Arch) {
|
if (filter.Os || filter.Arch) {
|
||||||
|
@ -132,8 +132,14 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
|||||||
|
|
||||||
const handleSearch = (event) => {
|
const handleSearch = (event) => {
|
||||||
const { key, type } = event;
|
const { key, type } = event;
|
||||||
|
const name = event.target.value;
|
||||||
if (key === 'Enter' || type === 'click') {
|
if (key === 'Enter' || type === 'click') {
|
||||||
navigate({ pathname: `/explore`, search: createSearchParams({ search: inputValue || '' }).toString() });
|
if (name?.includes(':')) {
|
||||||
|
const splitName = name.split(':');
|
||||||
|
navigate(`/image/${encodeURIComponent(splitName[0])}/tag/${splitName[1]}`);
|
||||||
|
} else {
|
||||||
|
navigate({ pathname: `/explore`, search: createSearchParams({ search: inputValue || '' }).toString() });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,11 +2,17 @@ import React, { useState } from 'react';
|
|||||||
|
|
||||||
import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material';
|
import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material';
|
||||||
|
|
||||||
import { getLoggedInUser, logoutUser } from '../../utilities/authUtilities';
|
import { getLoggedInUser, logoutUser, isApiKeyEnabled } from '../../utilities/authUtilities';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
function UserAccountMenu() {
|
function UserAccountMenu() {
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
const openMenu = Boolean(anchorEl);
|
const openMenu = Boolean(anchorEl);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const apiKeyManagement = () => {
|
||||||
|
navigate('/user/apikey');
|
||||||
|
};
|
||||||
|
|
||||||
const handleUserClick = (event) => {
|
const handleUserClick = (event) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@ -37,6 +43,8 @@ function UserAccountMenu() {
|
|||||||
>
|
>
|
||||||
<MenuItem onClick={handleUserClose}>{getLoggedInUser()}</MenuItem>
|
<MenuItem onClick={handleUserClose}>{getLoggedInUser()}</MenuItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
{isApiKeyEnabled() && <MenuItem onClick={apiKeyManagement}>API Keys</MenuItem>}
|
||||||
|
<Divider />
|
||||||
<MenuItem onClick={logoutUser}>Log out</MenuItem>
|
<MenuItem onClick={logoutUser}>Log out</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
|
@ -312,7 +312,13 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
|||||||
Welcome back! Please login.
|
Welcome back! Please login.
|
||||||
</Typography>
|
</Typography>
|
||||||
{renderThirdPartyLoginMethods()}
|
{renderThirdPartyLoginMethods()}
|
||||||
{Object.keys(authMethods).length > 1 && <Divider className={classes.divider}>or</Divider>}
|
{Object.keys(authMethods).length > 1 &&
|
||||||
|
Object.keys(authMethods).includes('openid') &&
|
||||||
|
Object.keys(authMethods.openid.providers).length > 0 && (
|
||||||
|
<Divider className={classes.divider} data-testId="openid-divider">
|
||||||
|
or
|
||||||
|
</Divider>
|
||||||
|
)}
|
||||||
{Object.keys(authMethods).includes('htpasswd') && (
|
{Object.keys(authMethods).includes('htpasswd') && (
|
||||||
<Box component="form" onSubmit={null} noValidate autoComplete="off">
|
<Box component="form" onSubmit={null} noValidate autoComplete="off">
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -9,6 +9,8 @@ import { isEmpty, uniq } from 'lodash';
|
|||||||
import { api, endpoints } from '../../api';
|
import { api, endpoints } from '../../api';
|
||||||
import { host } from '../../host';
|
import { host } from '../../host';
|
||||||
import { useParams, useNavigate, createSearchParams } from 'react-router-dom';
|
import { useParams, useNavigate, createSearchParams } from 'react-router-dom';
|
||||||
|
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
|
||||||
|
import { isAuthenticated } from 'utilities/authUtilities';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { Card, CardContent, CardMedia, Chip, Grid, Stack, Tooltip, Typography, IconButton } from '@mui/material';
|
import { Card, CardContent, CardMedia, Chip, Grid, Stack, Tooltip, Typography, IconButton } from '@mui/material';
|
||||||
@ -16,7 +18,11 @@ import BookmarkIcon from '@mui/icons-material/Bookmark';
|
|||||||
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
||||||
import StarIcon from '@mui/icons-material/Star';
|
import StarIcon from '@mui/icons-material/Star';
|
||||||
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
import Tags from './Tabs/Tags.jsx';
|
||||||
|
import RepoDetailsMetadata from './RepoDetailsMetadata';
|
||||||
|
import Loading from '../Shared/Loading';
|
||||||
|
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||||
|
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||||
|
|
||||||
// placeholder images
|
// placeholder images
|
||||||
import repocube1 from '../../assets/repocube-1.png';
|
import repocube1 from '../../assets/repocube-1.png';
|
||||||
@ -24,13 +30,7 @@ import repocube2 from '../../assets/repocube-2.png';
|
|||||||
import repocube3 from '../../assets/repocube-3.png';
|
import repocube3 from '../../assets/repocube-3.png';
|
||||||
import repocube4 from '../../assets/repocube-4.png';
|
import repocube4 from '../../assets/repocube-4.png';
|
||||||
|
|
||||||
import Tags from './Tabs/Tags.jsx';
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
import RepoDetailsMetadata from './RepoDetailsMetadata';
|
|
||||||
import Loading from '../Shared/Loading';
|
|
||||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
|
||||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
|
||||||
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
|
|
||||||
import { isAuthenticated } from 'utilities/authUtilities';
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
pageWrapper: {
|
pageWrapper: {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
|
||||||
|
import transform from 'utilities/transform';
|
||||||
|
|
||||||
import { Card, CardContent, Typography, Grid, Divider, Stack, Collapse } from '@mui/material';
|
import { Card, CardContent, Typography, Grid, Divider, Stack, Collapse } from '@mui/material';
|
||||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||||
import transform from 'utilities/transform';
|
|
||||||
import { useState } from 'react';
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles(() => ({
|
||||||
card: {
|
card: {
|
||||||
|
@ -13,6 +13,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||||
import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||||
import { CVE_FIXEDIN_PAGE_SIZE } from 'utilities/paginationConstants';
|
import { CVE_FIXEDIN_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||||
|
import VulnerabilityPackageSection from './VulnerabilityPackageSection';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
card: {
|
card: {
|
||||||
@ -29,18 +30,46 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
marginTop: '2rem',
|
marginTop: '2rem',
|
||||||
marginBottom: '2rem'
|
marginBottom: '2rem'
|
||||||
},
|
},
|
||||||
|
cardCollapsed: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: '#FFFFFF',
|
||||||
|
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||||
|
border: '1px solid #E0E5EB',
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
flex: 'none',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
color: '#606060',
|
color: '#606060',
|
||||||
padding: '2% 3% 2% 3%',
|
padding: '2% 3% 2% 3%',
|
||||||
width: '100%'
|
width: '100%'
|
||||||
},
|
},
|
||||||
|
contentCollapsed: {
|
||||||
|
textAlign: 'left',
|
||||||
|
color: '#606060',
|
||||||
|
padding: '1% 3% 1% 3%',
|
||||||
|
width: '100%',
|
||||||
|
'&:last-child': {
|
||||||
|
paddingBottom: '1%'
|
||||||
|
}
|
||||||
|
},
|
||||||
cveId: {
|
cveId: {
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
textDecoration: 'underline'
|
textDecoration: 'underline'
|
||||||
},
|
},
|
||||||
|
cveIdCollapsed: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
textDecoration: 'underline',
|
||||||
|
flexBasis: '19%'
|
||||||
|
},
|
||||||
cveSummary: {
|
cveSummary: {
|
||||||
color: theme.palette.secondary.dark,
|
color: theme.palette.secondary.dark,
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
@ -48,6 +77,13 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
marginTop: '0.5rem'
|
marginTop: '0.5rem'
|
||||||
},
|
},
|
||||||
|
cveSummaryCollapsed: {
|
||||||
|
color: theme.palette.secondary.dark,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
flexBasis: '82%'
|
||||||
|
},
|
||||||
link: {
|
link: {
|
||||||
color: '#52637A',
|
color: '#52637A',
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
@ -66,15 +102,21 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
|
dropdownCVE: {
|
||||||
|
color: '#1479FF',
|
||||||
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
vulnerabilityCardDivider: {
|
vulnerabilityCardDivider: {
|
||||||
margin: '1rem 0'
|
margin: '1rem 0'
|
||||||
|
},
|
||||||
|
cveInfo: {
|
||||||
|
marginTop: '2%'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
function VulnerabilitiyCard(props) {
|
function VulnerabilitiyCard(props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { cve, name, platform } = props;
|
const { cve, name, platform, expand } = props;
|
||||||
const [openDesc, setOpenDesc] = useState(false);
|
const [openCVE, setOpenCVE] = useState(expand);
|
||||||
const [openFixed, setOpenFixed] = useState(false);
|
|
||||||
const [loadingFixed, setLoadingFixed] = useState(true);
|
const [loadingFixed, setLoadingFixed] = useState(true);
|
||||||
const [fixedInfo, setFixedInfo] = useState([]);
|
const [fixedInfo, setFixedInfo] = useState([]);
|
||||||
const abortController = useMemo(() => new AbortController(), []);
|
const abortController = useMemo(() => new AbortController(), []);
|
||||||
@ -82,9 +124,10 @@ function VulnerabilitiyCard(props) {
|
|||||||
// pagination props
|
// pagination props
|
||||||
const [pageNumber, setPageNumber] = useState(1);
|
const [pageNumber, setPageNumber] = useState(1);
|
||||||
const [isEndOfList, setIsEndOfList] = useState(false);
|
const [isEndOfList, setIsEndOfList] = useState(false);
|
||||||
|
const [loadMoreInfo, setLoadMoreInfo] = useState(false);
|
||||||
|
|
||||||
const getPaginatedResults = () => {
|
const getPaginatedResults = () => {
|
||||||
if (!openFixed || isEndOfList) {
|
if (!openCVE || (!loadMoreInfo && !isEmpty(fixedInfo)) || isEndOfList) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoadingFixed(true);
|
setLoadingFixed(true);
|
||||||
@ -107,11 +150,13 @@ function VulnerabilitiyCard(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
setLoadingFixed(false);
|
setLoadingFixed(false);
|
||||||
|
setLoadMoreInfo(false);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setIsEndOfList(true);
|
setIsEndOfList(true);
|
||||||
setLoadingFixed(false);
|
setLoadingFixed(false);
|
||||||
|
setLoadMoreInfo(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -120,10 +165,15 @@ function VulnerabilitiyCard(props) {
|
|||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
}, [openFixed, pageNumber]);
|
}, [openCVE, pageNumber, loadMoreInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpenCVE(expand);
|
||||||
|
}, [expand]);
|
||||||
|
|
||||||
const loadMore = () => {
|
const loadMore = () => {
|
||||||
if (loadingFixed || isEndOfList) return;
|
if (loadingFixed || isEndOfList) return;
|
||||||
|
setLoadMoreInfo(true);
|
||||||
setPageNumber((pageNumber) => pageNumber + 1);
|
setPageNumber((pageNumber) => pageNumber + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -163,27 +213,65 @@ function VulnerabilitiyCard(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={classes.card} raised>
|
<Card className={openCVE ? classes.card : classes.cardCollapsed} raised>
|
||||||
<CardContent className={classes.content}>
|
<CardContent className={openCVE ? classes.content : classes.contentCollapsed}>
|
||||||
<Stack direction="row" spacing="1.25rem">
|
<Stack direction="row" spacing={openCVE ? '1.25rem' : '0.5rem'}>
|
||||||
<Typography variant="body1" align="left" className={classes.cveId}>
|
{!openCVE ? (
|
||||||
|
<KeyboardArrowRight className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowDown className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} />
|
||||||
|
)}
|
||||||
|
<Typography variant="body1" align="left" className={openCVE ? classes.cveId : classes.cveIdCollapsed}>
|
||||||
{cve.id}
|
{cve.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
{openCVE ? (
|
||||||
</Stack>
|
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
||||||
<Typography variant="body1" align="left" className={classes.cveSummary}>
|
|
||||||
{cve.title}
|
|
||||||
</Typography>
|
|
||||||
<Divider className={classes.vulnerabilityCardDivider} />
|
|
||||||
<Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}>
|
|
||||||
{!openFixed ? (
|
|
||||||
<KeyboardArrowRight className={classes.dropdownText} />
|
|
||||||
) : (
|
) : (
|
||||||
<KeyboardArrowDown className={classes.dropdownText} />
|
<Stack direction="row" spacing="0.5rem" flexBasis="90%">
|
||||||
|
<div style={{ transform: 'scale(0.8)', flexBasis: '18%', flexShrink: '0' }}>
|
||||||
|
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
||||||
|
</div>
|
||||||
|
<Typography variant="body1" align="left" className={classes.cveSummaryCollapsed}>
|
||||||
|
{cve.title}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<Typography className={classes.dropdownText}>Fixed in</Typography>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Collapse in={openFixed} timeout="auto" unmountOnExit>
|
<Collapse in={openCVE} timeout="auto" unmountOnExit>
|
||||||
|
<Typography variant="body1" align="left" className={classes.cveSummary}>
|
||||||
|
{cve.title}
|
||||||
|
</Typography>
|
||||||
|
<Divider className={classes.vulnerabilityCardDivider} />
|
||||||
|
<Typography variant="body2" align="left" className={classes.cveInfo}>
|
||||||
|
External reference
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
align="left"
|
||||||
|
sx={{ color: '#0F2139', fontSize: '1rem', textDecoration: 'underline' }}
|
||||||
|
component={Link}
|
||||||
|
to={cve.reference}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{cve.reference}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" align="left" className={classes.cveInfo}>
|
||||||
|
Packages
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
spacing="0.3rem"
|
||||||
|
sx={{ width: '100%', padding: '0.5rem 0' }}
|
||||||
|
data-testid="cve-package-list"
|
||||||
|
>
|
||||||
|
{cve.packageList.map((pkg) => (
|
||||||
|
<VulnerabilityPackageSection key={`${cve.id}-${pkg.packageName}`} cve={pkg} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="body2" align="left" className={classes.cveInfo}>
|
||||||
|
Fixed in
|
||||||
|
</Typography>
|
||||||
<Box sx={{ width: '100%', padding: '0.5rem 0' }}>
|
<Box sx={{ width: '100%', padding: '0.5rem 0' }}>
|
||||||
{loadingFixed ? (
|
{loadingFixed ? (
|
||||||
'Loading...'
|
'Loading...'
|
||||||
@ -194,16 +282,9 @@ function VulnerabilitiyCard(props) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Collapse>
|
<Typography variant="body2" align="left" className={classes.cveInfo}>
|
||||||
<Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}>
|
Description
|
||||||
{!openDesc ? (
|
</Typography>
|
||||||
<KeyboardArrowRight className={classes.dropdownText} />
|
|
||||||
) : (
|
|
||||||
<KeyboardArrowDown className={classes.dropdownText} />
|
|
||||||
)}
|
|
||||||
<Typography className={classes.dropdownText}>Description</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Collapse in={openDesc} timeout="auto" unmountOnExit>
|
|
||||||
<Box sx={{ padding: '0.5rem 0' }}>
|
<Box sx={{ padding: '0.5rem 0' }}>
|
||||||
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}>
|
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}>
|
||||||
{cve.description}
|
{cve.description}
|
||||||
|
100
src/components/Shared/VulnerabilityCountCard.jsx
Normal file
100
src/components/Shared/VulnerabilityCountCard.jsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
import { Stack, Tooltip } from '@mui/material';
|
||||||
|
|
||||||
|
const criticalColor = '#ff5c74';
|
||||||
|
const criticalBorderColor = '#f9546d';
|
||||||
|
|
||||||
|
const highColor = '#ff6840';
|
||||||
|
const highBorderColor = '#ee6b49';
|
||||||
|
|
||||||
|
const mediumColor = '#ffa052';
|
||||||
|
const mediumBorderColor = '#f19d5b';
|
||||||
|
|
||||||
|
const lowColor = '#f9f486';
|
||||||
|
const lowBorderColor = '#f0ed94';
|
||||||
|
|
||||||
|
const unknownColor = '#f2ffdd';
|
||||||
|
const unknownBorderColor = '#e9f4d7';
|
||||||
|
|
||||||
|
const totalBorderColor = '#e0e5eb';
|
||||||
|
|
||||||
|
const fontSize = '0.75rem';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
cveCountCard: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: '0.5rem',
|
||||||
|
paddingRight: '0.5rem',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: '600',
|
||||||
|
borderRadius: '3px',
|
||||||
|
marginBottom: '0',
|
||||||
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
|
totalSeverity: {
|
||||||
|
border: '1px solid ' + totalBorderColor
|
||||||
|
},
|
||||||
|
severityList: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5em'
|
||||||
|
},
|
||||||
|
criticalSeverity: {
|
||||||
|
backgroundColor: criticalColor,
|
||||||
|
border: '1px solid ' + criticalBorderColor
|
||||||
|
},
|
||||||
|
highSeverity: {
|
||||||
|
backgroundColor: highColor,
|
||||||
|
border: '1px solid ' + highBorderColor
|
||||||
|
},
|
||||||
|
mediumSeverity: {
|
||||||
|
backgroundColor: mediumColor,
|
||||||
|
border: '1px solid ' + mediumBorderColor
|
||||||
|
},
|
||||||
|
lowSeverity: {
|
||||||
|
backgroundColor: lowColor,
|
||||||
|
border: '1px solid ' + lowBorderColor
|
||||||
|
},
|
||||||
|
unknownSeverity: {
|
||||||
|
backgroundColor: unknownColor,
|
||||||
|
border: '1px solid ' + unknownBorderColor
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function VulnerabilitiyCountCard(props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { total, critical, high, medium, low, unknown, filterBySeverity } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="row" spacing="0.5em">
|
||||||
|
<Tooltip title="Total" onClick={() => filterBySeverity('')}>
|
||||||
|
<div className={[classes.cveCountCard, classes.totalSeverity].join(' ')}>Total {total}</div>
|
||||||
|
</Tooltip>
|
||||||
|
<div className={classes.severityList}>
|
||||||
|
<Tooltip title="Critical" onClick={() => filterBySeverity('CRITICAL')}>
|
||||||
|
<div className={[classes.cveCountCard, classes.criticalSeverity].join(' ')}>C {critical}</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="High" onClick={() => filterBySeverity('HIGH')}>
|
||||||
|
<div className={[classes.cveCountCard, classes.highSeverity].join(' ')}>H {high}</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Medium" onClick={() => filterBySeverity('MEDIUM')}>
|
||||||
|
<div className={[classes.cveCountCard, classes.mediumSeverity].join(' ')}>M {medium}</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Low" onClick={() => filterBySeverity('LOW')}>
|
||||||
|
<div className={[classes.cveCountCard, classes.lowSeverity].join(' ')}>L {low}</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Unknown" onClick={() => filterBySeverity('UNKNOWN')}>
|
||||||
|
<div className={[classes.cveCountCard, classes.unknownSeverity].join(' ')}>U {unknown}</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VulnerabilitiyCountCard;
|
69
src/components/Shared/VulnerabilityPackageSection.jsx
Normal file
69
src/components/Shared/VulnerabilityPackageSection.jsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Divider, Grid, Stack, Typography } from '@mui/material';
|
||||||
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
cvePackageCard: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: '#FFFFFF',
|
||||||
|
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||||
|
border: '1px solid #E0E5EB',
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
flex: 'none',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
cveInfo: {
|
||||||
|
marginTop: '2%'
|
||||||
|
},
|
||||||
|
vulnerabilityCardDivider: {
|
||||||
|
margin: '1rem 1rem'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function VulnerabilityPackageSection(props) {
|
||||||
|
const { cve } = props;
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
spacing="0.2rem"
|
||||||
|
sx={{ width: '100%', padding: '0.2rem 0.5rem' }}
|
||||||
|
data-testid="cve-package-section"
|
||||||
|
>
|
||||||
|
<Typography variant="overline" color="primary" data-testid="cve-info-pkg-name" sx={{ fontWeight: 'bold' }}>
|
||||||
|
{cve.packageName}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" className={classes.cveInfo}>
|
||||||
|
Package Path
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="primary" data-testid="cve-info-pkg-path">
|
||||||
|
{cve.packagePath}
|
||||||
|
</Typography>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="body2" className={classes.cveInfo}>
|
||||||
|
Installed Version
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="primary" data-testid="cve-info-pkg-install-ver">
|
||||||
|
{cve.packageInstalledVersion}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="body2" className={classes.cveInfo}>
|
||||||
|
Fixed Version
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="primary" data-testid="cve-info-pkg-fixed-ver">
|
||||||
|
{cve.packageFixedVersion}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Divider className={classes.vulnerabilityCardDivider} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VulnerabilityPackageSection;
|
@ -9,6 +9,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
InputBase,
|
InputBase,
|
||||||
|
ToggleButton,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Divider,
|
Divider,
|
||||||
@ -26,16 +27,32 @@ import DownloadIcon from '@mui/icons-material/Download';
|
|||||||
|
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import exportFromJSON from 'export-from-json';
|
import exportFromJSON from 'export-from-json';
|
||||||
|
import ViewHeadlineIcon from '@mui/icons-material/ViewHeadline';
|
||||||
|
import ViewAgendaIcon from '@mui/icons-material/ViewAgenda';
|
||||||
|
|
||||||
|
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
|
||||||
import VulnerabilitiyCard from '../../Shared/VulnerabilityCard';
|
import VulnerabilitiyCard from '../../Shared/VulnerabilityCard';
|
||||||
|
import VulnerabilityCountCard from '../../Shared/VulnerabilityCountCard';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
searchAndDisplayBar: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginBottom: '0'
|
marginBottom: '0'
|
||||||
},
|
},
|
||||||
|
cveCountSummary: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: '0'
|
||||||
|
},
|
||||||
cveId: {
|
cveId: {
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
@ -64,6 +81,7 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
search: {
|
search: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
|
flex: 0.95,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@ -71,6 +89,20 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
border: '0.063rem solid #E7E7E7',
|
border: '0.063rem solid #E7E7E7',
|
||||||
borderRadius: '0.625rem'
|
borderRadius: '0.625rem'
|
||||||
},
|
},
|
||||||
|
expandableSearchInput: {
|
||||||
|
flexGrow: 0.95
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
alignContent: 'right',
|
||||||
|
variant: 'outlined'
|
||||||
|
},
|
||||||
|
viewModes: {
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
maxWidth: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'right'
|
||||||
|
},
|
||||||
searchIcon: {
|
searchIcon: {
|
||||||
color: '#52637A',
|
color: '#52637A',
|
||||||
paddingRight: '3%'
|
paddingRight: '3%'
|
||||||
@ -87,15 +119,27 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
opacity: '1'
|
opacity: '1'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
export: {
|
|
||||||
alignContent: 'right'
|
|
||||||
},
|
|
||||||
popper: {
|
popper: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
padding: '0.3rem',
|
padding: '0.3rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
justifyContent: 'left'
|
||||||
|
},
|
||||||
|
dropdownArrowBox: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
dropdownText: {
|
||||||
|
color: '#1479FF',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
width: '95%'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -103,13 +147,18 @@ function VulnerabilitiesDetails(props) {
|
|||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [cveData, setCveData] = useState([]);
|
const [cveData, setCveData] = useState([]);
|
||||||
const [allCveData, setAllCveData] = useState([]);
|
const [allCveData, setAllCveData] = useState([]);
|
||||||
|
const [cveSummary, setCVESummary] = useState({});
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isLoadingAllCve, setIsLoadingAllCve] = useState(true);
|
const [isLoadingAllCve, setIsLoadingAllCve] = useState(true);
|
||||||
const abortController = useMemo(() => new AbortController(), []);
|
const abortController = useMemo(() => new AbortController(), []);
|
||||||
const { name, tag, digest, platform } = props;
|
const { name, tag, digest, platform } = props;
|
||||||
|
|
||||||
|
const [openExcludeSearch, setOpenExcludeSearch] = useState(false);
|
||||||
|
|
||||||
// pagination props
|
// pagination props
|
||||||
const [cveFilter, setCveFilter] = useState('');
|
const [cveFilter, setCveFilter] = useState('');
|
||||||
|
const [cveExcludeFilter, setCveExcludeFilter] = useState('');
|
||||||
|
const [cveSeverityFilter, setCveSeverityFilter] = useState('');
|
||||||
const [pageNumber, setPageNumber] = useState(1);
|
const [pageNumber, setPageNumber] = useState(1);
|
||||||
const [isEndOfList, setIsEndOfList] = useState(false);
|
const [isEndOfList, setIsEndOfList] = useState(false);
|
||||||
const listBottom = useRef(null);
|
const listBottom = useRef(null);
|
||||||
@ -117,6 +166,8 @@ function VulnerabilitiesDetails(props) {
|
|||||||
const [anchorExport, setAnchorExport] = useState(null);
|
const [anchorExport, setAnchorExport] = useState(null);
|
||||||
const openExport = Boolean(anchorExport);
|
const openExport = Boolean(anchorExport);
|
||||||
|
|
||||||
|
const [selectedViewMore, setSelectedViewMore] = useState(false);
|
||||||
|
|
||||||
const getCVERequestName = () => {
|
const getCVERequestName = () => {
|
||||||
return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;
|
return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;
|
||||||
};
|
};
|
||||||
@ -127,16 +178,32 @@ function VulnerabilitiesDetails(props) {
|
|||||||
`${host()}${endpoints.vulnerabilitiesForRepo(
|
`${host()}${endpoints.vulnerabilitiesForRepo(
|
||||||
getCVERequestName(),
|
getCVERequestName(),
|
||||||
{ pageNumber, pageSize: EXPLORE_PAGE_SIZE },
|
{ pageNumber, pageSize: EXPLORE_PAGE_SIZE },
|
||||||
cveFilter
|
cveFilter,
|
||||||
|
cveExcludeFilter,
|
||||||
|
cveSeverityFilter
|
||||||
)}`,
|
)}`,
|
||||||
abortController.signal
|
abortController.signal
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data && response.data.data) {
|
if (response.data && response.data.data) {
|
||||||
let cveInfo = response.data.data.CVEListForImage?.CVEList;
|
let cveInfo = response.data.data.CVEListForImage?.CVEList;
|
||||||
|
let summary = response.data.data.CVEListForImage?.Summary;
|
||||||
let cveListData = mapCVEInfo(cveInfo);
|
let cveListData = mapCVEInfo(cveInfo);
|
||||||
setCveData((previousState) => (pageNumber === 1 ? cveListData : [...previousState, ...cveListData]));
|
setCveData((previousState) => (pageNumber === 1 ? cveListData : [...previousState, ...cveListData]));
|
||||||
setIsEndOfList(response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE);
|
setIsEndOfList(response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE);
|
||||||
|
setCVESummary((previousState) => {
|
||||||
|
if (isEmpty(summary)) {
|
||||||
|
return previousState;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
Count: summary.Count,
|
||||||
|
UnknownCount: summary.UnknownCount,
|
||||||
|
LowCount: summary.LowCount,
|
||||||
|
MediumCount: summary.MediumCount,
|
||||||
|
HighCount: summary.HighCount,
|
||||||
|
CriticalCount: summary.CriticalCount
|
||||||
|
};
|
||||||
|
});
|
||||||
} else if (response.data.errors) {
|
} else if (response.data.errors) {
|
||||||
setIsEndOfList(true);
|
setIsEndOfList(true);
|
||||||
}
|
}
|
||||||
@ -146,6 +213,7 @@ function VulnerabilitiesDetails(props) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setCveData([]);
|
setCveData([]);
|
||||||
|
setCVESummary(() => {});
|
||||||
setIsEndOfList(true);
|
setIsEndOfList(true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -182,7 +250,7 @@ function VulnerabilitiesDetails(props) {
|
|||||||
const wb = XLSX.utils.book_new(),
|
const wb = XLSX.utils.book_new(),
|
||||||
ws = XLSX.utils.json_to_sheet(allCveData);
|
ws = XLSX.utils.json_to_sheet(allCveData);
|
||||||
|
|
||||||
XLSX.utils.book_append_sheet(wb, ws, name + '_' + tag);
|
XLSX.utils.book_append_sheet(wb, ws, 'vulnerabilities');
|
||||||
|
|
||||||
XLSX.writeFile(wb, `${name}:${tag}-vulnerabilities.xlsx`);
|
XLSX.writeFile(wb, `${name}:${tag}-vulnerabilities.xlsx`);
|
||||||
|
|
||||||
@ -211,7 +279,17 @@ function VulnerabilitiesDetails(props) {
|
|||||||
setAnchorExport(null);
|
setAnchorExport(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExpandCVESearch = () => {
|
||||||
|
setOpenExcludeSearch((openExcludeSearch) => !openExcludeSearch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCveExcludeFilterChange = (e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
setCveExcludeFilter(value);
|
||||||
|
};
|
||||||
|
|
||||||
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
|
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
|
||||||
|
const debouncedExcludeFilterChangeHandler = useMemo(() => debounce(handleCveExcludeFilterChange, 300));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getPaginatedCVEs();
|
getPaginatedCVEs();
|
||||||
@ -245,12 +323,13 @@ function VulnerabilitiesDetails(props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
resetPagination();
|
resetPagination();
|
||||||
}, [cveFilter]);
|
}, [cveFilter, cveExcludeFilter, cveSeverityFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
debouncedChangeHandler.cancel();
|
debouncedChangeHandler.cancel();
|
||||||
|
debouncedExcludeFilterChangeHandler.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -263,13 +342,33 @@ function VulnerabilitiesDetails(props) {
|
|||||||
const renderCVEs = () => {
|
const renderCVEs = () => {
|
||||||
return !isEmpty(cveData) ? (
|
return !isEmpty(cveData) ? (
|
||||||
cveData.map((cve, index) => {
|
cveData.map((cve, index) => {
|
||||||
return <VulnerabilitiyCard key={index} cve={cve} name={name} platform={platform} />;
|
return <VulnerabilitiyCard key={index} cve={cve} name={name} platform={platform} expand={selectedViewMore} />;
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<div>{!isLoading && <Typography className={classes.none}> No Vulnerabilities </Typography>}</div>
|
<div>{!isLoading && <Typography className={classes.none}> No Vulnerabilities </Typography>}</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderCVESummary = () => {
|
||||||
|
if (cveSummary === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isEmpty(cveSummary) ? (
|
||||||
|
<VulnerabilityCountCard
|
||||||
|
total={cveSummary.Count}
|
||||||
|
critical={cveSummary.CriticalCount}
|
||||||
|
high={cveSummary.HighCount}
|
||||||
|
medium={cveSummary.MediumCount}
|
||||||
|
low={cveSummary.LowCount}
|
||||||
|
unknown={cveSummary.UnknownCount}
|
||||||
|
filterBySeverity={setCveSeverityFilter}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderListBottom = () => {
|
const renderListBottom = () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
@ -286,14 +385,37 @@ function VulnerabilitiesDetails(props) {
|
|||||||
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
||||||
Vulnerabilities
|
Vulnerabilities
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton disableRipple onClick={handleClickExport} className={classes.export}>
|
<Stack direction="row" spacing="1rem" className={classes.viewModes}>
|
||||||
<DownloadIcon />
|
<IconButton disableRipple onClick={handleClickExport}>
|
||||||
</IconButton>
|
<DownloadIcon />
|
||||||
<Snackbar
|
</IconButton>
|
||||||
open={openExport && isLoadingAllCve}
|
<Snackbar
|
||||||
message="Getting your data ready for export"
|
open={openExport && isLoadingAllCve}
|
||||||
action={<CircularProgress size="2rem" sx={{ color: '#FFFFFF' }} />}
|
message="Getting your data ready for export"
|
||||||
/>
|
action={<CircularProgress size="2rem" sx={{ color: '#FFFFFF' }} />}
|
||||||
|
/>
|
||||||
|
<ToggleButton
|
||||||
|
value="viewLess"
|
||||||
|
title="Collapse list view"
|
||||||
|
size="small"
|
||||||
|
className={classes.view}
|
||||||
|
selected={!selectedViewMore}
|
||||||
|
onChange={() => setSelectedViewMore(false)}
|
||||||
|
>
|
||||||
|
<ViewHeadlineIcon />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
value="viewMore"
|
||||||
|
title="Expand list view"
|
||||||
|
size="small"
|
||||||
|
className={classes.view}
|
||||||
|
selected={selectedViewMore}
|
||||||
|
onChange={() => setSelectedViewMore(true)}
|
||||||
|
data-testid="expand-list-view-toggle"
|
||||||
|
>
|
||||||
|
<ViewAgendaIcon />
|
||||||
|
</ToggleButton>
|
||||||
|
</Stack>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorExport}
|
anchorEl={anchorExport}
|
||||||
open={openExport}
|
open={openExport}
|
||||||
@ -325,22 +447,46 @@ function VulnerabilitiesDetails(props) {
|
|||||||
className={classes.popper}
|
className={classes.popper}
|
||||||
data-testid="export-excel-menuItem"
|
data-testid="export-excel-menuItem"
|
||||||
>
|
>
|
||||||
MS Excel
|
xlsx
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack className={classes.search}>
|
{renderCVESummary()}
|
||||||
<InputBase
|
<Stack direction="row">
|
||||||
placeholder={'Search'}
|
<div className={classes.dropdownArrowBox} onClick={handleExpandCVESearch}>
|
||||||
classes={{ root: classes.searchInputBase, input: classes.input }}
|
{!openExcludeSearch ? (
|
||||||
onChange={debouncedChangeHandler}
|
<KeyboardArrowRight className={classes.dropdownText} />
|
||||||
/>
|
) : (
|
||||||
<div className={classes.searchIcon}>
|
<KeyboardArrowDown className={classes.dropdownText} />
|
||||||
<SearchIcon />
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Stack className={classes.test} direction="column" spacing="0.25em">
|
||||||
|
<Stack className={classes.search}>
|
||||||
|
<InputBase
|
||||||
|
placeholder={'Search'}
|
||||||
|
classes={{ root: classes.searchInputBase, input: classes.input }}
|
||||||
|
onChange={debouncedChangeHandler}
|
||||||
|
/>
|
||||||
|
<div className={classes.searchIcon}>
|
||||||
|
<SearchIcon />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Collapse in={openExcludeSearch} timeout="auto" unmountOnExit>
|
||||||
|
<Stack className={classes.search}>
|
||||||
|
<InputBase
|
||||||
|
placeholder={'Exclude'}
|
||||||
|
classes={{ root: classes.searchInputBase, input: classes.input }}
|
||||||
|
onChange={debouncedExcludeFilterChangeHandler}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="column" spacing={selectedViewMore ? '1rem' : '0.5rem'}>
|
||||||
|
{renderCVEs()}
|
||||||
|
{renderListBottom()}
|
||||||
</Stack>
|
</Stack>
|
||||||
{renderCVEs()}
|
|
||||||
{renderListBottom()}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,10 @@ import React, { useEffect, useMemo, useState, useRef } from 'react';
|
|||||||
|
|
||||||
// utility
|
// utility
|
||||||
import { api, endpoints } from '../../api';
|
import { api, endpoints } from '../../api';
|
||||||
|
import { host } from '../../host';
|
||||||
import { mapToImage } from '../../utilities/objectModels';
|
import { mapToImage } from '../../utilities/objectModels';
|
||||||
|
import { isEmpty, head } from 'lodash';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -19,23 +22,21 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
InputLabel
|
InputLabel
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import TagDetailsMetadata from './TagDetailsMetadata';
|
||||||
|
import VulnerabilitiesDetails from './Tabs/VulnerabilitiesDetails';
|
||||||
|
import HistoryLayers from './Tabs/HistoryLayers';
|
||||||
|
import DependsOn from './Tabs/DependsOn';
|
||||||
|
import IsDependentOn from './Tabs/IsDependentOn';
|
||||||
|
import Loading from '../Shared/Loading';
|
||||||
|
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||||
|
import ReferredBy from './Tabs/ReferredBy';
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
import { host } from '../../host';
|
|
||||||
|
|
||||||
// placeholder images
|
// placeholder images
|
||||||
import repocube1 from '../../assets/repocube-1.png';
|
import repocube1 from '../../assets/repocube-1.png';
|
||||||
import repocube2 from '../../assets/repocube-2.png';
|
import repocube2 from '../../assets/repocube-2.png';
|
||||||
import repocube3 from '../../assets/repocube-3.png';
|
import repocube3 from '../../assets/repocube-3.png';
|
||||||
import repocube4 from '../../assets/repocube-4.png';
|
import repocube4 from '../../assets/repocube-4.png';
|
||||||
import TagDetailsMetadata from './TagDetailsMetadata';
|
|
||||||
import VulnerabilitiesDetails from './Tabs/VulnerabilitiesDetails';
|
|
||||||
import HistoryLayers from './Tabs/HistoryLayers';
|
|
||||||
import DependsOn from './Tabs/DependsOn';
|
|
||||||
import IsDependentOn from './Tabs/IsDependentOn';
|
|
||||||
import { isEmpty, head } from 'lodash';
|
|
||||||
import Loading from '../Shared/Loading';
|
|
||||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
|
||||||
import ReferredBy from './Tabs/ReferredBy';
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
pageWrapper: {
|
pageWrapper: {
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
|
import transform from '../../utilities/transform';
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||||
import transform from '../../utilities/transform';
|
|
||||||
|
import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
|
||||||
import PullCommandButton from 'components/Shared/PullCommandButton';
|
import PullCommandButton from 'components/Shared/PullCommandButton';
|
||||||
|
|
||||||
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
card: {
|
card: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
163
src/components/User/ApiKeys/ApiKeyCard.jsx
Normal file
163
src/components/User/ApiKeys/ApiKeyCard.jsx
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
|
import { Card, CardContent, Typography, Grid, Divider, Stack, Collapse, Button } from '@mui/material';
|
||||||
|
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||||
|
import ApiKeyRevokeDialog from './ApiKeyRevokeDialog';
|
||||||
|
|
||||||
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
card: {
|
||||||
|
marginBottom: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
border: '1px solid #E0E5EB',
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
flexGrow: 0,
|
||||||
|
order: 0,
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
textAlign: 'left',
|
||||||
|
color: '#52637A',
|
||||||
|
width: '100%',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#FFFFFF'
|
||||||
|
},
|
||||||
|
'&:last-child': {
|
||||||
|
paddingBottom: '1rem'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: '400',
|
||||||
|
paddingRight: '0.5rem',
|
||||||
|
paddingBottom: '0.5rem',
|
||||||
|
paddingTop: '0.5rem',
|
||||||
|
textAlign: 'left',
|
||||||
|
width: '100%',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
|
expirationDate: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: '400',
|
||||||
|
paddingBottom: '0.5rem',
|
||||||
|
paddingTop: '0.5rem',
|
||||||
|
textAlign: 'right'
|
||||||
|
},
|
||||||
|
revokeButton: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'right'
|
||||||
|
},
|
||||||
|
dropdownText: {
|
||||||
|
color: '#1479FF',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
dropdownButton: {
|
||||||
|
color: '#1479FF',
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
|
dropdownContentBox: {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
color: '#52637A',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: '400',
|
||||||
|
padding: '0.75rem',
|
||||||
|
backgroundColor: '#F7F7F7',
|
||||||
|
borderRadius: '0.9rem',
|
||||||
|
overflowWrap: 'break-word'
|
||||||
|
},
|
||||||
|
keyCardDivider: {
|
||||||
|
margin: '1rem 0'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function ApiKeyCard(props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { apiKey, onRevoke } = props;
|
||||||
|
const [openDropdown, setOpenDropdown] = useState(false);
|
||||||
|
const [apiKeyRevokeOpen, setApiKeyRevokeOpen] = useState(false);
|
||||||
|
|
||||||
|
const getExpirationDisplay = () => {
|
||||||
|
const expDateTime = DateTime.fromISO(apiKey.expirationDate);
|
||||||
|
return `Expires on ${expDateTime.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApiKeyRevokeDialogOpen = () => {
|
||||||
|
setApiKeyRevokeOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card variant="outlined" className={classes.card}>
|
||||||
|
<CardContent className={classes.content}>
|
||||||
|
<Grid container alignItems="center" justifyContent="space-between">
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="body1" className={classes.label}>
|
||||||
|
{apiKey.label}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<Typography variant="body1" className={classes.expirationDate}>
|
||||||
|
{getExpirationDisplay()}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2} className={classes.revokeButton}>
|
||||||
|
<Button color="error" variant="contained" onClick={handleApiKeyRevokeDialogOpen}>
|
||||||
|
Revoke
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
{!isNil(apiKey.apiKey) && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Divider className={classes.keyCardDivider} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Stack direction="row" onClick={() => setOpenDropdown((prevOpenState) => !prevOpenState)}>
|
||||||
|
{!openDropdown ? (
|
||||||
|
<KeyboardArrowRight className={classes.dropdownText} />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowDown className={classes.dropdownText} />
|
||||||
|
)}
|
||||||
|
<Typography className={classes.dropdownButton}>KEY</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Collapse in={openDropdown} timeout="auto" unmountOnExit sx={{ marginTop: '1rem' }}>
|
||||||
|
<Stack direction="column" spacing="1.2rem">
|
||||||
|
<Typography variant="body1" align="left" className={classes.dropdownContentBox}>
|
||||||
|
{apiKey.apiKey}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
<ApiKeyRevokeDialog
|
||||||
|
open={apiKeyRevokeOpen}
|
||||||
|
setOpen={setApiKeyRevokeOpen}
|
||||||
|
apiKey={apiKey}
|
||||||
|
onConfirm={onRevoke}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiKeyCard;
|
57
src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
Normal file
57
src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography, Grid } from '@mui/material';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
gridWrapper: {
|
||||||
|
paddingTop: '2rem',
|
||||||
|
paddingBottom: '2rem'
|
||||||
|
},
|
||||||
|
apiKeyDisplay: {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
color: '#52637A',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: '400',
|
||||||
|
padding: '0.75rem',
|
||||||
|
backgroundColor: '#F7F7F7',
|
||||||
|
borderRadius: '0.9rem',
|
||||||
|
overflowWrap: 'break-word'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function ApiKeyConfirmDialog(props) {
|
||||||
|
const { open, setOpen, apiKey } = props;
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={handleClose}>
|
||||||
|
<DialogTitle>Api Key "{apiKey?.label}" Created</DialogTitle>
|
||||||
|
<DialogContent className={classes.apiKeyForm}>
|
||||||
|
<Grid container className={classes.gridWrapper}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography>Please copy the api key, you will not be able to see it once the page is refreshed</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="body1" align="center" className={classes.apiKeyDisplay}>
|
||||||
|
{apiKey?.apiKey}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="outlined" onClick={handleClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiKeyConfirmDialog;
|
172
src/components/User/ApiKeys/ApiKeyDialog.jsx
Normal file
172
src/components/User/ApiKeys/ApiKeyDialog.jsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { isNil, isNumber } from 'lodash';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { api, endpoints } from 'api';
|
||||||
|
import { host } from 'host';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
TextField,
|
||||||
|
DialogTitle,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Typography,
|
||||||
|
Grid
|
||||||
|
} from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
gridWrapper: {
|
||||||
|
paddingTop: '2rem',
|
||||||
|
paddingBottom: '2rem'
|
||||||
|
},
|
||||||
|
apiKeyLabel: {
|
||||||
|
paddingBottom: '1rem'
|
||||||
|
},
|
||||||
|
expirationDateContainer: {
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
expirationDateInput: {
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
expirationDateDisplay: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function ApiKeyDialog(props) {
|
||||||
|
const { open, setOpen, onConfirm } = props;
|
||||||
|
|
||||||
|
const [apiKeyLabel, setApiKeyLabel] = useState();
|
||||||
|
const [expirationDateOffset, setExpirationDateOffset] = useState(30);
|
||||||
|
const [selectedExpirationDate, setSelectedExpirationDate] = useState();
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
api
|
||||||
|
.post(`${host()}${endpoints.apiKeys}`, {
|
||||||
|
label: apiKeyLabel,
|
||||||
|
expirationDate: getExpirationDatetime().toISO()
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data) {
|
||||||
|
onConfirm(response.data);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelChange = (e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
setApiKeyLabel(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpirationDateChange = (e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
setExpirationDateOffset(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDatePickerChange = (newValue) => {
|
||||||
|
setSelectedExpirationDate(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExpirationDatetime = () => {
|
||||||
|
if (isNumber(expirationDateOffset)) {
|
||||||
|
return DateTime.now().plus({ days: expirationDateOffset }).endOf('day');
|
||||||
|
} else if (expirationDateOffset === 'custom') {
|
||||||
|
return DateTime.fromISO(selectedExpirationDate);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExpirationDisplay = () => {
|
||||||
|
const expDateTime = getExpirationDatetime();
|
||||||
|
return `Expires on ${expDateTime.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={handleClose}>
|
||||||
|
<DialogTitle>Create Api Key</DialogTitle>
|
||||||
|
<DialogContent className={classes.apiKeyForm}>
|
||||||
|
<Grid container className={classes.gridWrapper}>
|
||||||
|
<Grid item container className={classes.apiKeyLabel} xs={12}>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
id="apikeylabel"
|
||||||
|
label="Label"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleLabelChange}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid container item xs={12}>
|
||||||
|
<Grid item xs={5}>
|
||||||
|
<FormControl className={classes.expirationDateContainer} size="small" required>
|
||||||
|
<InputLabel disableAnimation>Expiration date</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="expirationDate"
|
||||||
|
id="expirationDate"
|
||||||
|
label="Expiration time"
|
||||||
|
onChange={handleExpirationDateChange}
|
||||||
|
value={expirationDateOffset}
|
||||||
|
className={classes.expirationDateInput}
|
||||||
|
>
|
||||||
|
<MenuItem value={7}>7 days</MenuItem>
|
||||||
|
<MenuItem value={30}>30 days</MenuItem>
|
||||||
|
<MenuItem value={60}>60 days</MenuItem>
|
||||||
|
<MenuItem value={90}>90 days</MenuItem>
|
||||||
|
<MenuItem value="custom">custom</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid item className={classes.expirationDateDisplay} xs={7}>
|
||||||
|
{expirationDateOffset === 'custom' ? (
|
||||||
|
<DatePicker
|
||||||
|
valueType="date"
|
||||||
|
slotProps={{ textField: { size: 'small' } }}
|
||||||
|
onChange={handleDatePickerChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography>{getExpirationDisplay()}</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={expirationDateOffset === 'custom' && isNil(selectedExpirationDate)}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" onClick={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiKeyDialog;
|
71
src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx
Normal file
71
src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { api, endpoints } from 'api';
|
||||||
|
import { host } from 'host';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography, Grid } from '@mui/material';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
gridWrapper: {
|
||||||
|
paddingTop: '2rem',
|
||||||
|
paddingBottom: '2rem'
|
||||||
|
},
|
||||||
|
apiKeyDisplay: {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
color: '#52637A',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: '400',
|
||||||
|
padding: '0.75rem',
|
||||||
|
backgroundColor: '#F7F7F7',
|
||||||
|
borderRadius: '0.9rem',
|
||||||
|
overflowWrap: 'break-word'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function ApiKeyRevokeDialog(props) {
|
||||||
|
const { open, setOpen, apiKey, onConfirm } = props;
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
api
|
||||||
|
.delete(`${host()}${endpoints.apiKeys}`, { id: apiKey.uuid })
|
||||||
|
.then((response) => {
|
||||||
|
onConfirm(response?.status, apiKey);
|
||||||
|
setOpen(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={handleClose}>
|
||||||
|
<DialogTitle>Revoke "{apiKey?.label}" key</DialogTitle>
|
||||||
|
<DialogContent className={classes.apiKeyForm}>
|
||||||
|
<Grid container className={classes.gridWrapper}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography>Are you sure you want to revoke this api key?</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" color="error" onClick={handleSubmit}>
|
||||||
|
Revoke
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" onClick={handleClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiKeyRevokeDialog;
|
146
src/components/User/ApiKeys/ApiKeys.jsx
Normal file
146
src/components/User/ApiKeys/ApiKeys.jsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { isEmpty, isNil } from 'lodash';
|
||||||
|
import { api, endpoints } from 'api';
|
||||||
|
import { host } from '../../../host';
|
||||||
|
|
||||||
|
import { Grid, Stack, Card, CardContent, Typography, Button } from '@mui/material';
|
||||||
|
import Loading from '../../Shared/Loading';
|
||||||
|
import ApiKeyDialog from './ApiKeyDialog';
|
||||||
|
import ApiKeyConfirmDialog from './ApiKeyConfirmDialog';
|
||||||
|
import ApiKeyCard from './ApiKeyCard';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
pageWrapper: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
padding: '0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cardRoot: {
|
||||||
|
boxShadow: 'none!important'
|
||||||
|
},
|
||||||
|
pageTitle: {
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
color: theme.palette.secondary.main,
|
||||||
|
textAlign: 'left'
|
||||||
|
},
|
||||||
|
apikeysContainer: {
|
||||||
|
marginTop: '1.5rem',
|
||||||
|
height: '100%',
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
padding: '0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apikeysContent: {
|
||||||
|
padding: '1.5rem'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function ApiKeys() {
|
||||||
|
const abortController = useMemo(() => new AbortController(), []);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [apiKeys, setApiKeys] = useState([]);
|
||||||
|
const [newApiKey, setNewApiKey] = useState();
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
// ApiKey dialog props
|
||||||
|
const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false);
|
||||||
|
const [apiKeyConfirmationOpen, setApiKeyConfirmationOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
api
|
||||||
|
.get(`${host()}${endpoints.apiKeys}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data && response.data.apiKeys) {
|
||||||
|
setApiKeys(response.data.apiKeys);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isNil(newApiKey) && !apiKeyConfirmationOpen) {
|
||||||
|
setApiKeyConfirmationOpen(true);
|
||||||
|
}
|
||||||
|
}, [newApiKey]);
|
||||||
|
|
||||||
|
const handleApiKeyDialogOpen = () => {
|
||||||
|
setApiKeyDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApiKeyCreateConfirm = (apiKey) => {
|
||||||
|
setNewApiKey(apiKey);
|
||||||
|
setApiKeys((prevState) => [...prevState, apiKey]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApiKeyRevokeConfirm = (status, apiKey) => {
|
||||||
|
if (status === 200) setApiKeys((prevState) => prevState.filter((ak) => ak.uuid != apiKey.uuid));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderApiKeys = () => {
|
||||||
|
return apiKeys.map((apiKey) => (
|
||||||
|
<ApiKeyCard key={apiKey.uuid} apiKey={apiKey} onRevoke={handleApiKeyRevokeConfirm} />
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
|
<Grid container className={classes.pageWrapper}>
|
||||||
|
<Grid item xs={12} md={12}>
|
||||||
|
<Card className={classes.cardRoot}>
|
||||||
|
<CardContent>
|
||||||
|
<Grid container className={classes.header}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="h4" className={classes.pageTitle}>
|
||||||
|
Manage your API Keys
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" color="success" onClick={handleApiKeyDialogOpen}>
|
||||||
|
Create new API key
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
{!isLoading && !isEmpty(apiKeys) && (
|
||||||
|
<Grid item xs={12} className={classes.apikeysContainer}>
|
||||||
|
<Card className={classes.cardRoot}>
|
||||||
|
<CardContent className={classes.apikeysContent}>
|
||||||
|
<Stack direction="column" spacing={1}>
|
||||||
|
{renderApiKeys()}
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<ApiKeyDialog open={apiKeyDialogOpen} setOpen={setApiKeyDialogOpen} onConfirm={handleApiKeyCreateConfirm} />
|
||||||
|
{!isNil(newApiKey) && (
|
||||||
|
<ApiKeyConfirmDialog open={apiKeyConfirmationOpen} setOpen={setApiKeyConfirmationOpen} apiKey={newApiKey} />
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiKeys;
|
@ -5,6 +5,8 @@ import App from './App';
|
|||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
import { createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from '@mui/material/styles';
|
import { createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from '@mui/material/styles';
|
||||||
|
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
|
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
|
||||||
|
|
||||||
const theme = createTheme(
|
const theme = createTheme(
|
||||||
adaptV4Theme({
|
adaptV4Theme({
|
||||||
@ -36,7 +38,9 @@ ReactDOM.render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<StyledEngineProvider injectFirst>
|
<StyledEngineProvider injectFirst>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<App />
|
<LocalizationProvider dateAdapter={AdapterLuxon}>
|
||||||
|
<App />
|
||||||
|
</LocalizationProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StyledEngineProvider>
|
</StyledEngineProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
@ -14,7 +14,6 @@ const useStyles = makeStyles(() => ({
|
|||||||
minWidth: '60%'
|
minWidth: '60%'
|
||||||
},
|
},
|
||||||
gridWrapper: {
|
gridWrapper: {
|
||||||
// backgroundColor: "#fff",
|
|
||||||
border: '0.0625em #f2f2f2 dashed'
|
border: '0.0625em #f2f2f2 dashed'
|
||||||
},
|
},
|
||||||
pageWrapper: {
|
pageWrapper: {
|
||||||
|
57
src/pages/UserManagementPage.jsx
Normal file
57
src/pages/UserManagementPage.jsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
import { getLoggedInUser } from 'utilities/authUtilities.js';
|
||||||
|
|
||||||
|
import { Container, Grid, Stack } from '@mui/material';
|
||||||
|
|
||||||
|
import Header from '../components/Header/Header.jsx';
|
||||||
|
import ApiKeys from '../components/User/ApiKeys/ApiKeys.jsx';
|
||||||
|
|
||||||
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
container: {
|
||||||
|
paddingTop: 30,
|
||||||
|
paddingBottom: 5,
|
||||||
|
height: '100%',
|
||||||
|
minWidth: '60%'
|
||||||
|
},
|
||||||
|
gridWrapper: {
|
||||||
|
border: '0.0625rem #f2f2f2 dashed'
|
||||||
|
},
|
||||||
|
pageWrapper: {
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
tile: {
|
||||||
|
width: '100%',
|
||||||
|
padding: 5
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function UserManagementPage() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmpty(getLoggedInUser())) {
|
||||||
|
navigate('/home');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className={classes.pageWrapper} direction="column" data-testid="explore-container">
|
||||||
|
<Header />
|
||||||
|
<Container className={classes.container}>
|
||||||
|
<Grid container className={classes.gridWrapper}>
|
||||||
|
<Grid item className={classes.tile}>
|
||||||
|
<ApiKeys />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserManagementPage;
|
@ -41,10 +41,15 @@ const isAuthenticationEnabled = () => {
|
|||||||
return Object.keys(authMethods).length > 0;
|
return Object.keys(authMethods).length > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isApiKeyEnabled = () => {
|
||||||
|
const authConfig = JSON.parse(localStorage.getItem('authConfig')) || {};
|
||||||
|
return authConfig?.apikey;
|
||||||
|
};
|
||||||
|
|
||||||
const getLoggedInUser = () => {
|
const getLoggedInUser = () => {
|
||||||
const userCookie = getCookie('user');
|
const userCookie = getCookie('user');
|
||||||
if (!userCookie) return null;
|
if (!userCookie) return null;
|
||||||
return userCookie;
|
return userCookie;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { isAuthenticated, isAuthenticationEnabled, getLoggedInUser, logoutUser };
|
export { isAuthenticated, isAuthenticationEnabled, isApiKeyEnabled, getLoggedInUser, logoutUser };
|
||||||
|
@ -96,7 +96,14 @@ const mapCVEInfo = (cveInfo) => {
|
|||||||
id: cve.Id,
|
id: cve.Id,
|
||||||
severity: cve.Severity,
|
severity: cve.Severity,
|
||||||
title: cve.Title,
|
title: cve.Title,
|
||||||
description: cve.Description
|
description: cve.Description,
|
||||||
|
reference: cve.Reference,
|
||||||
|
packageList: cve.PackageList?.map((pkg) => ({
|
||||||
|
packageName: pkg.Name,
|
||||||
|
packagePath: pkg.PackagePath,
|
||||||
|
packageInstalledVersion: pkg.InstalledVersion,
|
||||||
|
packageFixedVersion: pkg.FixedVersion
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return cveList;
|
return cveList;
|
||||||
@ -112,6 +119,7 @@ const mapAllCVEInfo = (cveInfo) => {
|
|||||||
description: cve.Description,
|
description: cve.Description,
|
||||||
reference: cve.Reference,
|
reference: cve.Reference,
|
||||||
packageName: packageInfo.Name,
|
packageName: packageInfo.Name,
|
||||||
|
packagePath: packageInfo.PackagePath,
|
||||||
packageInstalledVersion: packageInfo.InstalledVersion,
|
packageInstalledVersion: packageInfo.InstalledVersion,
|
||||||
packageFixedVersion: packageInfo.FixedVersion
|
packageFixedVersion: packageInfo.FixedVersion
|
||||||
};
|
};
|
||||||
|
@ -37,7 +37,8 @@ test.describe('Tag page test', () => {
|
|||||||
await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`);
|
await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`);
|
||||||
await page.getByRole('tab', { name: 'Vulnerabilities' }).click();
|
await page.getByRole('tab', { name: 'Vulnerabilities' }).click();
|
||||||
await expect(page.getByTestId('vulnerability-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
await expect(page.getByTestId('vulnerability-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
||||||
await expect(await page.getByText('CVE-').count()).toBeGreaterThan(0);
|
await expect(page.getByText(/CVE-/).nth(0)).toBeVisible({ timeout: 100000 });
|
||||||
await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
|
await expect(await page.getByText(/CVE-/).count()).toBeGreaterThan(0);
|
||||||
|
await expect(await page.getByText(/CVE-/).count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user