15 Commits

Author SHA1 Message Date
Andreea-Lupu
2e1e2e92b7 fix: show a loading message while waiting for a response
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2023-12-11 13:14:30 -08:00
Andreea Lupu
d9370fb9c1 feat: starred repos implementation (#399)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2023-12-07 16:36:54 +02:00
Andrei Aaron
e97e04eee5 ci: dco job should run only on PRs (#396)
See also message in b919279eef

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-11-28 11:23:15 +02:00
Andreea Lupu
a288523a3f feat: vulnerability chips - show icon before string (#392)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2023-11-28 10:51:23 +02:00
Andrei Aaron
fad5572db4 feat: add prefix zot to /auth urls (#389)
See: https://github.com/project-zot/zot/issues/1883

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-10-20 13:17:24 +03:00
Ramkumar Chinchani
19e366ee1f fix: use the official icon
Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>
2023-10-11 19:56:02 -07:00
Raul-Cristian Kele
b41fb2f841 patch: update nodata display on homepage
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-10-02 14:16:15 -07:00
Raul Kele
b787273b84 fix: fixed display of new signature tooltips in some cases (#379)
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-29 19:46:13 +03:00
Andrei Aaron
9ecd46e4d0 ci(end-to-end): Fix a few issues with the workflow (#380)
- free up disk space before running tests
- remove uneeded call to /v2/_catalog.
- add a check to make sure the images are scanned for CVEs before tests start

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-08-29 19:09:52 +03:00
Raul Kele
845726cd08 feat: Update signature integration to display extra info (#378)
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-28 18:03:32 +03:00
Damien Degois
ac84c375c0 feat: add customizable generic oidc login button
Rebased and modified to reflect https://github.com/project-zot/zot/pull/1691 conclusion

Signed-off-by: Damien Degois <damien@degois.info>
2023-08-28 15:15:17 +03:00
Raul-Cristian Kele
96008d67be feat: Implement no data component
- Implement customizeable component for no data display
- Added component to homepage

Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-18 20:09:28 +03:00
Raul-Cristian Kele
087b42693f patch: update integration tests
Signed-off-by: raulkele <raulkeleblk@gmail.com>
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-15 20:12:30 +03:00
Raul-Cristian Kele
8f4c23bf40 fix: Update tooltip for vulnerability chips
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-08 21:33:22 +03:00
Raul-Cristian Kele
54c764c996 patch: update cve api usage
- updated CVEListForImage api calls
- updated ImageListWithCVEFixed api calls
- now cves are shown for specific tag
- fixed tags now only shows tags that match platform with current digest
- moved platform selector on tagdetails page

Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-07-27 08:51:27 +03:00
32 changed files with 601 additions and 211 deletions

View File

@@ -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

View File

@@ -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:
@@ -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

View File

@@ -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);
});
}); });

View File

@@ -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()
});
}); });
}); });

View File

@@ -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();
});
}); });

View File

@@ -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}

View File

@@ -1,11 +1,11 @@
import axios from 'axios'; import axios from 'axios';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { sortByCriteria } from 'utilities/sortCriteria'; import { sortByCriteria } from 'utilities/sortCriteria';
import { logoutUser } from 'utilities/authUtilities'; import { isAuthenticationEnabled, logoutUser } from 'utilities/authUtilities';
import { host } from 'host'; import { host } from 'host';
axios.interceptors.request.use((config) => { axios.interceptors.request.use((config) => {
if (config.url.includes(endpoints.authConfig)) { if (config.url.includes(endpoints.authConfig) || !isAuthenticationEnabled()) {
config.withCredentials = false; config.withCredentials = false;
} else { } else {
config.headers['X-ZOT-API-CLIENT'] = 'zot-ui'; config.headers['X-ZOT-API-CLIENT'] = 'zot-ui';
@@ -79,16 +79,16 @@ 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`,
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 } 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 = '') => {
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
@@ -98,18 +98,26 @@ const endpoints = {
} }
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`; return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`;
}, },
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }) => imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }, filter = {}) => {
`/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${ let filterParam = '';
if (filter.Os || filter.Arch) {
filterParam = `,filter:{`;
if (filter.Os) filterParam += ` Os:${!isEmpty(filter.Os) ? `${JSON.stringify(filter.Os)}` : '""'}`;
if (filter.Arch) filterParam += ` Arch:${!isEmpty(filter.Arch) ? `${JSON.stringify(filter.Arch)}` : '""'}`;
filterParam += '}';
}
return `/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize (pageNumber - 1) * pageSize
}}) {Page {TotalCount ItemCount} Results {Tag}}}`, }}${filterParam}) {Page {TotalCount ItemCount} Results {Tag}}}`;
},
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,
@@ -126,9 +134,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:""`;
@@ -137,7 +146,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
View 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

View File

@@ -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}

View File

@@ -295,12 +295,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

View File

@@ -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;

View File

@@ -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,7 +309,7 @@ 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 && <Divider className={classes.divider}>or</Divider>}

View File

@@ -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 };

View File

@@ -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 { 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 StarIcon from '@mui/icons-material/Star';
import StarBorderIcon from '@mui/icons-material/StarBorder';
import makeStyles from '@mui/styles/makeStyles'; import makeStyles from '@mui/styles/makeStyles';
// placeholder images // placeholder images
@@ -230,6 +232,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 +284,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'}

View 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;

View File

@@ -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>

View 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;

View File

@@ -72,7 +72,7 @@ const useStyles = makeStyles((theme) => ({
})); }));
function VulnerabilitiyCard(props) { function VulnerabilitiyCard(props) {
const classes = useStyles(); const classes = useStyles();
const { cve, name } = props; const { cve, name, platform } = props;
const [openDesc, setOpenDesc] = useState(false); const [openDesc, setOpenDesc] = useState(false);
const [openFixed, setOpenFixed] = useState(false); const [openFixed, setOpenFixed] = useState(false);
const [loadingFixed, setLoadingFixed] = useState(true); const [loadingFixed, setLoadingFixed] = useState(true);
@@ -90,7 +90,12 @@ function VulnerabilitiyCard(props) {
setLoadingFixed(true); setLoadingFixed(true);
api api
.get( .get(
`${host()}${endpoints.imageListWithCVEFixed(cve.id, name, { pageNumber, pageSize: CVE_FIXEDIN_PAGE_SIZE })}`, `${host()}${endpoints.imageListWithCVEFixed(
cve.id,
name,
{ pageNumber, pageSize: CVE_FIXEDIN_PAGE_SIZE },
platform ? { Os: platform.Os, Arch: platform.Arch } : {}
)}`,
abortController.signal abortController.signal
) )
.then((response) => { .then((response) => {

View File

@@ -73,7 +73,7 @@ function VulnerabilitiesDetails(props) {
const [cveData, setCveData] = useState([]); const [cveData, setCveData] = useState([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const abortController = useMemo(() => new AbortController(), []); const abortController = useMemo(() => new AbortController(), []);
const { name, tag } = props; const { name, tag, digest, platform } = props;
// pagination props // pagination props
const [cveFilter, setCveFilter] = useState(''); const [cveFilter, setCveFilter] = useState('');
@@ -81,11 +81,15 @@ function VulnerabilitiesDetails(props) {
const [isEndOfList, setIsEndOfList] = useState(false); const [isEndOfList, setIsEndOfList] = useState(false);
const listBottom = useRef(null); const listBottom = useRef(null);
const getCVERequestName = () => {
return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;
};
const getPaginatedCVEs = () => { const getPaginatedCVEs = () => {
api api
.get( .get(
`${host()}${endpoints.vulnerabilitiesForRepo( `${host()}${endpoints.vulnerabilitiesForRepo(
`${name}:${tag}`, getCVERequestName(),
{ pageNumber, pageSize: EXPLORE_PAGE_SIZE }, { pageNumber, pageSize: EXPLORE_PAGE_SIZE },
cveFilter cveFilter
)}`, )}`,
@@ -171,7 +175,7 @@ function VulnerabilitiesDetails(props) {
const renderCVEs = () => { const renderCVEs = () => {
return !isEmpty(cveData) ? ( return !isEmpty(cveData) ? (
cveData.map((cve, index) => { cveData.map((cve, index) => {
return <VulnerabilitiyCard key={index} cve={cve} name={name} />; return <VulnerabilitiyCard key={index} cve={cve} name={name} platform={platform} />;
}) })
) : ( ) : (
<div>{!isLoading && <Typography className={classes.none}> No Vulnerabilities </Typography>}</div> <div>{!isLoading && <Typography className={classes.none}> No Vulnerabilities </Typography>}</div>

View File

@@ -59,7 +59,6 @@ const useStyles = makeStyles((theme) => ({
fontSize: '1rem', fontSize: '1rem',
lineHeight: '1.5rem', lineHeight: '1.5rem',
color: '#52637A', color: '#52637A',
padding: '1rem 0 0 0',
maxWidth: '100%', maxWidth: '100%',
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {
padding: '0.5rem 0 0 0', padding: '0.5rem 0 0 0',
@@ -209,7 +208,14 @@ function TagDetails() {
case 'IsDependentOn': case 'IsDependentOn':
return <IsDependentOn name={imageDetailData.name} digest={selectedManifest.digest} />; return <IsDependentOn name={imageDetailData.name} digest={selectedManifest.digest} />;
case 'Vulnerabilities': case 'Vulnerabilities':
return <VulnerabilitiesDetails name={reponame} tag={tag} />; return (
<VulnerabilitiesDetails
name={reponame}
tag={tag}
digest={selectedManifest?.digest}
platform={selectedManifest.platform}
/>
);
case 'ReferredBy': case 'ReferredBy':
return <ReferredBy referrers={imageDetailData.referrers} />; return <ReferredBy referrers={imageDetailData.referrers} />;
default: default:
@@ -227,10 +233,10 @@ function TagDetails() {
<Card className={classes.cardRoot}> <Card className={classes.cardRoot}>
<CardContent className={classes.cardContent}> <CardContent className={classes.cardContent}>
<Grid container> <Grid container>
<Grid item xs={12} md={8} className={classes.header}> <Grid item xs={12} md={9} className={classes.header}>
<Stack <Stack
alignItems="center" alignItems="center"
sx={{ width: { xs: '100%', md: 'auto' } }} sx={{ width: { xs: '100%', md: 'auto' }, marginBottom: '1rem' }}
direction={{ xs: 'column', md: 'row' }} direction={{ xs: 'column', md: 'row' }}
spacing={1} spacing={1}
> >
@@ -254,32 +260,34 @@ function TagDetails() {
vulnerabilitySeverity={imageDetailData.vulnerabiltySeverity} vulnerabilitySeverity={imageDetailData.vulnerabiltySeverity}
count={imageDetailData.vulnerabilityCount} count={imageDetailData.vulnerabilityCount}
/> />
<SignatureIconCheck isSigned={imageDetailData.isSigned} /> <SignatureIconCheck
</Stack> isSigned={imageDetailData.isSigned}
signatureInfo={imageDetailData.signatureInfo}
<Stack sx={{ width: { xs: '100%', md: 'auto' } }}> />
<FormControl sx={{ m: '1', minWidth: '4.6875rem' }} className={classes.sortForm} size="small">
<InputLabel>OS/Arch</InputLabel>
{!isEmpty(selectedManifest) && (
<Select
label="OS/Arch"
value={selectedManifest}
onChange={handleOSArchChange}
MenuProps={{ disableScrollLock: true }}
>
{imageDetailData.manifests.map((el) => (
<MenuItem key={el.digest} value={el}>
{`${el.platform?.Os}/${el.platform?.Arch}`}
</MenuItem>
))}
</Select>
)}
</FormControl>
</Stack> </Stack>
</Stack> </Stack>
<Typography gutterBottom className={classes.digest}> <Stack direction="row" alignItems="center" spacing="1rem">
Digest: {selectedManifest?.digest} <FormControl sx={{ m: '1', minWidth: '4.6875rem' }} className={classes.sortForm} size="small">
</Typography> <InputLabel>OS/Arch</InputLabel>
{!isEmpty(selectedManifest) && (
<Select
label="OS/Arch"
value={selectedManifest}
onChange={handleOSArchChange}
MenuProps={{ disableScrollLock: true }}
>
{imageDetailData.manifests.map((el) => (
<MenuItem key={el.digest} value={el}>
{`${el.platform?.Os}/${el.platform?.Arch}`}
</MenuItem>
))}
</Select>
)}
</FormControl>
<Typography gutterBottom className={classes.digest}>
Digest: {selectedManifest?.digest}
</Typography>
</Stack>
</Grid> </Grid>
</Grid> </Grid>
</CardContent> </CardContent>

View File

@@ -19,6 +19,11 @@ const imageFilters = [
label: 'Bookmarks', label: 'Bookmarks',
value: 'IsBookmarked', value: 'IsBookmarked',
type: 'boolean' type: 'boolean'
},
{
label: 'Starred Repositories',
value: 'IsStarred',
type: 'boolean'
} }
]; ];

View File

@@ -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,
@@ -76,6 +82,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
@@ -94,6 +101,20 @@ const mapCVEInfo = (cveInfo) => {
return cveList; 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,

View File

@@ -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
}; };

View File

@@ -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} />;
} }
}; };

View File

@@ -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" />,
@@ -23,7 +24,7 @@ const VerifiedShieldIcon = createSvgIcon(
const NoneVulnerabilityIcon = ({ vulnerabilityStringTitle }) => { const NoneVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
return ( return (
<Tooltip title={`${vulnerabilityStringTitle} Vulnerability`} placement="top"> <Tooltip title={`${vulnerabilityStringTitle}`} placement="top">
<OutlinedBugIcon <OutlinedBugIcon
sx={{ sx={{
color: '#43A047!important', color: '#43A047!important',
@@ -40,7 +41,7 @@ const NoneVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
}; };
const UnknownVulnerabilityIcon = ({ vulnerabilityStringTitle }) => { const UnknownVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
return ( return (
<Tooltip title={`${vulnerabilityStringTitle} Vulnerability`} placement="top"> <Tooltip title={`${vulnerabilityStringTitle}`} placement="top">
<OutlinedBugIcon <OutlinedBugIcon
sx={{ sx={{
color: '#52637A', color: '#52637A',
@@ -75,7 +76,7 @@ const FailedScanIcon = () => {
}; };
const LowVulnerabilityIcon = ({ vulnerabilityStringTitle }) => { const LowVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
return ( return (
<Tooltip title={`${vulnerabilityStringTitle} Vulnerability`} placement="top"> <Tooltip title={`${vulnerabilityStringTitle}`} placement="top">
<OutlinedBugIcon <OutlinedBugIcon
sx={{ sx={{
color: '#FB8C00', color: '#FB8C00',
@@ -92,7 +93,7 @@ const LowVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
}; };
const MediumVulnerabilityIcon = ({ vulnerabilityStringTitle }) => { const MediumVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
return ( return (
<Tooltip title={`${vulnerabilityStringTitle} Vulnerability`} placement="top"> <Tooltip title={`${vulnerabilityStringTitle}`} placement="top">
<FilledBugIcon <FilledBugIcon
sx={{ sx={{
color: '#FB8C00', color: '#FB8C00',
@@ -109,7 +110,7 @@ const MediumVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
}; };
const HighVulnerabilityIcon = ({ vulnerabilityStringTitle }) => { const HighVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
return ( return (
<Tooltip title={`${vulnerabilityStringTitle} Vulnerability`} placement="top"> <Tooltip title={`${vulnerabilityStringTitle}`} placement="top">
<OutlinedBugIcon <OutlinedBugIcon
sx={{ sx={{
color: '#E53935', color: '#E53935',
@@ -126,7 +127,7 @@ const HighVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
}; };
const CriticalVulnerabilityIcon = ({ vulnerabilityStringTitle }) => { const CriticalVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
return ( return (
<Tooltip title={`${vulnerabilityStringTitle} Vulnerability`} placement="top"> <Tooltip title={`${vulnerabilityStringTitle}`} placement="top">
<FilledBugIcon <FilledBugIcon
sx={{ sx={{
color: '#E53935', color: '#E53935',
@@ -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={{

View File

@@ -7,7 +7,7 @@ import { hosts, endpoints, sortCriteria } from './values/test-constants';
test.describe('explore page test', () => { test.describe('explore page test', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.addInitScript(() => { await page.addInitScript(() => {
window.localStorage.setItem('token', '-'); window.localStorage.setItem('authConfig', '{}');
}); });
}); });

View File

@@ -5,7 +5,7 @@ import { hosts, endpoints, sortCriteria } from './values/test-constants';
test.describe('homepage test', () => { test.describe('homepage test', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.addInitScript(() => { await page.addInitScript(() => {
window.localStorage.setItem('token', '-'); window.localStorage.setItem('authConfig', '{}');
}); });
}); });

View File

@@ -5,7 +5,7 @@ import { getRepoListOrderedAlpha } from './utils/test-data-parser';
test.describe('navbar test', () => { test.describe('navbar test', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.addInitScript(() => { await page.addInitScript(() => {
window.localStorage.setItem('token', '-'); window.localStorage.setItem('authConfig', '{}');
}); });
}); });

View File

@@ -8,7 +8,7 @@ const testRepo = getMultiTagRepo();
test.describe('Repository page test', () => { test.describe('Repository page test', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.addInitScript(() => { await page.addInitScript(() => {
window.localStorage.setItem('token', '-'); window.localStorage.setItem('authConfig', '{}');
}); });
await page.goto(`${hosts.ui}/image/${testRepo.repo}`); await page.goto(`${hosts.ui}/image/${testRepo.repo}`);

View File

@@ -5,7 +5,7 @@ import { hosts, pageSizes } from './values/test-constants';
test.describe('Tag page test', () => { test.describe('Tag page test', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.addInitScript(() => { await page.addInitScript(() => {
window.localStorage.setItem('token', '-'); window.localStorage.setItem('authConfig', '{}');
}); });
}); });

View File

@@ -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%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 };