Compare commits
10 Commits
commit-19e
...
commit-6cd
Author | SHA1 | Date | |
---|---|---|---|
6cda89c710 | |||
12b474e126 | |||
a9db66bd34 | |||
f4600b8b79 | |||
c375c0697a | |||
2e1e2e92b7 | |||
d9370fb9c1 | |||
e97e04eee5 | |||
a288523a3f | |||
fad5572db4 |
8
.github/workflows/dco.yml
vendored
8
.github/workflows/dco.yml
vendored
@ -2,16 +2,18 @@
|
||||
name: DCO
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.x
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Check DCO
|
||||
|
2
.github/workflows/end-to-end-test.yml
vendored
2
.github/workflows/end-to-end-test.yml
vendored
@ -94,7 +94,7 @@ jobs:
|
||||
- name: Build zot
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/zot
|
||||
make binary
|
||||
make binary ZUI_BUILD_PATH=$GITHUB_WORKSPACE/build
|
||||
ls -l bin/
|
||||
|
||||
- name: Bringup zot server
|
||||
|
103
package-lock.json
generated
103
package-lock.json
generated
@ -19,6 +19,7 @@
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.24.0",
|
||||
"downshift": "^6.1.12",
|
||||
"export-from-json": "^1.7.3",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.5.2",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
@ -26,7 +27,8 @@
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-sticky-el": "^2.0.9",
|
||||
"web-vitals": "^2.1.3"
|
||||
"web-vitals": "^2.1.3",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
||||
@ -5592,6 +5594,14 @@
|
||||
"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": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -6547,6 +6557,18 @@
|
||||
"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": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@ -6780,6 +6802,14 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||
@ -7030,6 +7060,17 @@
|
||||
"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": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@ -8926,6 +8967,11 @@
|
||||
"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": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
@ -9423,6 +9469,14 @@
|
||||
"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": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||
@ -16997,6 +17051,17 @@
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"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": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||
@ -18746,6 +18811,22 @@
|
||||
"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": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
@ -19133,6 +19214,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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
||||
|
@ -14,6 +14,7 @@
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.24.0",
|
||||
"downshift": "^6.1.12",
|
||||
"export-from-json": "^1.7.3",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.5.2",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
@ -21,17 +22,18 @@
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-sticky-el": "^2.0.9",
|
||||
"web-vitals": "^2.1.3"
|
||||
"web-vitals": "^2.1.3",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"prettier": "^2.7.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7"
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
@ -34,6 +34,7 @@ const mockImageList = {
|
||||
Size: '2806985',
|
||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
@ -58,6 +59,7 @@ const mockImageList = {
|
||||
Size: '231383863',
|
||||
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
@ -82,6 +84,7 @@ const mockImageList = {
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
@ -106,6 +109,7 @@ const mockImageList = {
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
@ -130,6 +134,7 @@ const mockImageList = {
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
@ -158,6 +163,7 @@ const mockImageList = {
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
@ -182,6 +188,7 @@ const mockImageList = {
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
@ -338,4 +345,13 @@ describe('Explore component', () => {
|
||||
await userEvent.click(bookmarkButton);
|
||||
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', () => {
|
||||
it('renders the filters cards', async () => {
|
||||
render(<StateFilterCardWrapper />);
|
||||
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
|
||||
expect(screen.getAllByRole('checkbox')).toHaveLength(3);
|
||||
|
||||
const checkbox = screen.getAllByRole('checkbox');
|
||||
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(() => {
|
||||
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').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<HomeWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(3));
|
||||
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(3));
|
||||
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(4));
|
||||
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(4));
|
||||
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').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<HomeWrapper />);
|
||||
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(3);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(4);
|
||||
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(4);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('renders vulnerability icons', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<HomeWrapper />);
|
||||
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(3);
|
||||
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(3);
|
||||
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(4);
|
||||
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(4);
|
||||
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
|
||||
});
|
||||
|
||||
@ -204,16 +246,17 @@ describe('Home component', () => {
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
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 () => {
|
||||
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: mockImageListBookmarks } });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListStars } });
|
||||
render(<HomeWrapper />);
|
||||
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: [] } });
|
||||
fireEvent.click(viewAllButtons[0]);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
@ -230,5 +273,10 @@ describe('Home component', () => {
|
||||
pathname: `/explore`,
|
||||
search: createSearchParams({ filter: 'IsBookmarked' }).toString()
|
||||
});
|
||||
fireEvent.click(viewAllButtons[3]);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
pathname: `/explore`,
|
||||
search: createSearchParams({ filter: 'IsStarred' }).toString()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -47,6 +47,7 @@ const mockRepoDetailsData = {
|
||||
Size: '451554070',
|
||||
Vendors: ['[The Node.js Docker Team](https://github.com/nodejs/docker-node)\n'],
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
@ -316,4 +317,13 @@ describe('Repo details component', () => {
|
||||
await userEvent.click(bookmarkButton);
|
||||
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',
|
||||
vendor: 'test1',
|
||||
isDeletable: true,
|
||||
manifests: [
|
||||
{
|
||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
@ -37,6 +38,7 @@ const mockedTagsData = [
|
||||
{
|
||||
tag: 'bullseye',
|
||||
vendor: 'test1',
|
||||
isDeletable: true,
|
||||
manifests: [
|
||||
{
|
||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
@ -52,6 +54,7 @@ const mockedTagsData = [
|
||||
{
|
||||
tag: '1.5.2',
|
||||
vendor: 'test1',
|
||||
isDeletable: true,
|
||||
manifests: [
|
||||
{
|
||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
@ -76,6 +79,18 @@ describe('Tags component', () => {
|
||||
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 () => {
|
||||
render(<TagsThemeWrapper />);
|
||||
const tagLink = await screen.findByText('latest');
|
||||
|
@ -6,6 +6,8 @@ import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
jest.mock('xlsx');
|
||||
|
||||
const StateVulnerabilitiesWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvider>
|
||||
@ -558,6 +560,32 @@ describe('Vulnerabilties page', () => {
|
||||
expect(await screen.findByText('latest')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow export of vulnerabilities list', async () => {
|
||||
const xlsxMock = jest.createMockFromModule('xlsx');
|
||||
xlsxMock.writeFile = jest.fn();
|
||||
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
|
||||
.mockResolvedValueOnce({ 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(/MS Excel/i);
|
||||
expect(exportAsExcelBtn).toBeInTheDocument();
|
||||
await fireEvent.click(exportAsExcelBtn);
|
||||
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle fixed CVE query errors', async () => {
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
|
17
src/api.js
17
src/api.js
@ -79,14 +79,15 @@ const api = {
|
||||
const endpoints = {
|
||||
status: `/v2/`,
|
||||
authConfig: `/v2/_zot/ext/mgmt`,
|
||||
openidAuth: `/auth/login`,
|
||||
logout: `/auth/logout`,
|
||||
openidAuth: `/zot/auth/login`,
|
||||
logout: `/zot/auth/logout`,
|
||||
deleteImage: (name, tag) => `/v2/${name}/manifests/${tag}`,
|
||||
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
||||
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
}}){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 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) =>
|
||||
`/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 SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor IsDeletable } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||
detailedImageInfo: (name, tag) =>
|
||||
`/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 = '') => {
|
||||
@ -98,6 +99,8 @@ const endpoints = {
|
||||
}
|
||||
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`;
|
||||
},
|
||||
allVulnerabilitiesForRepo: (name) =>
|
||||
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name InstalledVersion FixedVersion}}}}`,
|
||||
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }, filter = {}) => {
|
||||
let filterParam = '';
|
||||
if (filter.Os || filter.Arch) {
|
||||
@ -134,9 +137,10 @@ const endpoints = {
|
||||
if (filter.Arch) filterParam += ` Arch:${!isEmpty(filter.Arch) ? `${JSON.stringify(filter.Arch)}` : '""'}`;
|
||||
if (filter.HasToBeSigned) filterParam += ` HasToBeSigned: ${filter.HasToBeSigned}`;
|
||||
if (filter.IsBookmarked) filterParam += ` IsBookmarked: ${filter.IsBookmarked}`;
|
||||
if (filter.IsStarred) filterParam += ` IsStarred: ${filter.IsStarred}`;
|
||||
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 SignatureInfo { Tool IsTrusted Author } 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 }) => {
|
||||
const searchParam = searchQuery !== '' ? `query:"${searchQuery}"` : `query:""`;
|
||||
@ -145,7 +149,8 @@ const endpoints = {
|
||||
},
|
||||
referrers: ({ repo, digest, type = '' }) =>
|
||||
`/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 };
|
||||
|
@ -220,9 +220,11 @@ function Explore({ searchInputValue }) {
|
||||
version={item.latestVersion}
|
||||
description={item.description}
|
||||
downloads={item.downloads}
|
||||
stars={item.stars}
|
||||
isSigned={item.isSigned}
|
||||
signatureInfo={item.signatureInfo}
|
||||
isBookmarked={item.isBookmarked}
|
||||
isStarred={item.isStarred}
|
||||
vendor={item.vendor}
|
||||
platforms={item.platforms}
|
||||
key={index}
|
||||
|
@ -149,14 +149,14 @@ function Header({ setSearchCurrentValue = () => {} }) {
|
||||
</Link>
|
||||
</Grid>
|
||||
<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
|
||||
</a>
|
||||
</Grid>
|
||||
<Grid item className={classes.headerLinkContainer}>
|
||||
<a
|
||||
className={classes.link}
|
||||
href="https://zotregistry.io/v1.4.3/general/concepts/"
|
||||
href="https://zotregistry.dev/v2.0.0/general/concepts/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
@ -295,12 +295,26 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
||||
<List
|
||||
{...getMenuProps()}
|
||||
className={
|
||||
isOpen && !isLoading && !isFailedSearch
|
||||
isOpen && !isFailedSearch
|
||||
? `${classes.resultsWrapper} ${isComponentFocused && classes.resultsWrapperFocused}`
|
||||
: classes.resultsWrapperHidden
|
||||
}
|
||||
>
|
||||
{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) && (
|
||||
<>
|
||||
<ListItem
|
||||
|
@ -8,7 +8,12 @@ import { mapToRepo } from 'utilities/objectModels';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { useNavigate, createSearchParams } from 'react-router-dom';
|
||||
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 NoDataComponent from 'components/Shared/NoDataComponent';
|
||||
|
||||
@ -89,6 +94,8 @@ function Home() {
|
||||
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
|
||||
const [bookmarkData, setBookmarkData] = useState([]);
|
||||
const [isLoadingBookmarks, setIsLoadingBookmarks] = useState(true);
|
||||
const [starData, setStarData] = useState([]);
|
||||
const [isLoadingStars, setIsLoadingStars] = useState(true);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
@ -185,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(() => {
|
||||
window.scrollTo(0, 0);
|
||||
setIsLoading(true);
|
||||
getPopularData();
|
||||
getRecentData();
|
||||
getBookmarks();
|
||||
getStars();
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
@ -203,9 +242,11 @@ function Home() {
|
||||
const isNoData = () =>
|
||||
!isLoading &&
|
||||
!isLoadingBookmarks &&
|
||||
!isLoadingStars &&
|
||||
!isLoadingPopular &&
|
||||
!isLoadingRecent &&
|
||||
bookmarkData.length === 0 &&
|
||||
starData.length === 0 &&
|
||||
popularData.length === 0 &&
|
||||
recentData.length === 0;
|
||||
|
||||
@ -219,9 +260,11 @@ function Home() {
|
||||
version={item.latestVersion}
|
||||
description={item.description}
|
||||
downloads={item.downloads}
|
||||
stars={item.stars}
|
||||
isSigned={item.isSigned}
|
||||
signatureInfo={item.signatureInfo}
|
||||
isBookmarked={item.isBookmarked}
|
||||
isStarred={item.isStarred}
|
||||
vendor={item.vendor}
|
||||
platforms={item.platforms}
|
||||
key={index}
|
||||
@ -294,6 +337,27 @@ function Home() {
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
@ -14,6 +14,8 @@ import { useParams, useNavigate, createSearchParams } from 'react-router-dom';
|
||||
import { Card, CardContent, CardMedia, Chip, Grid, Stack, Tooltip, Typography, IconButton } from '@mui/material';
|
||||
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
// placeholder images
|
||||
@ -195,6 +197,10 @@ function RepoDetails() {
|
||||
};
|
||||
}, [name]);
|
||||
|
||||
const handleDeleteTag = (removed) => {
|
||||
setTags((prevState) => prevState.filter((tag) => tag.tag !== removed));
|
||||
};
|
||||
|
||||
const handlePlatformChipClick = (event) => {
|
||||
const { textContent } = event.target;
|
||||
event.stopPropagation();
|
||||
@ -221,7 +227,7 @@ function RepoDetails() {
|
||||
|
||||
const handleBookmarkClick = () => {
|
||||
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
|
||||
if (response.status === 200) {
|
||||
if (response && response.status === 200) {
|
||||
setRepoDetailData((prevState) => ({
|
||||
...prevState,
|
||||
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 = () => {
|
||||
return `${repoDetailData.newestTag?.Vendor || 'Vendor not available'} •`;
|
||||
};
|
||||
@ -276,15 +293,26 @@ function RepoDetails() {
|
||||
signatureInfo={repoDetailData.signatureInfo}
|
||||
/>
|
||||
</Stack>
|
||||
{isAuthenticated() && (
|
||||
<IconButton component="span" onClick={handleBookmarkClick} data-testid="bookmark-button">
|
||||
{repoDetailData?.isBookmarked ? (
|
||||
<BookmarkIcon data-testid="bookmarked" />
|
||||
) : (
|
||||
<BookmarkBorderIcon data-testid="not-bookmarked" />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
<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>
|
||||
<Typography gutterBottom className={classes.repoTitle}>
|
||||
{repoDetailData?.title || 'Title not available'}
|
||||
@ -317,7 +345,7 @@ function RepoDetails() {
|
||||
<Grid item xs={12} md={8} className={classes.tags}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent className={classes.tagsContent}>
|
||||
<Tags tags={tags} />
|
||||
<Tags tags={tags} repoName={name} onTagDelete={handleDeleteTag} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
@ -43,7 +43,7 @@ const useStyles = makeStyles(() => ({
|
||||
|
||||
export default function Tags(props) {
|
||||
const classes = useStyles();
|
||||
const { tags } = props;
|
||||
const { tags, repoName, onTagDelete } = props;
|
||||
const [tagsFilter, setTagsFilter] = useState('');
|
||||
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
|
||||
|
||||
@ -63,6 +63,9 @@ export default function Tags(props) {
|
||||
lastUpdated={tag.lastUpdated}
|
||||
vendor={tag.vendor}
|
||||
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>
|
||||
);
|
||||
}
|
@ -28,6 +28,8 @@ import {
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||
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';
|
||||
|
||||
// placeholder images
|
||||
@ -183,17 +185,24 @@ function RepoCard(props) {
|
||||
platforms,
|
||||
description,
|
||||
downloads,
|
||||
stars,
|
||||
isSigned,
|
||||
signatureInfo,
|
||||
lastUpdated,
|
||||
version,
|
||||
vulnerabilityData,
|
||||
isBookmarked
|
||||
isBookmarked,
|
||||
isStarred
|
||||
} = props;
|
||||
|
||||
// keep a local bookmark state to display in the ui dynamically on updates
|
||||
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 = () => {
|
||||
navigate(`/image/${encodeURIComponent(name)}`);
|
||||
};
|
||||
@ -215,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 filteredPlatforms = uniq(platforms?.flatMap((platform) => [platform.Os, platform.Arch]));
|
||||
const hiddenChips = filteredPlatforms.length - MAX_PLATFORM_CHIPS;
|
||||
@ -260,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 (
|
||||
<Card variant="outlined" className={classes.card} data-testid="repo-card">
|
||||
<CardActionArea
|
||||
@ -337,6 +373,15 @@ function RepoCard(props) {
|
||||
#1
|
||||
</Typography>
|
||||
</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 item>{renderBookmark()}</Grid>
|
||||
</Grid>
|
||||
|
@ -6,6 +6,7 @@ import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import transform from 'utilities/transform';
|
||||
import { DateTime } from 'luxon';
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||
import DeleteTag from 'components/Shared/DeleteTag';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
@ -78,9 +79,9 @@ const useStyles = makeStyles((theme) => ({
|
||||
}));
|
||||
|
||||
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 classes = useStyles();
|
||||
|
||||
const lastDate = lastUpdated
|
||||
@ -99,9 +100,12 @@ export default function TagCard(props) {
|
||||
return (
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent className={classes.content}>
|
||||
<Typography variant="body1" align="left" className={classes.tagHeading}>
|
||||
Tag
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={2} justifyContent="space-between">
|
||||
<Typography variant="body1" align="left" className={classes.tagHeading}>
|
||||
Tag
|
||||
</Typography>
|
||||
{isDeletable && <DeleteTag repo={repo} tag={tag} onTagDelete={onTagDelete} />}
|
||||
</Stack>
|
||||
<Typography variant="body1" align="left" className={classes.tagName} onClick={() => goToTags()}>
|
||||
{repoName && `${repoName}:`}
|
||||
{tag}
|
||||
|
@ -4,14 +4,28 @@ import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { api, endpoints } from '../../../api';
|
||||
|
||||
// components
|
||||
import { Stack, Typography, InputBase } from '@mui/material';
|
||||
import {
|
||||
IconButton,
|
||||
Stack,
|
||||
Typography,
|
||||
InputBase,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
Snackbar,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../../../host';
|
||||
import { debounce, isEmpty } from 'lodash';
|
||||
import Loading from '../../Shared/Loading';
|
||||
import { mapCVEInfo } from 'utilities/objectModels';
|
||||
import { mapCVEInfo, mapAllCVEInfo } from 'utilities/objectModels';
|
||||
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||
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 VulnerabilitiyCard from '../../Shared/VulnerabilityCard';
|
||||
|
||||
@ -40,6 +54,13 @@ const useStyles = makeStyles((theme) => ({
|
||||
fontSize: '1.4rem',
|
||||
fontWeight: '600'
|
||||
},
|
||||
vulnerabilities: {
|
||||
position: 'relative',
|
||||
maxWidth: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
search: {
|
||||
position: 'relative',
|
||||
maxWidth: '100%',
|
||||
@ -65,13 +86,25 @@ const useStyles = makeStyles((theme) => ({
|
||||
'&::placeholder': {
|
||||
opacity: '1'
|
||||
}
|
||||
},
|
||||
export: {
|
||||
alignContent: 'right'
|
||||
},
|
||||
popper: {
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
padding: '0.3rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
}));
|
||||
|
||||
function VulnerabilitiesDetails(props) {
|
||||
const classes = useStyles();
|
||||
const [cveData, setCveData] = useState([]);
|
||||
const [allCveData, setAllCveData] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoadingAllCve, setIsLoadingAllCve] = useState(true);
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
const { name, tag, digest, platform } = props;
|
||||
|
||||
@ -81,6 +114,9 @@ function VulnerabilitiesDetails(props) {
|
||||
const [isEndOfList, setIsEndOfList] = useState(false);
|
||||
const listBottom = useRef(null);
|
||||
|
||||
const [anchorExport, setAnchorExport] = useState(null);
|
||||
const openExport = Boolean(anchorExport);
|
||||
|
||||
const getCVERequestName = () => {
|
||||
return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;
|
||||
};
|
||||
@ -114,6 +150,24 @@ function VulnerabilitiesDetails(props) {
|
||||
});
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
setIsLoading(true);
|
||||
setIsEndOfList(false);
|
||||
@ -124,11 +178,39 @@ 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 + '_' + 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 { value } = e.target;
|
||||
setCveFilter(value);
|
||||
};
|
||||
|
||||
const handleClickExport = (event) => {
|
||||
setAnchorExport(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseExport = () => {
|
||||
setAnchorExport(null);
|
||||
};
|
||||
|
||||
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
|
||||
|
||||
useEffect(() => {
|
||||
@ -172,6 +254,12 @@ function VulnerabilitiesDetails(props) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (openExport && isEmpty(allCveData)) {
|
||||
getAllCVEs();
|
||||
}
|
||||
}, [openExport]);
|
||||
|
||||
const renderCVEs = () => {
|
||||
return !isEmpty(cveData) ? (
|
||||
cveData.map((cve, index) => {
|
||||
@ -194,9 +282,53 @@ function VulnerabilitiesDetails(props) {
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing="1rem" data-testid="vulnerability-container">
|
||||
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
||||
Vulnerabilities
|
||||
</Typography>
|
||||
<Stack className={classes.vulnerabilities}>
|
||||
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
||||
Vulnerabilities
|
||||
</Typography>
|
||||
<IconButton disableRipple onClick={handleClickExport} className={classes.export}>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
<Snackbar
|
||||
open={openExport && isLoadingAllCve}
|
||||
message="Getting your data ready for export"
|
||||
action={<CircularProgress size="2rem" sx={{ color: '#FFFFFF' }} />}
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
MS Excel
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
<Stack className={classes.search}>
|
||||
<InputBase
|
||||
placeholder={'Search'}
|
||||
|
@ -6,6 +6,10 @@ const osFilters = [
|
||||
{
|
||||
label: 'linux',
|
||||
value: 'linux'
|
||||
},
|
||||
{
|
||||
label: 'freebsd',
|
||||
value: 'freebsd'
|
||||
}
|
||||
];
|
||||
|
||||
@ -19,6 +23,11 @@ const imageFilters = [
|
||||
label: 'Bookmarks',
|
||||
value: 'IsBookmarked',
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
label: 'Starred Repositories',
|
||||
value: 'IsStarred',
|
||||
type: 'boolean'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -15,6 +15,7 @@ const mapToRepo = (responseRepo) => {
|
||||
logo: responseRepo.NewestImage?.Logo,
|
||||
lastUpdated: responseRepo.LastUpdated,
|
||||
downloads: responseRepo.DownloadCount,
|
||||
stars: responseRepo.StarCount,
|
||||
vulnerabiltySeverity: responseRepo.NewestImage?.Vulnerabilities?.MaxSeverity,
|
||||
vulnerabilityCount: responseRepo.NewestImage?.Vulnerabilities?.Count
|
||||
};
|
||||
@ -33,6 +34,7 @@ const mapToRepoFromRepoInfo = (responseRepoInfo) => {
|
||||
title: responseRepoInfo.Summary?.NewestImage?.Title,
|
||||
source: responseRepoInfo.Summary?.NewestImage?.Source,
|
||||
downloads: responseRepoInfo.Summary?.NewestImage?.DownloadCount,
|
||||
stars: responseRepoInfo.Summary?.NewestImage?.StarCount,
|
||||
overview: responseRepoInfo.Summary?.NewestImage?.Documentation,
|
||||
license: responseRepoInfo.Summary?.NewestImage?.Licenses,
|
||||
vulnerabilitySeverity: responseRepoInfo.Summary?.NewestImage?.Vulnerabilities?.MaxSeverity,
|
||||
@ -53,6 +55,7 @@ const mapToImage = (responseImage) => {
|
||||
referrers: responseImage.Referrers,
|
||||
size: responseImage.Size,
|
||||
downloadCount: responseImage.DownloadCount,
|
||||
starCount: responseImage.StarCount,
|
||||
lastUpdated: responseImage.LastUpdated,
|
||||
description: responseImage.Description,
|
||||
isSigned: responseImage.IsSigned,
|
||||
@ -66,6 +69,7 @@ const mapToImage = (responseImage) => {
|
||||
authors: responseImage.Authors,
|
||||
vulnerabiltySeverity: responseImage.Vulnerabilities?.MaxSeverity,
|
||||
vulnerabilityCount: responseImage.Vulnerabilities?.Count,
|
||||
isDeletable: responseImage.IsDeletable,
|
||||
// frontend only prop to increase interop with Repo objects and code reusability
|
||||
name: `${responseImage.RepoName}:${responseImage.Tag}`
|
||||
};
|
||||
@ -79,6 +83,7 @@ const mapToManifest = (responseManifest) => {
|
||||
size: responseManifest.Size,
|
||||
platform: responseManifest.Platform,
|
||||
downloadCount: responseManifest.DownloadCount,
|
||||
starCount: responseManifest.StarCount,
|
||||
layers: responseManifest.Layers,
|
||||
history: responseManifest.History,
|
||||
vulnerabilities: responseManifest.Vulnerabilities
|
||||
@ -97,6 +102,24 @@ const mapCVEInfo = (cveInfo) => {
|
||||
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,
|
||||
packageInstalledVersion: packageInfo.InstalledVersion,
|
||||
packageFixedVersion: packageInfo.FixedVersion
|
||||
};
|
||||
});
|
||||
});
|
||||
return cveList;
|
||||
};
|
||||
|
||||
const mapSignatureInfo = (signatureInfo) => {
|
||||
return signatureInfo
|
||||
? {
|
||||
@ -119,4 +142,4 @@ const mapReferrer = (referrer) => ({
|
||||
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_RECENT_PAGE_SIZE = 2;
|
||||
const HOME_BOOKMARKS_PAGE_SIZE = 2;
|
||||
const HOME_STARS_PAGE_SIZE = 2;
|
||||
const CVE_FIXEDIN_PAGE_SIZE = 5;
|
||||
|
||||
export {
|
||||
@ -13,5 +14,6 @@ export {
|
||||
CVE_FIXEDIN_PAGE_SIZE,
|
||||
HOME_POPULAR_PAGE_SIZE,
|
||||
HOME_RECENT_PAGE_SIZE,
|
||||
HOME_BOOKMARKS_PAGE_SIZE
|
||||
HOME_BOOKMARKS_PAGE_SIZE,
|
||||
HOME_STARS_PAGE_SIZE
|
||||
};
|
||||
|
@ -148,10 +148,7 @@ const NoneVulnerabilityChip = () => {
|
||||
label="None"
|
||||
sx={{ backgroundColor: '#E8F5E9', color: '#388E3C', fontSize: '0.8125rem' }}
|
||||
variant="filled"
|
||||
onDelete={() => {
|
||||
return;
|
||||
}}
|
||||
deleteIcon={<OutlinedBugIcon sx={{ color: '#388E3C!important' }} />}
|
||||
icon={<OutlinedBugIcon sx={{ color: '#388E3C!important' }} />}
|
||||
data-testid="none-vulnerability-chip"
|
||||
/>
|
||||
);
|
||||
@ -162,10 +159,7 @@ const UnknownVulnerabilityChip = () => {
|
||||
label="Unknown"
|
||||
sx={{ backgroundColor: '#ECEFF1', color: '#52637A', fontSize: '0.8125rem' }}
|
||||
variant="filled"
|
||||
onDelete={() => {
|
||||
return;
|
||||
}}
|
||||
deleteIcon={<OutlinedBugIcon sx={{ color: '#52637A!important' }} />}
|
||||
icon={<OutlinedBugIcon sx={{ color: '#52637A!important' }} />}
|
||||
data-testid="unknown-vulnerability-chip"
|
||||
/>
|
||||
);
|
||||
@ -176,10 +170,7 @@ const FailedScanChip = () => {
|
||||
label="Failed to scan"
|
||||
sx={{ backgroundColor: '#848484', color: '#F6F7F9', fontSize: '0.8125rem' }}
|
||||
variant="filled"
|
||||
onDelete={() => {
|
||||
return;
|
||||
}}
|
||||
deleteIcon={<SvgIcon component={failedScanBug} sx={{ color: '#F6F7F9!important' }} />}
|
||||
icon={<SvgIcon component={failedScanBug} sx={{ color: '#F6F7F9!important' }} />}
|
||||
data-testid="failed-vulnerability-chip"
|
||||
/>
|
||||
);
|
||||
@ -190,10 +181,7 @@ const LowVulnerabilityChip = () => {
|
||||
label="Low"
|
||||
sx={{ backgroundColor: '#FFF3E0', color: '#FB8C00', fontSize: '0.8125rem' }}
|
||||
variant="filled"
|
||||
onDelete={() => {
|
||||
return;
|
||||
}}
|
||||
deleteIcon={<OutlinedBugIcon sx={{ color: '#FB8C00!important' }} />}
|
||||
icon={<OutlinedBugIcon sx={{ color: '#FB8C00!important' }} />}
|
||||
data-testid="low-vulnerability-chip"
|
||||
/>
|
||||
);
|
||||
@ -204,10 +192,7 @@ const MediumVulnerabilityChip = () => {
|
||||
label="Medium"
|
||||
sx={{ backgroundColor: '#FFF3E0', color: '#FB8C00', fontSize: '0.8125rem' }}
|
||||
variant="filled"
|
||||
onDelete={() => {
|
||||
return;
|
||||
}}
|
||||
deleteIcon={<FilledBugIcon sx={{ color: '#FB8C00!important' }} />}
|
||||
icon={<FilledBugIcon sx={{ color: '#FB8C00!important' }} />}
|
||||
data-testid="medium-vulnerability-chip"
|
||||
/>
|
||||
);
|
||||
@ -218,10 +203,7 @@ const HighVulnerabilityChip = () => {
|
||||
label="High"
|
||||
sx={{ backgroundColor: '#FEEBEE', color: '#E53935', fontSize: '0.8125rem' }}
|
||||
variant="filled"
|
||||
onDelete={() => {
|
||||
return;
|
||||
}}
|
||||
deleteIcon={<OutlinedBugIcon sx={{ color: '#E53935!important' }} />}
|
||||
icon={<OutlinedBugIcon sx={{ color: '#E53935!important' }} />}
|
||||
data-testid="high-vulnerability-chip"
|
||||
/>
|
||||
);
|
||||
@ -232,10 +214,7 @@ const CriticalVulnerabilityChip = () => {
|
||||
label="Critical"
|
||||
sx={{ backgroundColor: '#FEEBEE', color: '#E53935', fontSize: '0.8125rem' }}
|
||||
variant="filled"
|
||||
onDelete={() => {
|
||||
return;
|
||||
}}
|
||||
deleteIcon={<FilledBugIcon sx={{ color: '#E53935!important' }} />}
|
||||
icon={<FilledBugIcon sx={{ color: '#E53935!important' }} />}
|
||||
data-testid="critical-vulnerability-chip"
|
||||
/>
|
||||
);
|
||||
|
@ -76,8 +76,14 @@ test.describe('explore page test', () => {
|
||||
|
||||
await expect(exploreFirst).toBeVisible({ timeout: 250000 });
|
||||
|
||||
const windowsFilter = page.getByRole('checkbox', { name: 'windows' });
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
@ -17,13 +17,13 @@ const pageSizes = {
|
||||
};
|
||||
|
||||
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) =>
|
||||
`/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) =>
|
||||
`/v2/_zot/ext/search?query={GlobalSearch(query:%22${searchTerm}%22,%20requestedPage:%20{limit:${pageSize}%20offset:${
|
||||
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%20SignatureInfo%20{%20Tool%20IsTrusted%20Author%20}%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) =>
|
||||
`/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}}`
|
||||
};
|
||||
|
Reference in New Issue
Block a user