Compare commits
28 Commits
commit-087
...
commit-177
Author | SHA1 | Date | |
---|---|---|---|
177406df41 | |||
e2367c2a33 | |||
33524ce3cc | |||
e037c6c577 | |||
c268991495 | |||
0edfe0f73a | |||
f4a6030d93 | |||
9358539e0c | |||
5bf7d5652c | |||
12f9229320 | |||
df19fa811c | |||
6cda89c710 | |||
12b474e126 | |||
a9db66bd34 | |||
f4600b8b79 | |||
c375c0697a | |||
2e1e2e92b7 | |||
d9370fb9c1 | |||
e97e04eee5 | |||
a288523a3f | |||
fad5572db4 | |||
19e366ee1f | |||
b41fb2f841 | |||
b787273b84 | |||
9ecd46e4d0 | |||
845726cd08 | |||
ac84c375c0 | |||
96008d67be |
8
.github/workflows/dco.yml
vendored
8
.github/workflows/dco.yml
vendored
@ -2,16 +2,18 @@
|
|||||||
name: DCO
|
name: DCO
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.x
|
- name: Set up Python 3.x
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: '3.x'
|
||||||
- name: Check DCO
|
- name: Check DCO
|
||||||
|
17
.github/workflows/end-to-end-test.yml
vendored
17
.github/workflows/end-to-end-test.yml
vendored
@ -23,6 +23,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Cleanup disk space
|
||||||
|
run: |
|
||||||
|
# To free up ~15 GB of disk space
|
||||||
|
sudo rm -rf /opt/ghc
|
||||||
|
sudo rm -rf /usr/local/share/boost
|
||||||
|
sudo rm -rf /usr/local/lib/android
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
|
||||||
- name: Checkout zui repository
|
- name: Checkout zui repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
@ -73,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
|
||||||
@ -86,7 +94,7 @@ jobs:
|
|||||||
- name: Build zot
|
- name: Build zot
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE/zot
|
cd $GITHUB_WORKSPACE/zot
|
||||||
make binary
|
make binary ZUI_BUILD_PATH=$GITHUB_WORKSPACE/build
|
||||||
ls -l bin/
|
ls -l bin/
|
||||||
|
|
||||||
- name: Bringup zot server
|
- name: Bringup zot server
|
||||||
@ -116,6 +124,11 @@ jobs:
|
|||||||
cd $GITHUB_WORKSPACE
|
cd $GITHUB_WORKSPACE
|
||||||
make playwright-browsers
|
make playwright-browsers
|
||||||
|
|
||||||
|
- name: Trigger CVE scanning
|
||||||
|
run: |
|
||||||
|
# trigger CVE scanning for all images before running the tests
|
||||||
|
curl -X POST -H "Content-Type: application/json" -m 600 --data '{ "query": "{ ImageListForCVE (id:\"CVE-2021-43616\") { Results { RepoName Tag } } }" }' http://$REGISTRY_HOST:$REGISTRY_PORT/v2/_zot/ext/search
|
||||||
|
|
||||||
- name: Run integration tests
|
- name: Run integration tests
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE
|
cd $GITHUB_WORKSPACE
|
||||||
|
313
package-lock.json
generated
313
package-lock.json
generated
@ -14,19 +14,22 @@
|
|||||||
"@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",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"downshift": "^6.1.12",
|
"downshift": "^6.1.12",
|
||||||
|
"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",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"react-sticky-el": "^2.0.9",
|
"react-sticky-el": "^2.0.9",
|
||||||
"web-vitals": "^2.1.3"
|
"web-vitals": "^2.1.3",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
||||||
@ -2128,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",
|
||||||
@ -2676,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",
|
||||||
@ -3933,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": {
|
||||||
@ -3946,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": {
|
||||||
@ -4842,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",
|
||||||
@ -4882,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": "*"
|
||||||
}
|
}
|
||||||
@ -5592,6 +5735,14 @@
|
|||||||
"node": ">=8.9"
|
"node": ">=8.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@ -6547,6 +6698,18 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@ -6780,6 +6943,14 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/collect-v8-coverage": {
|
"node_modules/collect-v8-coverage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||||
@ -7030,6 +7201,17 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
@ -8926,6 +9108,11 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/export-from-json": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/export-from-json/-/export-from-json-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-Xg0L0saYz+CBz2MnaZvSEAHr17hWtHAfFWXw/frllG9t6aijuQukiU40ElOeM9nDTrtQPhLJMLN0q8lo897FYg=="
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.18.2",
|
"version": "4.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||||
@ -9423,6 +9610,14 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||||
@ -13196,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"
|
||||||
}
|
}
|
||||||
@ -16148,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",
|
||||||
@ -16997,6 +17193,17 @@
|
|||||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable": {
|
"node_modules/stable": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||||
@ -18746,6 +18953,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||||
@ -19133,6 +19356,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
||||||
|
11
package.json
11
package.json
@ -9,29 +9,32 @@
|
|||||||
"@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",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"downshift": "^6.1.12",
|
"downshift": "^6.1.12",
|
||||||
|
"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",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"react-sticky-el": "^2.0.9",
|
"react-sticky-el": "^2.0.9",
|
||||||
"web-vitals": "^2.1.3"
|
"web-vitals": "^2.1.3",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
||||||
"@playwright/test": "^1.28.1",
|
"@playwright/test": "^1.28.1",
|
||||||
"eslint": "^8.23.1",
|
"eslint": "^8.23.1",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.31.8",
|
"eslint-plugin-react": "^7.31.8",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1"
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
@ -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="/" />}>
|
||||||
|
@ -34,6 +34,7 @@ const mockImageList = {
|
|||||||
Size: '2806985',
|
Size: '2806985',
|
||||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||||
IsBookmarked: false,
|
IsBookmarked: false,
|
||||||
|
IsStarred: false,
|
||||||
NewestImage: {
|
NewestImage: {
|
||||||
Tag: 'latest',
|
Tag: 'latest',
|
||||||
Description: 'w',
|
Description: 'w',
|
||||||
@ -58,6 +59,7 @@ const mockImageList = {
|
|||||||
Size: '231383863',
|
Size: '231383863',
|
||||||
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||||
IsBookmarked: false,
|
IsBookmarked: false,
|
||||||
|
IsStarred: false,
|
||||||
NewestImage: {
|
NewestImage: {
|
||||||
Tag: 'latest',
|
Tag: 'latest',
|
||||||
Description: '',
|
Description: '',
|
||||||
@ -82,6 +84,7 @@ const mockImageList = {
|
|||||||
Size: '369311301',
|
Size: '369311301',
|
||||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||||
IsBookmarked: false,
|
IsBookmarked: false,
|
||||||
|
IsStarred: false,
|
||||||
NewestImage: {
|
NewestImage: {
|
||||||
Tag: 'latest',
|
Tag: 'latest',
|
||||||
Description: '',
|
Description: '',
|
||||||
@ -106,6 +109,7 @@ const mockImageList = {
|
|||||||
Size: '369311301',
|
Size: '369311301',
|
||||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||||
IsBookmarked: false,
|
IsBookmarked: false,
|
||||||
|
IsStarred: false,
|
||||||
NewestImage: {
|
NewestImage: {
|
||||||
Tag: 'latest',
|
Tag: 'latest',
|
||||||
Description: '',
|
Description: '',
|
||||||
@ -130,6 +134,7 @@ const mockImageList = {
|
|||||||
Size: '369311301',
|
Size: '369311301',
|
||||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||||
IsBookmarked: false,
|
IsBookmarked: false,
|
||||||
|
IsStarred: false,
|
||||||
NewestImage: {
|
NewestImage: {
|
||||||
Tag: 'latest',
|
Tag: 'latest',
|
||||||
Description: '',
|
Description: '',
|
||||||
@ -158,6 +163,7 @@ const mockImageList = {
|
|||||||
Size: '369311301',
|
Size: '369311301',
|
||||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||||
IsBookmarked: false,
|
IsBookmarked: false,
|
||||||
|
IsStarred: false,
|
||||||
NewestImage: {
|
NewestImage: {
|
||||||
Tag: 'latest',
|
Tag: 'latest',
|
||||||
Description: '',
|
Description: '',
|
||||||
@ -182,6 +188,7 @@ const mockImageList = {
|
|||||||
Size: '369311301',
|
Size: '369311301',
|
||||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||||
IsBookmarked: false,
|
IsBookmarked: false,
|
||||||
|
IsStarred: false,
|
||||||
NewestImage: {
|
NewestImage: {
|
||||||
Tag: 'latest',
|
Tag: 'latest',
|
||||||
Description: '',
|
Description: '',
|
||||||
@ -338,4 +345,13 @@ describe('Explore component', () => {
|
|||||||
await userEvent.click(bookmarkButton);
|
await userEvent.click(bookmarkButton);
|
||||||
expect(await screen.findAllByTestId('bookmarked')).toHaveLength(1);
|
expect(await screen.findAllByTestId('bookmarked')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should star a repo if star button is clicked', async () => {
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||||
|
render(<StateExploreWrapper />);
|
||||||
|
const starButton = (await screen.findAllByTestId('star-button'))[0];
|
||||||
|
jest.spyOn(api, 'put').mockResolvedValueOnce({ status: 200, data: {} });
|
||||||
|
await userEvent.click(starButton);
|
||||||
|
expect(await screen.findAllByTestId('starred')).toHaveLength(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -21,7 +21,7 @@ const StateFilterCardWrapper = () => {
|
|||||||
describe('Filters components', () => {
|
describe('Filters components', () => {
|
||||||
it('renders the filters cards', async () => {
|
it('renders the filters cards', async () => {
|
||||||
render(<StateFilterCardWrapper />);
|
render(<StateFilterCardWrapper />);
|
||||||
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
|
expect(screen.getAllByRole('checkbox')).toHaveLength(3);
|
||||||
|
|
||||||
const checkbox = screen.getAllByRole('checkbox');
|
const checkbox = screen.getAllByRole('checkbox');
|
||||||
expect(checkbox[0]).not.toBeChecked();
|
expect(checkbox[0]).not.toBeChecked();
|
||||||
|
@ -164,6 +164,48 @@ const mockImageListBookmarks = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockImageListStars = {
|
||||||
|
GlobalSearch: {
|
||||||
|
Page: { TotalCount: 3, ItemCount: 2 },
|
||||||
|
Repos: [
|
||||||
|
{
|
||||||
|
Name: 'alpine',
|
||||||
|
Size: '2806985',
|
||||||
|
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||||
|
NewestImage: {
|
||||||
|
Tag: 'latest',
|
||||||
|
Description: 'w',
|
||||||
|
IsSigned: false,
|
||||||
|
Licenses: '',
|
||||||
|
Vendor: '',
|
||||||
|
Labels: '',
|
||||||
|
Vulnerabilities: {
|
||||||
|
MaxSeverity: 'LOW',
|
||||||
|
Count: 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'mongo',
|
||||||
|
Size: '231383863',
|
||||||
|
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||||
|
NewestImage: {
|
||||||
|
Tag: 'latest',
|
||||||
|
Description: '',
|
||||||
|
IsSigned: true,
|
||||||
|
Licenses: '',
|
||||||
|
Vendor: '',
|
||||||
|
Labels: '',
|
||||||
|
Vulnerabilities: {
|
||||||
|
MaxSeverity: 'HIGH',
|
||||||
|
Count: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
window.scrollTo = jest.fn();
|
window.scrollTo = jest.fn();
|
||||||
});
|
});
|
||||||
@ -178,8 +220,8 @@ describe('Home component', () => {
|
|||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
||||||
render(<HomeWrapper />);
|
render(<HomeWrapper />);
|
||||||
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(3));
|
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(4));
|
||||||
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(3));
|
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(4));
|
||||||
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
|
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -187,16 +229,16 @@ describe('Home component', () => {
|
|||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
||||||
render(<HomeWrapper />);
|
render(<HomeWrapper />);
|
||||||
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(3);
|
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(4);
|
||||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(4);
|
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders vulnerability icons', async () => {
|
it('renders vulnerability icons', async () => {
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
||||||
render(<HomeWrapper />);
|
render(<HomeWrapper />);
|
||||||
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(3);
|
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(4);
|
||||||
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(3);
|
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(4);
|
||||||
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
|
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,16 +246,17 @@ describe('Home component', () => {
|
|||||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
render(<HomeWrapper />);
|
render(<HomeWrapper />);
|
||||||
await waitFor(() => expect(error).toBeCalledTimes(3));
|
await waitFor(() => expect(error).toBeCalledTimes(4));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to explore page when clicking view all popular', async () => {
|
it('should redirect to explore page when clicking view all popular', async () => {
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListBookmarks } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListBookmarks } });
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListStars } });
|
||||||
render(<HomeWrapper />);
|
render(<HomeWrapper />);
|
||||||
const viewAllButtons = await screen.findAllByText(/view all/i);
|
const viewAllButtons = await screen.findAllByText(/view all/i);
|
||||||
expect(viewAllButtons).toHaveLength(3);
|
expect(viewAllButtons).toHaveLength(4);
|
||||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: [] } });
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: [] } });
|
||||||
fireEvent.click(viewAllButtons[0]);
|
fireEvent.click(viewAllButtons[0]);
|
||||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||||
@ -230,5 +273,10 @@ describe('Home component', () => {
|
|||||||
pathname: `/explore`,
|
pathname: `/explore`,
|
||||||
search: createSearchParams({ filter: 'IsBookmarked' }).toString()
|
search: createSearchParams({ filter: 'IsBookmarked' }).toString()
|
||||||
});
|
});
|
||||||
|
fireEvent.click(viewAllButtons[3]);
|
||||||
|
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||||
|
pathname: `/explore`,
|
||||||
|
search: createSearchParams({ filter: 'IsStarred' }).toString()
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 () => {
|
||||||
|
@ -47,6 +47,7 @@ const mockRepoDetailsData = {
|
|||||||
Size: '451554070',
|
Size: '451554070',
|
||||||
Vendors: ['[The Node.js Docker Team](https://github.com/nodejs/docker-node)\n'],
|
Vendors: ['[The Node.js Docker Team](https://github.com/nodejs/docker-node)\n'],
|
||||||
IsBookmarked: false,
|
IsBookmarked: false,
|
||||||
|
IsStarred: false,
|
||||||
NewestImage: {
|
NewestImage: {
|
||||||
RepoName: 'mongo',
|
RepoName: 'mongo',
|
||||||
IsSigned: true,
|
IsSigned: true,
|
||||||
@ -316,4 +317,13 @@ describe('Repo details component', () => {
|
|||||||
await userEvent.click(bookmarkButton);
|
await userEvent.click(bookmarkButton);
|
||||||
expect(await screen.findByTestId('bookmarked')).toBeInTheDocument();
|
expect(await screen.findByTestId('bookmarked')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should star a repo if star button is clicked', async () => {
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockRepoDetailsData } });
|
||||||
|
render(<RepoDetailsThemeWrapper />);
|
||||||
|
const starButton = await screen.findByTestId('star-button');
|
||||||
|
jest.spyOn(api, 'put').mockResolvedValue({ status: 200, data: {} });
|
||||||
|
await userEvent.click(starButton);
|
||||||
|
expect(await screen.findByTestId('starred')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -22,6 +22,7 @@ const mockedTagsData = [
|
|||||||
{
|
{
|
||||||
tag: 'latest',
|
tag: 'latest',
|
||||||
vendor: 'test1',
|
vendor: 'test1',
|
||||||
|
isDeletable: true,
|
||||||
manifests: [
|
manifests: [
|
||||||
{
|
{
|
||||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||||
@ -37,6 +38,7 @@ const mockedTagsData = [
|
|||||||
{
|
{
|
||||||
tag: 'bullseye',
|
tag: 'bullseye',
|
||||||
vendor: 'test1',
|
vendor: 'test1',
|
||||||
|
isDeletable: true,
|
||||||
manifests: [
|
manifests: [
|
||||||
{
|
{
|
||||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||||
@ -52,6 +54,7 @@ const mockedTagsData = [
|
|||||||
{
|
{
|
||||||
tag: '1.5.2',
|
tag: '1.5.2',
|
||||||
vendor: 'test1',
|
vendor: 'test1',
|
||||||
|
isDeletable: true,
|
||||||
manifests: [
|
manifests: [
|
||||||
{
|
{
|
||||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||||
@ -76,6 +79,18 @@ describe('Tags component', () => {
|
|||||||
await waitFor(() => expect(screen.queryByText(/OS\/ARCH/i)).not.toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByText(/OS\/ARCH/i)).not.toBeInTheDocument());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should see delete tag button and its dialog', async () => {
|
||||||
|
render(<TagsThemeWrapper />);
|
||||||
|
const deleteBtn = await screen.findAllByTestId('DeleteIcon');
|
||||||
|
fireEvent.click(deleteBtn[0]);
|
||||||
|
expect(await screen.findByTestId('delete-dialog')).toBeInTheDocument();
|
||||||
|
const confirmBtn = await screen.findByTestId('confirm-delete');
|
||||||
|
expect(confirmBtn).toBeInTheDocument();
|
||||||
|
fireEvent.click(confirmBtn);
|
||||||
|
expect(await screen.findByTestId('confirm-delete')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByTestId('cancel-delete')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should navigate to tag page details when tag is clicked', async () => {
|
it('should navigate to tag page details when tag is clicked', async () => {
|
||||||
render(<TagsThemeWrapper />);
|
render(<TagsThemeWrapper />);
|
||||||
const tagLink = await screen.findByText('latest');
|
const tagLink = await screen.findByText('latest');
|
||||||
|
@ -22,6 +22,29 @@ const mockImage = {
|
|||||||
vendor: '',
|
vendor: '',
|
||||||
size: '585',
|
size: '585',
|
||||||
tags: '',
|
tags: '',
|
||||||
|
isSigned: true,
|
||||||
|
signatureInfo: [
|
||||||
|
{
|
||||||
|
Tool: 'cosign',
|
||||||
|
IsTrusted: false,
|
||||||
|
Author: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tool: 'cosign',
|
||||||
|
IsTrusted: false,
|
||||||
|
Author: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tool: 'cosign',
|
||||||
|
IsTrusted: false,
|
||||||
|
Author: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tool: 'cosign',
|
||||||
|
IsTrusted: false,
|
||||||
|
Author: ''
|
||||||
|
}
|
||||||
|
],
|
||||||
platforms: [{ Os: 'linux', Arch: 'amd64' }]
|
platforms: [{ Os: 'linux', Arch: 'amd64' }]
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,6 +57,8 @@ const RepoCardWrapper = (props) => {
|
|||||||
version={image.latestVersion}
|
version={image.latestVersion}
|
||||||
description={image.description}
|
description={image.description}
|
||||||
vendor={image.vendor}
|
vendor={image.vendor}
|
||||||
|
isSigned={image.isSigned}
|
||||||
|
signatureInfo={image.signatureInfo}
|
||||||
key={1}
|
key={1}
|
||||||
lastUpdated={image.lastUpdated}
|
lastUpdated={image.lastUpdated}
|
||||||
platforms={image.platforms}
|
platforms={image.platforms}
|
||||||
|
@ -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';
|
||||||
@ -6,6 +6,8 @@ import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
jest.mock('xlsx');
|
||||||
|
|
||||||
const StateVulnerabilitiesWrapper = () => {
|
const StateVulnerabilitiesWrapper = () => {
|
||||||
return (
|
return (
|
||||||
<MockThemeProvider>
|
<MockThemeProvider>
|
||||||
@ -16,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',
|
||||||
@ -29,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'
|
||||||
}
|
}
|
||||||
@ -44,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'
|
||||||
}
|
}
|
||||||
@ -78,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'
|
||||||
}
|
}
|
||||||
@ -92,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'
|
||||||
}
|
}
|
||||||
@ -106,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'
|
||||||
}
|
}
|
||||||
@ -120,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'
|
||||||
}
|
}
|
||||||
@ -134,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'
|
||||||
}
|
}
|
||||||
@ -148,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'
|
||||||
}
|
}
|
||||||
@ -167,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'
|
||||||
}
|
}
|
||||||
@ -181,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'
|
||||||
}
|
}
|
||||||
@ -195,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'
|
||||||
}
|
}
|
||||||
@ -229,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'
|
||||||
}
|
}
|
||||||
@ -243,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'
|
||||||
}
|
}
|
||||||
@ -277,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'
|
||||||
}
|
}
|
||||||
@ -291,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'
|
||||||
}
|
}
|
||||||
@ -345,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'
|
||||||
}
|
}
|
||||||
@ -363,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'
|
||||||
}
|
}
|
||||||
@ -378,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'
|
||||||
}
|
}
|
||||||
@ -412,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'
|
||||||
}
|
}
|
||||||
@ -427,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'
|
||||||
}
|
}
|
||||||
@ -440,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: {
|
||||||
@ -497,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 () => {
|
||||||
@ -549,15 +749,113 @@ 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 () => {
|
||||||
|
const xlsxMock = jest.createMockFromModule('xlsx');
|
||||||
|
xlsxMock.writeFile = jest.fn();
|
||||||
|
|
||||||
|
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
||||||
|
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();
|
||||||
|
const exportAsCSVBtn = screen.getByText(/csv/i);
|
||||||
|
expect(exportAsCSVBtn).toBeInTheDocument();
|
||||||
|
global.URL.createObjectURL = jest.fn();
|
||||||
|
await fireEvent.click(exportAsCSVBtn);
|
||||||
|
expect(await screen.findByTestId('export-csv-menuItem')).not.toBeInTheDocument();
|
||||||
|
fireEvent.click(downloadBtn[0]);
|
||||||
|
const exportAsExcelBtn = screen.getByText(/xlsx/i);
|
||||||
|
expect(exportAsExcelBtn).toBeInTheDocument();
|
||||||
|
await fireEvent.click(exportAsExcelBtn);
|
||||||
|
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')
|
||||||
@ -566,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));
|
||||||
});
|
});
|
||||||
|
45
src/api.js
45
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -79,25 +82,41 @@ const api = {
|
|||||||
const endpoints = {
|
const endpoints = {
|
||||||
status: `/v2/`,
|
status: `/v2/`,
|
||||||
authConfig: `/v2/_zot/ext/mgmt`,
|
authConfig: `/v2/_zot/ext/mgmt`,
|
||||||
openidAuth: `/auth/login`,
|
openidAuth: `/zot/auth/login`,
|
||||||
logout: `/auth/logout`,
|
logout: `/zot/auth/logout`,
|
||||||
|
apiKeys: '/zot/auth/apikey',
|
||||||
|
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:${
|
||||||
(pageNumber - 1) * pageSize
|
(pageNumber - 1) * pageSize
|
||||||
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned Documentation Vendor Labels} IsStarred IsBookmarked DownloadCount}}}`,
|
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned SignatureInfo { Tool IsTrusted Author } Documentation Vendor Labels} IsStarred IsBookmarked StarCount DownloadCount}}}`,
|
||||||
detailedRepoInfo: (name) =>
|
detailedRepoInfo: (name) =>
|
||||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned 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 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) =>
|
||||||
|
`/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) {
|
||||||
@ -113,11 +132,11 @@ const endpoints = {
|
|||||||
dependsOnForImage: (name, { pageNumber = 1, pageSize = 15 } = {}) =>
|
dependsOnForImage: (name, { pageNumber = 1, pageSize = 15 } = {}) =>
|
||||||
`/v2/_zot/ext/search?query={BaseImageList(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
`/v2/_zot/ext/search?query={BaseImageList(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||||
(pageNumber - 1) * pageSize
|
(pageNumber - 1) * pageSize
|
||||||
}}){Page {TotalCount ItemCount} Results { RepoName Tag Description Manifests {Digest Platform {Os Arch} Size} Vendor DownloadCount LastUpdated IsSigned Vulnerabilities {MaxSeverity Count}}}}`,
|
}}){Page {TotalCount ItemCount} Results { RepoName Tag Description Manifests {Digest Platform {Os Arch} Size} Vendor DownloadCount LastUpdated IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count}}}}`,
|
||||||
isDependentOnForImage: (name, { pageNumber = 1, pageSize = 15 } = {}) =>
|
isDependentOnForImage: (name, { pageNumber = 1, pageSize = 15 } = {}) =>
|
||||||
`/v2/_zot/ext/search?query={DerivedImageList(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
`/v2/_zot/ext/search?query={DerivedImageList(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||||
(pageNumber - 1) * pageSize
|
(pageNumber - 1) * pageSize
|
||||||
}}){Page {TotalCount ItemCount} Results {RepoName Tag Description Manifests {Digest Platform {Os Arch} Size} Vendor DownloadCount LastUpdated IsSigned Vulnerabilities {MaxSeverity Count}}}}`,
|
}}){Page {TotalCount ItemCount} Results {RepoName Tag Description Manifests {Digest Platform {Os Arch} Size} Vendor DownloadCount LastUpdated IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count}}}}`,
|
||||||
globalSearch: ({
|
globalSearch: ({
|
||||||
searchQuery = '""',
|
searchQuery = '""',
|
||||||
pageNumber = 1,
|
pageNumber = 1,
|
||||||
@ -134,9 +153,10 @@ const endpoints = {
|
|||||||
if (filter.Arch) filterParam += ` Arch:${!isEmpty(filter.Arch) ? `${JSON.stringify(filter.Arch)}` : '""'}`;
|
if (filter.Arch) filterParam += ` Arch:${!isEmpty(filter.Arch) ? `${JSON.stringify(filter.Arch)}` : '""'}`;
|
||||||
if (filter.HasToBeSigned) filterParam += ` HasToBeSigned: ${filter.HasToBeSigned}`;
|
if (filter.HasToBeSigned) filterParam += ` HasToBeSigned: ${filter.HasToBeSigned}`;
|
||||||
if (filter.IsBookmarked) filterParam += ` IsBookmarked: ${filter.IsBookmarked}`;
|
if (filter.IsBookmarked) filterParam += ` IsBookmarked: ${filter.IsBookmarked}`;
|
||||||
|
if (filter.IsStarred) filterParam += ` IsStarred: ${filter.IsStarred}`;
|
||||||
filterParam += '}';
|
filterParam += '}';
|
||||||
if (Object.keys(filter).length === 0) filterParam = '';
|
if (Object.keys(filter).length === 0) filterParam = '';
|
||||||
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam} ${filterParam}) {Page {TotalCount ItemCount} Repos {Name LastUpdated Size Platforms { Os Arch } IsStarred IsBookmarked NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description IsSigned Licenses Vendor Labels } DownloadCount}}}`;
|
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam} ${filterParam}) {Page {TotalCount ItemCount} Repos {Name LastUpdated Size Platforms { Os Arch } IsStarred IsBookmarked NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description IsSigned SignatureInfo { Tool IsTrusted Author } Licenses Vendor Labels } StarCount DownloadCount}}}`;
|
||||||
},
|
},
|
||||||
imageSuggestions: ({ searchQuery = '""', pageNumber = 1, pageSize = 15 }) => {
|
imageSuggestions: ({ searchQuery = '""', pageNumber = 1, pageSize = 15 }) => {
|
||||||
const searchParam = searchQuery !== '' ? `query:"${searchQuery}"` : `query:""`;
|
const searchParam = searchQuery !== '' ? `query:"${searchQuery}"` : `query:""`;
|
||||||
@ -145,7 +165,8 @@ const endpoints = {
|
|||||||
},
|
},
|
||||||
referrers: ({ repo, digest, type = '' }) =>
|
referrers: ({ repo, digest, type = '' }) =>
|
||||||
`/v2/_zot/ext/search?query={Referrers(repo: "${repo}" digest: "${digest}" type: "${type}"){MediaType ArtifactType Size Digest Annotations{Key Value}}}`,
|
`/v2/_zot/ext/search?query={Referrers(repo: "${repo}" digest: "${digest}" type: "${type}"){MediaType ArtifactType Size Digest Annotations{Key Value}}}`,
|
||||||
bookmarkToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleBookmark`
|
bookmarkToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleBookmark`,
|
||||||
|
starToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleStar`
|
||||||
};
|
};
|
||||||
|
|
||||||
export { api, endpoints };
|
export { api, endpoints };
|
||||||
|
15
src/assets/noData.svg
Normal file
15
src/assets/noData.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg width="235" height="240" viewBox="0 0 235 240" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="34.8711" y="16.2017" width="36" height="249.944" rx="18" transform="rotate(-26.7465 34.8711 16.2017)" fill="#F15527" fill-opacity="0.2"/>
|
||||||
|
<rect x="156.871" y="36.2017" width="36" height="100.235" rx="18" transform="rotate(-26.7465 156.871 36.2017)" fill="#F15527" fill-opacity="0.2"/>
|
||||||
|
<rect x="0.871094" y="138.037" width="26.745" height="74.4663" rx="13.3725" transform="rotate(-26.7465 0.871094 138.037)" fill="#F15527" fill-opacity="0.2"/>
|
||||||
|
<g clip-path="url(#clip0_2865_33046)">
|
||||||
|
<path d="M117.5 199C161.225 199 197 163.225 197 119.5C197 75.775 161.225 40 117.5 40C73.775 40 38 75.775 38 119.5C38 163.225 73.775 199 117.5 199ZM119.885 82.0555C121.475 80.6245 123.383 79.75 125.45 79.75C127.596 79.75 129.425 80.6245 131.094 82.0555C132.605 83.725 133.4 85.633 133.4 87.7C133.4 89.8465 132.605 91.675 131.094 93.3445C129.425 94.855 127.596 95.65 125.45 95.65C123.383 95.65 121.475 94.855 119.885 93.3445C118.375 91.675 117.5 89.8465 117.5 87.7C117.5 85.633 118.375 83.725 119.885 82.0555ZM100.01 119.261C100.01 119.261 117.261 105.588 123.542 105.031C129.425 104.554 128.232 111.311 127.676 114.809L127.597 115.286C126.484 119.5 125.132 124.588 123.78 129.438C120.759 140.488 117.818 151.3 118.534 153.287C119.328 155.991 124.257 152.572 127.835 150.187C128.312 149.869 128.709 149.551 129.107 149.312C129.107 149.312 129.743 148.676 130.379 149.551C130.538 149.789 130.697 150.028 130.856 150.187C131.572 151.3 131.969 151.697 131.015 152.333L130.697 152.492C128.948 153.685 121.475 158.932 118.454 160.84C115.194 162.987 102.713 170.142 104.621 156.229C106.291 146.451 108.517 138.023 110.266 131.425C113.525 119.5 114.956 114.094 107.642 118.785C104.701 120.534 102.951 121.646 101.918 122.362C101.043 122.998 100.964 122.998 100.408 121.965L100.169 121.487L99.7715 120.852C99.215 120.057 99.215 119.977 100.01 119.261Z" fill="#0F2139"/>
|
||||||
|
<path d="M119.885 82.0555C121.475 80.6245 123.383 79.75 125.45 79.75C127.596 79.75 129.425 80.6245 131.094 82.0555C132.605 83.725 133.4 85.633 133.4 87.7C133.4 89.8465 132.605 91.675 131.094 93.3445C129.425 94.855 127.596 95.65 125.45 95.65C123.383 95.65 121.475 94.855 119.885 93.3445C118.375 91.675 117.5 89.8465 117.5 87.7C117.5 85.633 118.375 83.725 119.885 82.0555Z" fill="white"/>
|
||||||
|
<path d="M100.01 119.261C100.01 119.261 117.261 105.588 123.542 105.031C129.425 104.554 128.232 111.311 127.676 114.809L127.597 115.286C126.484 119.5 125.132 124.588 123.78 129.438C120.759 140.488 117.818 151.3 118.534 153.287C119.328 155.991 124.257 152.572 127.835 150.187C128.312 149.869 128.709 149.551 129.107 149.312C129.107 149.312 129.743 148.676 130.379 149.551C130.538 149.789 130.697 150.028 130.856 150.187C131.572 151.3 131.969 151.697 131.015 152.333L130.697 152.492C128.948 153.685 121.475 158.932 118.454 160.84C115.194 162.987 102.713 170.142 104.621 156.229C106.291 146.451 108.517 138.023 110.266 131.425C113.525 119.5 114.956 114.094 107.642 118.785C104.701 120.534 102.951 121.646 101.918 122.362C101.043 122.998 100.964 122.998 100.408 121.965L100.169 121.487L99.7715 120.852C99.215 120.057 99.215 119.977 100.01 119.261Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2865_33046">
|
||||||
|
<rect width="159" height="159" fill="white" transform="translate(38 40)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 1.7 KiB |
@ -220,8 +220,11 @@ function Explore({ searchInputValue }) {
|
|||||||
version={item.latestVersion}
|
version={item.latestVersion}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
downloads={item.downloads}
|
downloads={item.downloads}
|
||||||
|
stars={item.stars}
|
||||||
isSigned={item.isSigned}
|
isSigned={item.isSigned}
|
||||||
|
signatureInfo={item.signatureInfo}
|
||||||
isBookmarked={item.isBookmarked}
|
isBookmarked={item.isBookmarked}
|
||||||
|
isStarred={item.isStarred}
|
||||||
vendor={item.vendor}
|
vendor={item.vendor}
|
||||||
platforms={item.platforms}
|
platforms={item.platforms}
|
||||||
key={index}
|
key={index}
|
||||||
|
@ -149,14 +149,14 @@ function Header({ setSearchCurrentValue = () => {} }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item className={classes.headerLinkContainer}>
|
<Grid item className={classes.headerLinkContainer}>
|
||||||
<a className={classes.link} href="https://zotregistry.io" target="_blank" rel="noreferrer">
|
<a className={classes.link} href="https://zotregistry.dev" target="_blank" rel="noreferrer">
|
||||||
Product
|
Product
|
||||||
</a>
|
</a>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item className={classes.headerLinkContainer}>
|
<Grid item className={classes.headerLinkContainer}>
|
||||||
<a
|
<a
|
||||||
className={classes.link}
|
className={classes.link}
|
||||||
href="https://zotregistry.io/v1.4.3/general/concepts/"
|
href="https://zotregistry.dev/v2.0.0/general/concepts/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
@ -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() });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -295,12 +301,26 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
|||||||
<List
|
<List
|
||||||
{...getMenuProps()}
|
{...getMenuProps()}
|
||||||
className={
|
className={
|
||||||
isOpen && !isLoading && !isFailedSearch
|
isOpen && !isFailedSearch
|
||||||
? `${classes.resultsWrapper} ${isComponentFocused && classes.resultsWrapperFocused}`
|
? `${classes.resultsWrapper} ${isComponentFocused && classes.resultsWrapperFocused}`
|
||||||
: classes.resultsWrapperHidden
|
: classes.resultsWrapperHidden
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isOpen && suggestionData?.length > 0 && renderSuggestions()}
|
{isOpen && suggestionData?.length > 0 && renderSuggestions()}
|
||||||
|
{isOpen && isLoading && !isEmpty(searchQuery) && isEmpty(suggestionData) && (
|
||||||
|
<>
|
||||||
|
<ListItem
|
||||||
|
className={classes.searchItem}
|
||||||
|
style={{ color: '#52637A', fontSize: '1rem', textOverflow: 'ellipsis' }}
|
||||||
|
{...getItemProps({ item: '', index: 0 })}
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<Typography>Loading...</Typography>
|
||||||
|
</Stack>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{isOpen && isEmpty(searchQuery) && isEmpty(suggestionData) && (
|
{isOpen && isEmpty(searchQuery) && isEmpty(suggestionData) && (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -8,8 +8,14 @@ import { mapToRepo } from 'utilities/objectModels';
|
|||||||
import Loading from '../Shared/Loading';
|
import Loading from '../Shared/Loading';
|
||||||
import { useNavigate, createSearchParams } from 'react-router-dom';
|
import { useNavigate, createSearchParams } from 'react-router-dom';
|
||||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||||
import { HOME_POPULAR_PAGE_SIZE, HOME_RECENT_PAGE_SIZE, HOME_BOOKMARKS_PAGE_SIZE } from 'utilities/paginationConstants';
|
import {
|
||||||
|
HOME_POPULAR_PAGE_SIZE,
|
||||||
|
HOME_RECENT_PAGE_SIZE,
|
||||||
|
HOME_BOOKMARKS_PAGE_SIZE,
|
||||||
|
HOME_STARS_PAGE_SIZE
|
||||||
|
} from 'utilities/paginationConstants';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
import NoDataComponent from 'components/Shared/NoDataComponent';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
gridWrapper: {
|
gridWrapper: {
|
||||||
@ -88,6 +94,8 @@ function Home() {
|
|||||||
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
|
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
|
||||||
const [bookmarkData, setBookmarkData] = useState([]);
|
const [bookmarkData, setBookmarkData] = useState([]);
|
||||||
const [isLoadingBookmarks, setIsLoadingBookmarks] = useState(true);
|
const [isLoadingBookmarks, setIsLoadingBookmarks] = useState(true);
|
||||||
|
const [starData, setStarData] = useState([]);
|
||||||
|
const [isLoadingStars, setIsLoadingStars] = useState(true);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const abortController = useMemo(() => new AbortController(), []);
|
const abortController = useMemo(() => new AbortController(), []);
|
||||||
@ -184,12 +192,44 @@ function Home() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStars = () => {
|
||||||
|
setIsLoadingStars(true);
|
||||||
|
api
|
||||||
|
.get(
|
||||||
|
`${host()}${endpoints.globalSearch({
|
||||||
|
searchQuery: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: HOME_STARS_PAGE_SIZE,
|
||||||
|
sortBy: sortByCriteria.relevance?.value,
|
||||||
|
filter: { IsStarred: true }
|
||||||
|
})}`,
|
||||||
|
abortController.signal
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
let repoList = response.data.data.GlobalSearch.Repos;
|
||||||
|
let repoData = repoList.map((responseRepo) => {
|
||||||
|
return mapToRepo(responseRepo);
|
||||||
|
});
|
||||||
|
setStarData(repoData);
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsLoadingStars(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsLoadingStars(false);
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
getPopularData();
|
getPopularData();
|
||||||
getRecentData();
|
getRecentData();
|
||||||
getBookmarks();
|
getBookmarks();
|
||||||
|
getStars();
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
@ -199,6 +239,17 @@ function Home() {
|
|||||||
navigate({ pathname: `/explore`, search: createSearchParams({ [type]: value }).toString() });
|
navigate({ pathname: `/explore`, search: createSearchParams({ [type]: value }).toString() });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isNoData = () =>
|
||||||
|
!isLoading &&
|
||||||
|
!isLoadingBookmarks &&
|
||||||
|
!isLoadingStars &&
|
||||||
|
!isLoadingPopular &&
|
||||||
|
!isLoadingRecent &&
|
||||||
|
bookmarkData.length === 0 &&
|
||||||
|
starData.length === 0 &&
|
||||||
|
popularData.length === 0 &&
|
||||||
|
recentData.length === 0;
|
||||||
|
|
||||||
const renderCards = (cardArray) => {
|
const renderCards = (cardArray) => {
|
||||||
return (
|
return (
|
||||||
cardArray &&
|
cardArray &&
|
||||||
@ -209,8 +260,11 @@ function Home() {
|
|||||||
version={item.latestVersion}
|
version={item.latestVersion}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
downloads={item.downloads}
|
downloads={item.downloads}
|
||||||
|
stars={item.stars}
|
||||||
isSigned={item.isSigned}
|
isSigned={item.isSigned}
|
||||||
|
signatureInfo={item.signatureInfo}
|
||||||
isBookmarked={item.isBookmarked}
|
isBookmarked={item.isBookmarked}
|
||||||
|
isStarred={item.isStarred}
|
||||||
vendor={item.vendor}
|
vendor={item.vendor}
|
||||||
platforms={item.platforms}
|
platforms={item.platforms}
|
||||||
key={index}
|
key={index}
|
||||||
@ -226,68 +280,89 @@ function Home() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const renderContent = () => {
|
||||||
<>
|
return isNoData() === true ? (
|
||||||
{isLoading ? (
|
<NoDataComponent text="No images" />
|
||||||
<Loading />
|
) : (
|
||||||
) : (
|
<Stack alignItems="center" className={classes.gridWrapper}>
|
||||||
<Stack alignItems="center" className={classes.gridWrapper}>
|
<Stack className={classes.sectionHeaderContainer} sx={{ paddingTop: '3rem' }}>
|
||||||
<Stack className={classes.sectionHeaderContainer} sx={{ paddingTop: '3rem' }}>
|
<div>
|
||||||
<div>
|
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||||
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
Most popular images
|
||||||
Most popular images
|
</Typography>
|
||||||
</Typography>
|
</div>
|
||||||
</div>
|
<div onClick={() => handleClickViewAll('sortby', sortByCriteria.downloads.value)}>
|
||||||
<div onClick={() => handleClickViewAll('sortby', sortByCriteria.downloads.value)}>
|
<Typography variant="body2" className={classes.viewAll}>
|
||||||
<Typography variant="body2" className={classes.viewAll}>
|
View all
|
||||||
View all
|
</Typography>
|
||||||
</Typography>
|
</div>
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
{isLoadingPopular ? <Loading /> : renderCards(popularData)}
|
|
||||||
{/* currently most popular will be by downloads until stars are implemented */}
|
|
||||||
<Stack className={classes.sectionHeaderContainer}>
|
|
||||||
<div>
|
|
||||||
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
|
||||||
Recently updated images
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
className={classes.viewAll}
|
|
||||||
onClick={() => handleClickViewAll('sortby', sortByCriteria.updateTime.value)}
|
|
||||||
>
|
|
||||||
View all
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
{isLoadingRecent ? <Loading /> : renderCards(recentData)}
|
|
||||||
{!isEmpty(bookmarkData) && (
|
|
||||||
<>
|
|
||||||
<Stack className={classes.sectionHeaderContainer}>
|
|
||||||
<div>
|
|
||||||
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
|
||||||
Bookmarks
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
className={classes.viewAll}
|
|
||||||
onClick={() => handleClickViewAll('filter', 'IsBookmarked')}
|
|
||||||
>
|
|
||||||
View all
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
{isLoadingBookmarks ? <Loading /> : renderCards(bookmarkData)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
{isLoadingPopular ? <Loading /> : renderCards(popularData, isLoadingPopular)}
|
||||||
</>
|
{/* currently most popular will be by downloads until stars are implemented */}
|
||||||
);
|
<Stack className={classes.sectionHeaderContainer}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||||
|
Recently updated images
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className={classes.viewAll}
|
||||||
|
onClick={() => handleClickViewAll('sortby', sortByCriteria.updateTime.value)}
|
||||||
|
>
|
||||||
|
View all
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
{isLoadingRecent ? <Loading /> : renderCards(recentData, isLoadingRecent)}
|
||||||
|
{!isEmpty(bookmarkData) && (
|
||||||
|
<>
|
||||||
|
<Stack className={classes.sectionHeaderContainer}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||||
|
Bookmarks
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className={classes.viewAll}
|
||||||
|
onClick={() => handleClickViewAll('filter', 'IsBookmarked')}
|
||||||
|
>
|
||||||
|
View all
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
{isLoadingBookmarks ? <Loading /> : renderCards(bookmarkData, isLoadingBookmarks)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isEmpty(starData) && (
|
||||||
|
<>
|
||||||
|
<Stack className={classes.sectionHeaderContainer}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||||
|
Stars
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className={classes.viewAll}
|
||||||
|
onClick={() => handleClickViewAll('filter', 'IsStarred')}
|
||||||
|
>
|
||||||
|
View all
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
{isLoadingStars ? <Loading /> : renderCards(starData, isLoadingStars)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>{isLoading ? <Loading /> : renderContent()}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
||||||
|
@ -20,7 +20,7 @@ import Alert from '@mui/material/Alert';
|
|||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Loading from '../Shared/Loading';
|
import Loading from '../Shared/Loading';
|
||||||
|
|
||||||
import { GoogleLoginButton, GithubLoginButton, DexLoginButton } from './ThirdPartyLoginComponents';
|
import { GoogleLoginButton, GithubLoginButton, OIDCLoginButton } from './ThirdPartyLoginComponents';
|
||||||
|
|
||||||
// styling
|
// styling
|
||||||
import { makeStyles } from '@mui/styles';
|
import { makeStyles } from '@mui/styles';
|
||||||
@ -284,14 +284,15 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
|||||||
let isGoogle = isObject(authMethods.openid?.providers?.google);
|
let isGoogle = isObject(authMethods.openid?.providers?.google);
|
||||||
// let isGitlab = isObject(authMethods.openid?.providers?.gitlab);
|
// let isGitlab = isObject(authMethods.openid?.providers?.gitlab);
|
||||||
let isGithub = isObject(authMethods.openid?.providers?.github);
|
let isGithub = isObject(authMethods.openid?.providers?.github);
|
||||||
let isDex = isObject(authMethods.openid?.providers?.dex);
|
let isOIDC = isObject(authMethods.openid?.providers?.oidc);
|
||||||
|
let oidcName = authMethods.openid?.providers?.oidc?.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" spacing="1rem" className={classes.thirdPartyLoginContainer}>
|
<Stack direction="column" spacing="1rem" className={classes.thirdPartyLoginContainer}>
|
||||||
{isGithub && <GithubLoginButton handleClick={handleClickExternalLogin} />}
|
{isGithub && <GithubLoginButton handleClick={handleClickExternalLogin} />}
|
||||||
{isGoogle && <GoogleLoginButton handleClick={handleClickExternalLogin} />}
|
{isGoogle && <GoogleLoginButton handleClick={handleClickExternalLogin} />}
|
||||||
{/* {isGitlab && <GitlabLoginButton handleClick={handleClickExternalLogin} />} */}
|
{/* {isGitlab && <GitlabLoginButton handleClick={handleClickExternalLogin} />} */}
|
||||||
{isDex && <DexLoginButton handleClick={handleClickExternalLogin} />}
|
{isOIDC && <OIDCLoginButton handleClick={handleClickExternalLogin} oidcName={oidcName} />}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -308,10 +309,16 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
|||||||
Sign In
|
Sign In
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography align="left" className={classes.subtext} variant="body1" gutterBottom>
|
<Typography align="left" className={classes.subtext} variant="body1" gutterBottom>
|
||||||
Welcome back! Please enter your details.
|
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
|
||||||
|
@ -80,14 +80,15 @@ function GitlabLoginButton({ handleClick }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DexLoginButton({ handleClick }) {
|
function OIDCLoginButton({ handleClick, oidcName }) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const loginWithName = oidcName || 'OIDC';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button fullWidth variant="contained" className={classes.button} onClick={(e) => handleClick(e, 'dex')}>
|
<Button fullWidth variant="contained" className={classes.button} onClick={(e) => handleClick(e, 'oidc')}>
|
||||||
Sign in with Dex
|
Sign in with {loginWithName}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { GithubLoginButton, GoogleLoginButton, GitlabLoginButton, DexLoginButton };
|
export { GithubLoginButton, GoogleLoginButton, GitlabLoginButton, OIDCLoginButton };
|
||||||
|
@ -9,12 +9,20 @@ 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';
|
||||||
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||||
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
import StarIcon from '@mui/icons-material/Star';
|
||||||
|
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||||
|
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';
|
||||||
@ -22,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: {
|
||||||
@ -195,6 +197,10 @@ function RepoDetails() {
|
|||||||
};
|
};
|
||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
|
const handleDeleteTag = (removed) => {
|
||||||
|
setTags((prevState) => prevState.filter((tag) => tag.tag !== removed));
|
||||||
|
};
|
||||||
|
|
||||||
const handlePlatformChipClick = (event) => {
|
const handlePlatformChipClick = (event) => {
|
||||||
const { textContent } = event.target;
|
const { textContent } = event.target;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -221,7 +227,7 @@ function RepoDetails() {
|
|||||||
|
|
||||||
const handleBookmarkClick = () => {
|
const handleBookmarkClick = () => {
|
||||||
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
|
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
|
||||||
if (response.status === 200) {
|
if (response && response.status === 200) {
|
||||||
setRepoDetailData((prevState) => ({
|
setRepoDetailData((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
isBookmarked: !prevState.isBookmarked
|
isBookmarked: !prevState.isBookmarked
|
||||||
@ -230,6 +236,17 @@ function RepoDetails() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStarClick = () => {
|
||||||
|
api.put(`${host()}${endpoints.starToggle(name)}`, abortController.signal).then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
setRepoDetailData((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
isStarred: !prevState.isStarred
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getVendor = () => {
|
const getVendor = () => {
|
||||||
return `${repoDetailData.newestTag?.Vendor || 'Vendor not available'} •`;
|
return `${repoDetailData.newestTag?.Vendor || 'Vendor not available'} •`;
|
||||||
};
|
};
|
||||||
@ -271,17 +288,31 @@ function RepoDetails() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
|
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
|
||||||
<VulnerabilityIconCheck vulnerabilitySeverity={repoDetailData?.vulnerabilitySeverity} />
|
<VulnerabilityIconCheck vulnerabilitySeverity={repoDetailData?.vulnerabilitySeverity} />
|
||||||
<SignatureIconCheck isSigned={repoDetailData.isSigned} />
|
<SignatureIconCheck
|
||||||
|
isSigned={repoDetailData.isSigned}
|
||||||
|
signatureInfo={repoDetailData.signatureInfo}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={1}>
|
||||||
|
{isAuthenticated() && (
|
||||||
|
<IconButton component="span" onClick={handleStarClick} data-testid="star-button">
|
||||||
|
{repoDetailData?.isStarred ? (
|
||||||
|
<StarIcon data-testid="starred" />
|
||||||
|
) : (
|
||||||
|
<StarBorderIcon data-testid="not-starred" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{isAuthenticated() && (
|
||||||
|
<IconButton component="span" onClick={handleBookmarkClick} data-testid="bookmark-button">
|
||||||
|
{repoDetailData?.isBookmarked ? (
|
||||||
|
<BookmarkIcon data-testid="bookmarked" />
|
||||||
|
) : (
|
||||||
|
<BookmarkBorderIcon data-testid="not-bookmarked" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{isAuthenticated() && (
|
|
||||||
<IconButton component="span" onClick={handleBookmarkClick} data-testid="bookmark-button">
|
|
||||||
{repoDetailData?.isBookmarked ? (
|
|
||||||
<BookmarkIcon data-testid="bookmarked" />
|
|
||||||
) : (
|
|
||||||
<BookmarkBorderIcon data-testid="not-bookmarked" />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Typography gutterBottom className={classes.repoTitle}>
|
<Typography gutterBottom className={classes.repoTitle}>
|
||||||
{repoDetailData?.title || 'Title not available'}
|
{repoDetailData?.title || 'Title not available'}
|
||||||
@ -314,7 +345,7 @@ function RepoDetails() {
|
|||||||
<Grid item xs={12} md={8} className={classes.tags}>
|
<Grid item xs={12} md={8} className={classes.tags}>
|
||||||
<Card className={classes.cardRoot}>
|
<Card className={classes.cardRoot}>
|
||||||
<CardContent className={classes.tagsContent}>
|
<CardContent className={classes.tagsContent}>
|
||||||
<Tags tags={tags} />
|
<Tags tags={tags} repoName={name} onTagDelete={handleDeleteTag} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -43,7 +43,7 @@ const useStyles = makeStyles(() => ({
|
|||||||
|
|
||||||
export default function Tags(props) {
|
export default function Tags(props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { tags } = props;
|
const { tags, repoName, onTagDelete } = props;
|
||||||
const [tagsFilter, setTagsFilter] = useState('');
|
const [tagsFilter, setTagsFilter] = useState('');
|
||||||
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
|
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
|
||||||
|
|
||||||
@ -63,6 +63,9 @@ export default function Tags(props) {
|
|||||||
lastUpdated={tag.lastUpdated}
|
lastUpdated={tag.lastUpdated}
|
||||||
vendor={tag.vendor}
|
vendor={tag.vendor}
|
||||||
manifests={tag.manifests}
|
manifests={tag.manifests}
|
||||||
|
repo={repoName}
|
||||||
|
onTagDelete={onTagDelete}
|
||||||
|
isDeletable={tag.isDeletable}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
54
src/components/Shared/DeleteTag.jsx
Normal file
54
src/components/Shared/DeleteTag.jsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { IconButton } from '@mui/material';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
|
// utility
|
||||||
|
import { api, endpoints } from '../../api';
|
||||||
|
|
||||||
|
// components
|
||||||
|
import DeleteTagConfirmDialog from 'components/Shared/DeleteTagConfirmDialog';
|
||||||
|
import { host } from '../../host';
|
||||||
|
|
||||||
|
export default function DeleteTag(props) {
|
||||||
|
const { repo, tag, onTagDelete } = props;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleClickOpen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTag = (repo, tag) => {
|
||||||
|
api
|
||||||
|
.delete(`${host()}${endpoints.deleteImage(repo, tag)}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response && response.status == 202) {
|
||||||
|
onTagDelete(tag);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfirm = () => {
|
||||||
|
deleteTag(repo, tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<IconButton onClick={handleClickOpen}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
<DeleteTagConfirmDialog
|
||||||
|
onClose={handleClose}
|
||||||
|
open={open}
|
||||||
|
title={`Permanently delete image ${repo}:${tag}?`}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
30
src/components/Shared/DeleteTagConfirmDialog.jsx
Normal file
30
src/components/Shared/DeleteTagConfirmDialog.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Button, Dialog, DialogTitle, DialogActions } from '@mui/material';
|
||||||
|
|
||||||
|
export default function DeleteTagConfirmDialog(props) {
|
||||||
|
const { onClose, open, title, onConfirm } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog data-testid="delete-dialog" onClose={onClose} open={open} color="primary">
|
||||||
|
<DialogTitle> {title} </DialogTitle>
|
||||||
|
<DialogActions style={{ justifyContent: 'center' }}>
|
||||||
|
<Button data-testid="cancel-delete" variant="contained" onClick={onClose} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-testid="confirm-delete"
|
||||||
|
color="error"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
onConfirm();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@ -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: {
|
||||||
|
40
src/components/Shared/NoDataComponent.jsx
Normal file
40
src/components/Shared/NoDataComponent.jsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// react global
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Stack, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
//styling
|
||||||
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
|
||||||
|
import nodataImage from '../../assets/noData.svg';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
noDataContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
noDataImage: {
|
||||||
|
maxWidth: '233px',
|
||||||
|
maxHeight: '240px'
|
||||||
|
},
|
||||||
|
noDataText: {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: theme.palette.secondary.main
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function NoDataComponent({ text }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className={classes.noDataContainer}>
|
||||||
|
<img src={nodataImage} className={classes.noDataImage} />
|
||||||
|
<Typography className={classes.noDataText}>{text ? text : 'No Data'}</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NoDataComponent;
|
@ -28,6 +28,8 @@ import {
|
|||||||
import makeStyles from '@mui/styles/makeStyles';
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
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 StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
// placeholder images
|
// placeholder images
|
||||||
@ -183,16 +185,24 @@ function RepoCard(props) {
|
|||||||
platforms,
|
platforms,
|
||||||
description,
|
description,
|
||||||
downloads,
|
downloads,
|
||||||
|
stars,
|
||||||
isSigned,
|
isSigned,
|
||||||
|
signatureInfo,
|
||||||
lastUpdated,
|
lastUpdated,
|
||||||
version,
|
version,
|
||||||
vulnerabilityData,
|
vulnerabilityData,
|
||||||
isBookmarked
|
isBookmarked,
|
||||||
|
isStarred
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// keep a local bookmark state to display in the ui dynamically on updates
|
// keep a local bookmark state to display in the ui dynamically on updates
|
||||||
const [currentBookmarkValue, setCurrentBookmarkValue] = useState(isBookmarked);
|
const [currentBookmarkValue, setCurrentBookmarkValue] = useState(isBookmarked);
|
||||||
|
|
||||||
|
// keep a local star state to display in the ui dynamically on updates
|
||||||
|
const [currentStarValue, setCurrentStarValue] = useState(isStarred);
|
||||||
|
|
||||||
|
const [currentStarCount, setCurrentStarCount] = useState(stars);
|
||||||
|
|
||||||
const goToDetails = () => {
|
const goToDetails = () => {
|
||||||
navigate(`/image/${encodeURIComponent(name)}`);
|
navigate(`/image/${encodeURIComponent(name)}`);
|
||||||
};
|
};
|
||||||
@ -214,6 +224,23 @@ function RepoCard(props) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStarClick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
api.put(`${host()}${endpoints.starToggle(name)}`, abortController.signal).then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
setCurrentStarValue((prevState) => !prevState);
|
||||||
|
currentStarValue
|
||||||
|
? setCurrentStarCount((prevState) => {
|
||||||
|
return !isNaN(prevState) ? prevState - 1 : prevState;
|
||||||
|
})
|
||||||
|
: setCurrentStarCount((prevState) => {
|
||||||
|
return !isNaN(prevState) ? prevState + 1 : prevState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const platformChips = () => {
|
const platformChips = () => {
|
||||||
const filteredPlatforms = uniq(platforms?.flatMap((platform) => [platform.Os, platform.Arch]));
|
const filteredPlatforms = uniq(platforms?.flatMap((platform) => [platform.Os, platform.Arch]));
|
||||||
const hiddenChips = filteredPlatforms.length - MAX_PLATFORM_CHIPS;
|
const hiddenChips = filteredPlatforms.length - MAX_PLATFORM_CHIPS;
|
||||||
@ -259,6 +286,16 @@ function RepoCard(props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderStar = () => {
|
||||||
|
return (
|
||||||
|
isAuthenticated() && (
|
||||||
|
<IconButton component="span" onClick={handleStarClick} data-testid="star-button">
|
||||||
|
{currentStarValue ? <StarIcon data-testid="starred" /> : <StarBorderIcon data-testid="not-starred" />}
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="outlined" className={classes.card} data-testid="repo-card">
|
<Card variant="outlined" className={classes.card} data-testid="repo-card">
|
||||||
<CardActionArea
|
<CardActionArea
|
||||||
@ -290,7 +327,7 @@ function RepoCard(props) {
|
|||||||
<VulnerabilityIconCheck {...vulnerabilityData} className="hide-on-mobile" />
|
<VulnerabilityIconCheck {...vulnerabilityData} className="hide-on-mobile" />
|
||||||
</div>
|
</div>
|
||||||
<div className="hide-on-mobile">
|
<div className="hide-on-mobile">
|
||||||
<SignatureIconCheck isSigned={isSigned} className="hide-on-mobile" />
|
<SignatureIconCheck isSigned={isSigned} signatureInfo={signatureInfo} className="hide-on-mobile" />
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Tooltip title={description || 'Description not available'} placement="top">
|
<Tooltip title={description || 'Description not available'} placement="top">
|
||||||
@ -336,6 +373,15 @@ function RepoCard(props) {
|
|||||||
#1
|
#1
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid> */}
|
</Grid> */}
|
||||||
|
<Grid item xs={12}>
|
||||||
|
{renderStar()}
|
||||||
|
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
|
||||||
|
Stars •
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" component="span" className={classes.contentRightValue}>
|
||||||
|
{!isNaN(currentStarCount) ? currentStarCount : `not available`}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
<Grid container item xs={12} className={classes.contentRightActions}>
|
<Grid container item xs={12} className={classes.contentRightActions}>
|
||||||
<Grid item>{renderBookmark()}</Grid>
|
<Grid item>{renderBookmark()}</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
21
src/components/Shared/SignatureTooltip.jsx
Normal file
21
src/components/Shared/SignatureTooltip.jsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Typography, Stack } from '@mui/material';
|
||||||
|
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
function SignatureTooltip({ isSigned, signatureInfo }) {
|
||||||
|
const { tool, isTrusted, author } = !isEmpty(signatureInfo)
|
||||||
|
? signatureInfo[0]
|
||||||
|
: { tool: 'Unknown', isTrusted: 'Unknown', author: 'Unknown' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="column">
|
||||||
|
<Typography>{isSigned ? 'Verified Signature' : 'Unverified Signature'}</Typography>
|
||||||
|
<Typography>Tool: {tool}</Typography>
|
||||||
|
<Typography>Trusted: {!isEmpty(isTrusted) ? isTrusted : 'Unknown'}</Typography>
|
||||||
|
<Typography>Author: {!isEmpty(author) ? author : 'Unknown'}</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignatureTooltip;
|
@ -6,6 +6,7 @@ import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
|||||||
import transform from 'utilities/transform';
|
import transform from 'utilities/transform';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||||
|
import DeleteTag from 'components/Shared/DeleteTag';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
card: {
|
card: {
|
||||||
@ -78,9 +79,9 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export default function TagCard(props) {
|
export default function TagCard(props) {
|
||||||
const { repoName, tag, lastUpdated, vendor, manifests } = props;
|
const { repoName, tag, lastUpdated, vendor, manifests, repo, onTagDelete, isDeletable } = props;
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
const lastDate = lastUpdated
|
const lastDate = lastUpdated
|
||||||
@ -99,9 +100,12 @@ export default function TagCard(props) {
|
|||||||
return (
|
return (
|
||||||
<Card className={classes.card} raised>
|
<Card className={classes.card} raised>
|
||||||
<CardContent className={classes.content}>
|
<CardContent className={classes.content}>
|
||||||
<Typography variant="body1" align="left" className={classes.tagHeading}>
|
<Stack direction="row" spacing={2} justifyContent="space-between">
|
||||||
Tag
|
<Typography variant="body1" align="left" className={classes.tagHeading}>
|
||||||
</Typography>
|
Tag
|
||||||
|
</Typography>
|
||||||
|
{isDeletable && <DeleteTag repo={repo} tag={tag} onTagDelete={onTagDelete} />}
|
||||||
|
</Stack>
|
||||||
<Typography variant="body1" align="left" className={classes.tagName} onClick={() => goToTags()}>
|
<Typography variant="body1" align="left" className={classes.tagName} onClick={() => goToTags()}>
|
||||||
{repoName && `${repoName}:`}
|
{repoName && `${repoName}:`}
|
||||||
{tag}
|
{tag}
|
||||||
|
@ -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;
|
@ -4,24 +4,55 @@ import React, { useEffect, useMemo, useState, useRef } from 'react';
|
|||||||
import { api, endpoints } from '../../../api';
|
import { api, endpoints } from '../../../api';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { Stack, Typography, InputBase } from '@mui/material';
|
import {
|
||||||
|
IconButton,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
InputBase,
|
||||||
|
ToggleButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Divider,
|
||||||
|
Snackbar,
|
||||||
|
CircularProgress
|
||||||
|
} from '@mui/material';
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
import { host } from '../../../host';
|
import { host } from '../../../host';
|
||||||
import { debounce, isEmpty } from 'lodash';
|
import { debounce, isEmpty } from 'lodash';
|
||||||
import Loading from '../../Shared/Loading';
|
import Loading from '../../Shared/Loading';
|
||||||
import { mapCVEInfo } from 'utilities/objectModels';
|
import { mapCVEInfo, mapAllCVEInfo } from 'utilities/objectModels';
|
||||||
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
|
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
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',
|
||||||
@ -40,9 +71,17 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
fontSize: '1.4rem',
|
fontSize: '1.4rem',
|
||||||
fontWeight: '600'
|
fontWeight: '600'
|
||||||
},
|
},
|
||||||
|
vulnerabilities: {
|
||||||
|
position: 'relative',
|
||||||
|
maxWidth: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
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',
|
||||||
@ -50,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%'
|
||||||
@ -65,22 +118,56 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
'&::placeholder': {
|
'&::placeholder': {
|
||||||
opacity: '1'
|
opacity: '1'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
popper: {
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: '0.3rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'left'
|
||||||
|
},
|
||||||
|
dropdownArrowBox: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
dropdownText: {
|
||||||
|
color: '#1479FF',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
width: '95%'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function VulnerabilitiesDetails(props) {
|
function VulnerabilitiesDetails(props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [cveData, setCveData] = useState([]);
|
const [cveData, setCveData] = 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 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);
|
||||||
|
|
||||||
|
const [anchorExport, setAnchorExport] = useState(null);
|
||||||
|
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}`;
|
||||||
};
|
};
|
||||||
@ -91,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);
|
||||||
}
|
}
|
||||||
@ -110,10 +213,29 @@ function VulnerabilitiesDetails(props) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setCveData([]);
|
setCveData([]);
|
||||||
|
setCVESummary(() => {});
|
||||||
setIsEndOfList(true);
|
setIsEndOfList(true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllCVEs = () => {
|
||||||
|
api
|
||||||
|
.get(`${host()}${endpoints.allVulnerabilitiesForRepo(getCVERequestName())}`, abortController.signal)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
const cveInfo = response.data.data.CVEListForImage?.CVEList;
|
||||||
|
const cveListData = mapAllCVEInfo(cveInfo);
|
||||||
|
setAllCveData(cveListData);
|
||||||
|
}
|
||||||
|
setIsLoadingAllCve(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
setAllCveData([]);
|
||||||
|
setIsLoadingAllCve(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const resetPagination = () => {
|
const resetPagination = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setIsEndOfList(false);
|
setIsEndOfList(false);
|
||||||
@ -124,12 +246,50 @@ function VulnerabilitiesDetails(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnExportExcel = () => {
|
||||||
|
const wb = XLSX.utils.book_new(),
|
||||||
|
ws = XLSX.utils.json_to_sheet(allCveData);
|
||||||
|
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, name.replaceAll('/', '_') + '_' + tag);
|
||||||
|
|
||||||
|
XLSX.writeFile(wb, `${name}:${tag}-vulnerabilities.xlsx`);
|
||||||
|
|
||||||
|
handleCloseExport();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnExportCSV = () => {
|
||||||
|
const fileName = `${name}:${tag}-vulnerabilities`;
|
||||||
|
const exportType = exportFromJSON.types.csv;
|
||||||
|
|
||||||
|
exportFromJSON({ data: allCveData, fileName, exportType });
|
||||||
|
|
||||||
|
handleCloseExport();
|
||||||
|
};
|
||||||
|
|
||||||
const handleCveFilterChange = (e) => {
|
const handleCveFilterChange = (e) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
setCveFilter(value);
|
setCveFilter(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClickExport = (event) => {
|
||||||
|
setAnchorExport(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseExport = () => {
|
||||||
|
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();
|
||||||
@ -163,25 +323,52 @@ 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();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openExport && isEmpty(allCveData)) {
|
||||||
|
getAllCVEs();
|
||||||
|
}
|
||||||
|
}, [openExport]);
|
||||||
|
|
||||||
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 />;
|
||||||
@ -194,21 +381,112 @@ function VulnerabilitiesDetails(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" spacing="1rem" data-testid="vulnerability-container">
|
<Stack direction="column" spacing="1rem" data-testid="vulnerability-container">
|
||||||
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
<Stack className={classes.vulnerabilities}>
|
||||||
Vulnerabilities
|
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
||||||
</Typography>
|
Vulnerabilities
|
||||||
<Stack className={classes.search}>
|
</Typography>
|
||||||
<InputBase
|
<Stack direction="row" spacing="1rem" className={classes.viewModes}>
|
||||||
placeholder={'Search'}
|
<IconButton disableRipple onClick={handleClickExport}>
|
||||||
classes={{ root: classes.searchInputBase, input: classes.input }}
|
<DownloadIcon />
|
||||||
onChange={debouncedChangeHandler}
|
</IconButton>
|
||||||
/>
|
<Snackbar
|
||||||
<div className={classes.searchIcon}>
|
open={openExport && isLoadingAllCve}
|
||||||
<SearchIcon />
|
message="Getting your data ready for export"
|
||||||
</div>
|
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
|
||||||
|
anchorEl={anchorExport}
|
||||||
|
open={openExport}
|
||||||
|
onClose={handleCloseExport}
|
||||||
|
data-testid="export-dropdown"
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleOnExportCSV}
|
||||||
|
disableRipple
|
||||||
|
disabled={isLoadingAllCve}
|
||||||
|
className={classes.popper}
|
||||||
|
data-testid="export-csv-menuItem"
|
||||||
|
>
|
||||||
|
csv
|
||||||
|
</MenuItem>
|
||||||
|
<Divider sx={{ my: 0.5 }} />
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleOnExportExcel}
|
||||||
|
disableRipple
|
||||||
|
disabled={isLoadingAllCve}
|
||||||
|
className={classes.popper}
|
||||||
|
data-testid="export-excel-menuItem"
|
||||||
|
>
|
||||||
|
xlsx
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Stack>
|
||||||
|
{renderCVESummary()}
|
||||||
|
<Stack direction="row">
|
||||||
|
<div className={classes.dropdownArrowBox} onClick={handleExpandCVESearch}>
|
||||||
|
{!openExcludeSearch ? (
|
||||||
|
<KeyboardArrowRight className={classes.dropdownText} />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowDown className={classes.dropdownText} />
|
||||||
|
)}
|
||||||
|
</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: {
|
||||||
@ -260,7 +261,10 @@ function TagDetails() {
|
|||||||
vulnerabilitySeverity={imageDetailData.vulnerabiltySeverity}
|
vulnerabilitySeverity={imageDetailData.vulnerabiltySeverity}
|
||||||
count={imageDetailData.vulnerabilityCount}
|
count={imageDetailData.vulnerabilityCount}
|
||||||
/>
|
/>
|
||||||
<SignatureIconCheck isSigned={imageDetailData.isSigned} />
|
<SignatureIconCheck
|
||||||
|
isSigned={imageDetailData.isSigned}
|
||||||
|
signatureInfo={imageDetailData.signatureInfo}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" alignItems="center" spacing="1rem">
|
<Stack direction="row" alignItems="center" spacing="1rem">
|
||||||
|
@ -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 };
|
||||||
|
@ -6,6 +6,10 @@ const osFilters = [
|
|||||||
{
|
{
|
||||||
label: 'linux',
|
label: 'linux',
|
||||||
value: 'linux'
|
value: 'linux'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'freebsd',
|
||||||
|
value: 'freebsd'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -19,6 +23,11 @@ const imageFilters = [
|
|||||||
label: 'Bookmarks',
|
label: 'Bookmarks',
|
||||||
value: 'IsBookmarked',
|
value: 'IsBookmarked',
|
||||||
type: 'boolean'
|
type: 'boolean'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Starred Repositories',
|
||||||
|
value: 'IsStarred',
|
||||||
|
type: 'boolean'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ const mapToRepo = (responseRepo) => {
|
|||||||
tags: responseRepo.NewestImage?.Labels,
|
tags: responseRepo.NewestImage?.Labels,
|
||||||
description: responseRepo.NewestImage?.Description,
|
description: responseRepo.NewestImage?.Description,
|
||||||
isSigned: responseRepo.NewestImage?.IsSigned,
|
isSigned: responseRepo.NewestImage?.IsSigned,
|
||||||
|
signatureInfo: responseRepo.NewestImage?.SignatureInfo?.map((sigInfo) => mapSignatureInfo(sigInfo)),
|
||||||
isBookmarked: responseRepo.IsBookmarked,
|
isBookmarked: responseRepo.IsBookmarked,
|
||||||
isStarred: responseRepo.IsStarred,
|
isStarred: responseRepo.IsStarred,
|
||||||
platforms: responseRepo.Platforms,
|
platforms: responseRepo.Platforms,
|
||||||
@ -14,6 +15,7 @@ const mapToRepo = (responseRepo) => {
|
|||||||
logo: responseRepo.NewestImage?.Logo,
|
logo: responseRepo.NewestImage?.Logo,
|
||||||
lastUpdated: responseRepo.LastUpdated,
|
lastUpdated: responseRepo.LastUpdated,
|
||||||
downloads: responseRepo.DownloadCount,
|
downloads: responseRepo.DownloadCount,
|
||||||
|
stars: responseRepo.StarCount,
|
||||||
vulnerabiltySeverity: responseRepo.NewestImage?.Vulnerabilities?.MaxSeverity,
|
vulnerabiltySeverity: responseRepo.NewestImage?.Vulnerabilities?.MaxSeverity,
|
||||||
vulnerabilityCount: responseRepo.NewestImage?.Vulnerabilities?.Count
|
vulnerabilityCount: responseRepo.NewestImage?.Vulnerabilities?.Count
|
||||||
};
|
};
|
||||||
@ -32,11 +34,13 @@ const mapToRepoFromRepoInfo = (responseRepoInfo) => {
|
|||||||
title: responseRepoInfo.Summary?.NewestImage?.Title,
|
title: responseRepoInfo.Summary?.NewestImage?.Title,
|
||||||
source: responseRepoInfo.Summary?.NewestImage?.Source,
|
source: responseRepoInfo.Summary?.NewestImage?.Source,
|
||||||
downloads: responseRepoInfo.Summary?.NewestImage?.DownloadCount,
|
downloads: responseRepoInfo.Summary?.NewestImage?.DownloadCount,
|
||||||
|
stars: responseRepoInfo.Summary?.NewestImage?.StarCount,
|
||||||
overview: responseRepoInfo.Summary?.NewestImage?.Documentation,
|
overview: responseRepoInfo.Summary?.NewestImage?.Documentation,
|
||||||
license: responseRepoInfo.Summary?.NewestImage?.Licenses,
|
license: responseRepoInfo.Summary?.NewestImage?.Licenses,
|
||||||
vulnerabilitySeverity: responseRepoInfo.Summary?.NewestImage?.Vulnerabilities?.MaxSeverity,
|
vulnerabilitySeverity: responseRepoInfo.Summary?.NewestImage?.Vulnerabilities?.MaxSeverity,
|
||||||
vulnerabilityCount: responseRepoInfo.Summary?.NewestImage?.Vulnerabilities?.Count,
|
vulnerabilityCount: responseRepoInfo.Summary?.NewestImage?.Vulnerabilities?.Count,
|
||||||
isSigned: responseRepoInfo.Summary?.NewestImage?.IsSigned,
|
isSigned: responseRepoInfo.Summary?.NewestImage?.IsSigned,
|
||||||
|
signatureInfo: responseRepoInfo.Summary?.NewestImage?.SignatureInfo?.map((sigInfo) => mapSignatureInfo(sigInfo)),
|
||||||
isBookmarked: responseRepoInfo.Summary?.IsBookmarked,
|
isBookmarked: responseRepoInfo.Summary?.IsBookmarked,
|
||||||
isStarred: responseRepoInfo.Summary?.IsStarred,
|
isStarred: responseRepoInfo.Summary?.IsStarred,
|
||||||
logo: responseRepoInfo.Summary?.NewestImage?.Logo
|
logo: responseRepoInfo.Summary?.NewestImage?.Logo
|
||||||
@ -51,9 +55,11 @@ const mapToImage = (responseImage) => {
|
|||||||
referrers: responseImage.Referrers,
|
referrers: responseImage.Referrers,
|
||||||
size: responseImage.Size,
|
size: responseImage.Size,
|
||||||
downloadCount: responseImage.DownloadCount,
|
downloadCount: responseImage.DownloadCount,
|
||||||
|
starCount: responseImage.StarCount,
|
||||||
lastUpdated: responseImage.LastUpdated,
|
lastUpdated: responseImage.LastUpdated,
|
||||||
description: responseImage.Description,
|
description: responseImage.Description,
|
||||||
isSigned: responseImage.IsSigned,
|
isSigned: responseImage.IsSigned,
|
||||||
|
signatureInfo: responseImage.SignatureInfo?.map((sigInfo) => mapSignatureInfo(sigInfo)),
|
||||||
license: responseImage.Licenses,
|
license: responseImage.Licenses,
|
||||||
labels: responseImage.Labels,
|
labels: responseImage.Labels,
|
||||||
title: responseImage.Title,
|
title: responseImage.Title,
|
||||||
@ -63,6 +69,7 @@ const mapToImage = (responseImage) => {
|
|||||||
authors: responseImage.Authors,
|
authors: responseImage.Authors,
|
||||||
vulnerabiltySeverity: responseImage.Vulnerabilities?.MaxSeverity,
|
vulnerabiltySeverity: responseImage.Vulnerabilities?.MaxSeverity,
|
||||||
vulnerabilityCount: responseImage.Vulnerabilities?.Count,
|
vulnerabilityCount: responseImage.Vulnerabilities?.Count,
|
||||||
|
isDeletable: responseImage.IsDeletable,
|
||||||
// frontend only prop to increase interop with Repo objects and code reusability
|
// frontend only prop to increase interop with Repo objects and code reusability
|
||||||
name: `${responseImage.RepoName}:${responseImage.Tag}`
|
name: `${responseImage.RepoName}:${responseImage.Tag}`
|
||||||
};
|
};
|
||||||
@ -76,6 +83,7 @@ const mapToManifest = (responseManifest) => {
|
|||||||
size: responseManifest.Size,
|
size: responseManifest.Size,
|
||||||
platform: responseManifest.Platform,
|
platform: responseManifest.Platform,
|
||||||
downloadCount: responseManifest.DownloadCount,
|
downloadCount: responseManifest.DownloadCount,
|
||||||
|
starCount: responseManifest.StarCount,
|
||||||
layers: responseManifest.Layers,
|
layers: responseManifest.Layers,
|
||||||
history: responseManifest.History,
|
history: responseManifest.History,
|
||||||
vulnerabilities: responseManifest.Vulnerabilities
|
vulnerabilities: responseManifest.Vulnerabilities
|
||||||
@ -88,12 +96,52 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapAllCVEInfo = (cveInfo) => {
|
||||||
|
const cveList = cveInfo.flatMap((cve) => {
|
||||||
|
return cve.PackageList.map((packageInfo) => {
|
||||||
|
return {
|
||||||
|
id: cve.Id,
|
||||||
|
severity: cve.Severity,
|
||||||
|
title: cve.Title,
|
||||||
|
description: cve.Description,
|
||||||
|
reference: cve.Reference,
|
||||||
|
packageName: packageInfo.Name,
|
||||||
|
packagePath: packageInfo.PackagePath,
|
||||||
|
packageInstalledVersion: packageInfo.InstalledVersion,
|
||||||
|
packageFixedVersion: packageInfo.FixedVersion
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return cveList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapSignatureInfo = (signatureInfo) => {
|
||||||
|
return signatureInfo
|
||||||
|
? {
|
||||||
|
tool: signatureInfo.Tool,
|
||||||
|
isTrusted: signatureInfo.IsTrusted?.toString(),
|
||||||
|
author: signatureInfo.Author
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
tool: 'Unknown',
|
||||||
|
isTrusted: 'Unknown',
|
||||||
|
author: 'Unknown'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const mapReferrer = (referrer) => ({
|
const mapReferrer = (referrer) => ({
|
||||||
mediaType: referrer.MediaType,
|
mediaType: referrer.MediaType,
|
||||||
artifactType: referrer.ArtifactType,
|
artifactType: referrer.ArtifactType,
|
||||||
@ -102,4 +150,4 @@ const mapReferrer = (referrer) => ({
|
|||||||
annotations: referrer.Annotations?.map((annotation) => ({ key: annotation.Key, value: annotation.Value }))
|
annotations: referrer.Annotations?.map((annotation) => ({ key: annotation.Key, value: annotation.Value }))
|
||||||
});
|
});
|
||||||
|
|
||||||
export { mapToRepo, mapToImage, mapToRepoFromRepoInfo, mapCVEInfo, mapReferrer, mapToManifest };
|
export { mapToRepo, mapToImage, mapToRepoFromRepoInfo, mapCVEInfo, mapAllCVEInfo, mapReferrer, mapToManifest };
|
||||||
|
@ -4,6 +4,7 @@ const HOME_PAGE_SIZE = 10;
|
|||||||
const HOME_POPULAR_PAGE_SIZE = 3;
|
const HOME_POPULAR_PAGE_SIZE = 3;
|
||||||
const HOME_RECENT_PAGE_SIZE = 2;
|
const HOME_RECENT_PAGE_SIZE = 2;
|
||||||
const HOME_BOOKMARKS_PAGE_SIZE = 2;
|
const HOME_BOOKMARKS_PAGE_SIZE = 2;
|
||||||
|
const HOME_STARS_PAGE_SIZE = 2;
|
||||||
const CVE_FIXEDIN_PAGE_SIZE = 5;
|
const CVE_FIXEDIN_PAGE_SIZE = 5;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -13,5 +14,6 @@ export {
|
|||||||
CVE_FIXEDIN_PAGE_SIZE,
|
CVE_FIXEDIN_PAGE_SIZE,
|
||||||
HOME_POPULAR_PAGE_SIZE,
|
HOME_POPULAR_PAGE_SIZE,
|
||||||
HOME_RECENT_PAGE_SIZE,
|
HOME_RECENT_PAGE_SIZE,
|
||||||
HOME_BOOKMARKS_PAGE_SIZE
|
HOME_BOOKMARKS_PAGE_SIZE,
|
||||||
|
HOME_STARS_PAGE_SIZE
|
||||||
};
|
};
|
||||||
|
@ -84,11 +84,11 @@ const VulnerabilityChipCheck = ({ vulnerabilitySeverity }) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SignatureIconCheck = ({ isSigned }) => {
|
const SignatureIconCheck = ({ isSigned, signatureInfo }) => {
|
||||||
if (isSigned) {
|
if (isSigned) {
|
||||||
return <VerifiedSignatureIcon />;
|
return <VerifiedSignatureIcon signatureInfo={signatureInfo} />;
|
||||||
} else {
|
} else {
|
||||||
return <UnverifiedSignatureIcon />;
|
return <UnverifiedSignatureIcon signatureInfo={signatureInfo} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { Chip, Tooltip } from '@mui/material';
|
|||||||
import SvgIcon from '@mui/material/SvgIcon';
|
import SvgIcon from '@mui/material/SvgIcon';
|
||||||
import { ReactComponent as failedScanBug } from '../assets/failedScan.svg';
|
import { ReactComponent as failedScanBug } from '../assets/failedScan.svg';
|
||||||
import { createSvgIcon } from '@mui/material/utils';
|
import { createSvgIcon } from '@mui/material/utils';
|
||||||
|
import SignatureTooltip from 'components/Shared/SignatureTooltip';
|
||||||
|
|
||||||
const FilledBugIcon = createSvgIcon(
|
const FilledBugIcon = createSvgIcon(
|
||||||
<path d="M17.0293 5.13093V6.1543H18.3828L21.2414 3.24068L22.2621 4.27812L19.5552 7.03876L19.5879 7.12668C20.1841 8.73695 20.4862 10.4449 20.4793 12.1662C20.4793 12.5064 20.4678 12.8466 20.4448 13.186L20.4397 13.2634H24V14.7334H20.2569L20.2466 14.7932C19.9431 16.4882 19.3517 18.0338 18.5466 19.335L18.4862 19.4335L21.9276 22.9608L20.9052 24L17.6121 20.6239L17.5138 20.7365C16.0259 22.4333 14.0983 23.4514 11.9983 23.4514C9.86724 23.4514 7.91207 22.4016 6.41552 20.6573L6.31552 20.5413L3.08966 23.833L2.06897 22.792L5.45345 19.3403L5.39483 19.2436C4.61897 17.9618 4.04655 16.4478 3.75 14.7932L3.73966 14.7334H0V13.2634H3.55862L3.55345 13.1843C3.53103 12.8502 3.51897 12.509 3.51897 12.1644C3.51202 10.4654 3.80581 8.77905 4.38621 7.18646L4.41897 7.1003L1.64138 4.2535L2.66379 3.21606L5.53103 6.1543H6.96724V5.13093C6.96724 3.77012 7.49729 2.46505 8.4408 1.50281C9.3843 0.540578 10.664 0 11.9983 0C13.3326 0 14.6123 0.540578 15.5558 1.50281C16.4993 2.46505 17.0293 3.77012 17.0293 5.13093Z" />,
|
<path d="M17.0293 5.13093V6.1543H18.3828L21.2414 3.24068L22.2621 4.27812L19.5552 7.03876L19.5879 7.12668C20.1841 8.73695 20.4862 10.4449 20.4793 12.1662C20.4793 12.5064 20.4678 12.8466 20.4448 13.186L20.4397 13.2634H24V14.7334H20.2569L20.2466 14.7932C19.9431 16.4882 19.3517 18.0338 18.5466 19.335L18.4862 19.4335L21.9276 22.9608L20.9052 24L17.6121 20.6239L17.5138 20.7365C16.0259 22.4333 14.0983 23.4514 11.9983 23.4514C9.86724 23.4514 7.91207 22.4016 6.41552 20.6573L6.31552 20.5413L3.08966 23.833L2.06897 22.792L5.45345 19.3403L5.39483 19.2436C4.61897 17.9618 4.04655 16.4478 3.75 14.7932L3.73966 14.7334H0V13.2634H3.55862L3.55345 13.1843C3.53103 12.8502 3.51897 12.509 3.51897 12.1644C3.51202 10.4654 3.80581 8.77905 4.38621 7.18646L4.41897 7.1003L1.64138 4.2535L2.66379 3.21606L5.53103 6.1543H6.96724V5.13093C6.96724 3.77012 7.49729 2.46505 8.4408 1.50281C9.3843 0.540578 10.664 0 11.9983 0C13.3326 0 14.6123 0.540578 15.5558 1.50281C16.4993 2.46505 17.0293 3.77012 17.0293 5.13093Z" />,
|
||||||
@ -144,13 +145,10 @@ const CriticalVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
|
|||||||
const NoneVulnerabilityChip = () => {
|
const NoneVulnerabilityChip = () => {
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
label="No Vulnerability"
|
label="None"
|
||||||
sx={{ backgroundColor: '#E8F5E9', color: '#388E3C', fontSize: '0.8125rem' }}
|
sx={{ backgroundColor: '#E8F5E9', color: '#388E3C', fontSize: '0.8125rem' }}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onDelete={() => {
|
icon={<OutlinedBugIcon sx={{ color: '#388E3C!important' }} />}
|
||||||
return;
|
|
||||||
}}
|
|
||||||
deleteIcon={<OutlinedBugIcon sx={{ color: '#388E3C!important' }} />}
|
|
||||||
data-testid="none-vulnerability-chip"
|
data-testid="none-vulnerability-chip"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -158,13 +156,10 @@ const NoneVulnerabilityChip = () => {
|
|||||||
const UnknownVulnerabilityChip = () => {
|
const UnknownVulnerabilityChip = () => {
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
label="Unknown Vulnerability"
|
label="Unknown"
|
||||||
sx={{ backgroundColor: '#ECEFF1', color: '#52637A', fontSize: '0.8125rem' }}
|
sx={{ backgroundColor: '#ECEFF1', color: '#52637A', fontSize: '0.8125rem' }}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onDelete={() => {
|
icon={<OutlinedBugIcon sx={{ color: '#52637A!important' }} />}
|
||||||
return;
|
|
||||||
}}
|
|
||||||
deleteIcon={<OutlinedBugIcon sx={{ color: '#52637A!important' }} />}
|
|
||||||
data-testid="unknown-vulnerability-chip"
|
data-testid="unknown-vulnerability-chip"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -175,10 +170,7 @@ const FailedScanChip = () => {
|
|||||||
label="Failed to scan"
|
label="Failed to scan"
|
||||||
sx={{ backgroundColor: '#848484', color: '#F6F7F9', fontSize: '0.8125rem' }}
|
sx={{ backgroundColor: '#848484', color: '#F6F7F9', fontSize: '0.8125rem' }}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onDelete={() => {
|
icon={<SvgIcon component={failedScanBug} sx={{ color: '#F6F7F9!important' }} />}
|
||||||
return;
|
|
||||||
}}
|
|
||||||
deleteIcon={<SvgIcon component={failedScanBug} sx={{ color: '#F6F7F9!important' }} />}
|
|
||||||
data-testid="failed-vulnerability-chip"
|
data-testid="failed-vulnerability-chip"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -186,13 +178,10 @@ const FailedScanChip = () => {
|
|||||||
const LowVulnerabilityChip = () => {
|
const LowVulnerabilityChip = () => {
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
label="Low Vulnerability"
|
label="Low"
|
||||||
sx={{ backgroundColor: '#FFF3E0', color: '#FB8C00', fontSize: '0.8125rem' }}
|
sx={{ backgroundColor: '#FFF3E0', color: '#FB8C00', fontSize: '0.8125rem' }}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onDelete={() => {
|
icon={<OutlinedBugIcon sx={{ color: '#FB8C00!important' }} />}
|
||||||
return;
|
|
||||||
}}
|
|
||||||
deleteIcon={<OutlinedBugIcon sx={{ color: '#FB8C00!important' }} />}
|
|
||||||
data-testid="low-vulnerability-chip"
|
data-testid="low-vulnerability-chip"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -200,13 +189,10 @@ const LowVulnerabilityChip = () => {
|
|||||||
const MediumVulnerabilityChip = () => {
|
const MediumVulnerabilityChip = () => {
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
label="Medium Vulnerability"
|
label="Medium"
|
||||||
sx={{ backgroundColor: '#FFF3E0', color: '#FB8C00', fontSize: '0.8125rem' }}
|
sx={{ backgroundColor: '#FFF3E0', color: '#FB8C00', fontSize: '0.8125rem' }}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onDelete={() => {
|
icon={<FilledBugIcon sx={{ color: '#FB8C00!important' }} />}
|
||||||
return;
|
|
||||||
}}
|
|
||||||
deleteIcon={<FilledBugIcon sx={{ color: '#FB8C00!important' }} />}
|
|
||||||
data-testid="medium-vulnerability-chip"
|
data-testid="medium-vulnerability-chip"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -214,13 +200,10 @@ const MediumVulnerabilityChip = () => {
|
|||||||
const HighVulnerabilityChip = () => {
|
const HighVulnerabilityChip = () => {
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
label="High Vulnerability"
|
label="High"
|
||||||
sx={{ backgroundColor: '#FEEBEE', color: '#E53935', fontSize: '0.8125rem' }}
|
sx={{ backgroundColor: '#FEEBEE', color: '#E53935', fontSize: '0.8125rem' }}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onDelete={() => {
|
icon={<OutlinedBugIcon sx={{ color: '#E53935!important' }} />}
|
||||||
return;
|
|
||||||
}}
|
|
||||||
deleteIcon={<OutlinedBugIcon sx={{ color: '#E53935!important' }} />}
|
|
||||||
data-testid="high-vulnerability-chip"
|
data-testid="high-vulnerability-chip"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -228,21 +211,18 @@ const HighVulnerabilityChip = () => {
|
|||||||
const CriticalVulnerabilityChip = () => {
|
const CriticalVulnerabilityChip = () => {
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
label="Critical Vulnerability"
|
label="Critical"
|
||||||
sx={{ backgroundColor: '#FEEBEE', color: '#E53935', fontSize: '0.8125rem' }}
|
sx={{ backgroundColor: '#FEEBEE', color: '#E53935', fontSize: '0.8125rem' }}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onDelete={() => {
|
icon={<FilledBugIcon sx={{ color: '#E53935!important' }} />}
|
||||||
return;
|
|
||||||
}}
|
|
||||||
deleteIcon={<FilledBugIcon sx={{ color: '#E53935!important' }} />}
|
|
||||||
data-testid="critical-vulnerability-chip"
|
data-testid="critical-vulnerability-chip"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UnverifiedSignatureIcon = () => {
|
const UnverifiedSignatureIcon = ({ signatureInfo }) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Unverified Signature" placement="top">
|
<Tooltip title={<SignatureTooltip isSigned={false} signatureInfo={signatureInfo} />} placement="top">
|
||||||
<UnverifiedShieldIcon
|
<UnverifiedShieldIcon
|
||||||
sx={{
|
sx={{
|
||||||
color: '#E53935',
|
color: '#E53935',
|
||||||
@ -257,9 +237,9 @@ const UnverifiedSignatureIcon = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const VerifiedSignatureIcon = () => {
|
const VerifiedSignatureIcon = ({ signatureInfo }) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Verified Signature" placement="top">
|
<Tooltip title={<SignatureTooltip isSigned={true} signatureInfo={signatureInfo} />} placement="top">
|
||||||
<VerifiedShieldIcon
|
<VerifiedShieldIcon
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -76,8 +76,14 @@ test.describe('explore page test', () => {
|
|||||||
|
|
||||||
await expect(exploreFirst).toBeVisible({ timeout: 250000 });
|
await expect(exploreFirst).toBeVisible({ timeout: 250000 });
|
||||||
|
|
||||||
|
const windowsFilter = page.getByRole('checkbox', { name: 'windows' });
|
||||||
await linuxFilter.uncheck();
|
await linuxFilter.uncheck();
|
||||||
await page.getByRole('checkbox', { name: 'windows' }).check();
|
await windowsFilter.check();
|
||||||
|
await expect(exploreFirst).not.toBeVisible({ timeout: 250000 });
|
||||||
|
|
||||||
|
const freebsdFilter = page.getByRole('checkbox', { name: 'freebsd' });
|
||||||
|
await windowsFilter.uncheck();
|
||||||
|
await freebsdFilter.check();
|
||||||
await expect(exploreFirst).not.toBeVisible({ timeout: 250000 });
|
await expect(exploreFirst).not.toBeVisible({ timeout: 250000 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,15 +17,15 @@ const pageSizes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const endpoints = {
|
const endpoints = {
|
||||||
repoList: `/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage:%20{limit:15%20offset:0}){Results%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20%20Licenses%20Logo%20Title%20Source%20IsSigned%20Documentation%20Vendor%20Labels}%20DownloadCount}}}`,
|
repoList: `/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage:%20{limit:15%20offset:0}){Results%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20%20Licenses%20Logo%20Title%20Source%20IsSigned%20Documentation%20Vendor%20Labels}%20StarCount%20DownloadCount}}}`,
|
||||||
detailedRepoInfo: (name) =>
|
detailedRepoInfo: (name) =>
|
||||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:%22${name}%22){Images%20{Manifests%20{Digest%20Platform%20{Os%20Arch}%20Size}%20Vulnerabilities%20{MaxSeverity%20Count}%20Tag%20LastUpdated%20Vendor%20}%20Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20NewestImage%20{RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20Manifests%20{Digest}%20Tag%20Title%20Documentation%20DownloadCount%20Source%20Description%20Licenses}}}}`,
|
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:%22${name}%22){Images%20{Manifests%20{Digest%20Platform%20{Os%20Arch}%20Size}%20Vulnerabilities%20{MaxSeverity%20Count}%20Tag%20LastUpdated%20Vendor%20IsDeletable%20}%20Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20NewestImage%20{RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20Manifests%20{Digest}%20Tag%20Title%20Documentation%20DownloadCount%20Source%20Description%20Licenses}}}}`,
|
||||||
globalSearch: (searchTerm, sortCriteria, pageNumber = 1, pageSize = 10) =>
|
globalSearch: (searchTerm, sortCriteria, pageNumber = 1, pageSize = 10) =>
|
||||||
`/v2/_zot/ext/search?query={GlobalSearch(query:%22${searchTerm}%22,%20requestedPage:%20{limit:${pageSize}%20offset:${
|
`/v2/_zot/ext/search?query={GlobalSearch(query:%22${searchTerm}%22,%20requestedPage:%20{limit:${pageSize}%20offset:${
|
||||||
10 * (pageNumber - 1)
|
10 * (pageNumber - 1)
|
||||||
}%20sortBy:%20${sortCriteria}}%20)%20{Page%20{TotalCount%20ItemCount}%20Repos%20{Name%20LastUpdated%20Size%20Platforms%20{%20Os%20Arch%20}%20IsStarred%20IsBookmarked%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20IsSigned%20Licenses%20Vendor%20Labels%20}%20DownloadCount}}}`,
|
}%20sortBy:%20${sortCriteria}}%20)%20{Page%20{TotalCount%20ItemCount}%20Repos%20{Name%20LastUpdated%20Size%20Platforms%20{%20Os%20Arch%20}%20IsStarred%20IsBookmarked%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20IsSigned%20SignatureInfo%20{%20Tool%20IsTrusted%20Author%20}%20Licenses%20Vendor%20Labels%20}%20StarCount%20DownloadCount}}}`,
|
||||||
image: (name) =>
|
image: (name) =>
|
||||||
`/v2/_zot/ext/search?query={Image(image:%20%22${name}%22){RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20%20Referrers%20{MediaType%20ArtifactType%20Size%20Digest%20Annotations{Key%20Value}}%20Tag%20Manifests%20{History%20{Layer%20{Size%20Digest}%20HistoryDescription%20{CreatedBy%20EmptyLayer}}%20Digest%20ConfigDigest%20LastUpdated%20Size%20Platform%20{Os%20Arch}}%20Vendor%20Licenses%20}}`
|
`/v2/_zot/ext/search?query={Image(image:%20%22${name}%22){RepoName%20IsSigned%20SignatureInfo%20{%20Tool%20IsTrusted%20Author%20}%20Vulnerabilities%20{MaxSeverity%20Count}%20%20Referrers%20{MediaType%20ArtifactType%20Size%20Digest%20Annotations{Key%20Value}}%20Tag%20Manifests%20{History%20{Layer%20{Size%20Digest}%20HistoryDescription%20{CreatedBy%20EmptyLayer}}%20Digest%20ConfigDigest%20LastUpdated%20Size%20Platform%20{Os%20Arch}}%20Vendor%20Licenses%20}}`
|
||||||
};
|
};
|
||||||
|
|
||||||
export { hosts, endpoints, sortCriteria, pageSizes };
|
export { hosts, endpoints, sortCriteria, pageSizes };
|
||||||
|
Reference in New Issue
Block a user