feat: bookmark implementation

Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Raul Kele 2023-05-12 12:27:06 +03:00
parent 769ffdc60d
commit 05d5f744b0
24 changed files with 447 additions and 130 deletions

View File

@ -33,6 +33,7 @@ const mockImageList = {
Name: 'alpine',
Size: '2806985',
LastUpdated: '2022-08-09T17:19:53.274069586Z',
IsBookmarked: false,
NewestImage: {
Tag: 'latest',
Description: 'w',
@ -44,12 +45,19 @@ const mockImageList = {
MaxSeverity: 'LOW',
Count: 7
}
}
},
Platforms: [
{
Os: 'linux',
Arch: 'amd64'
}
]
},
{
Name: 'mongo',
Size: '231383863',
LastUpdated: '2022-08-02T01:30:49.193203152Z',
IsBookmarked: false,
NewestImage: {
Tag: 'latest',
Description: '',
@ -61,12 +69,19 @@ const mockImageList = {
MaxSeverity: 'HIGH',
Count: 2
}
}
},
Platforms: [
{
Os: 'linux',
Arch: 'amd64'
}
]
},
{
Name: 'node',
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
NewestImage: {
Tag: 'latest',
Description: '',
@ -78,12 +93,19 @@ const mockImageList = {
MaxSeverity: 'CRITICAL',
Count: 10
}
}
},
Platforms: [
{
Os: 'linux',
Arch: 'amd64'
}
]
},
{
Name: 'centos',
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
NewestImage: {
Tag: 'latest',
Description: '',
@ -95,12 +117,19 @@ const mockImageList = {
MaxSeverity: 'NONE',
Count: 10
}
}
},
Platforms: [
{
Os: 'linux',
Arch: 'amd64'
}
]
},
{
Name: 'debian',
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
NewestImage: {
Tag: 'latest',
Description: '',
@ -112,12 +141,23 @@ const mockImageList = {
MaxSeverity: 'MEDIUM',
Count: 10
}
}
},
Platforms: [
{
Os: 'linux',
Arch: 'amd64'
},
{
Os: 'windows',
Arch: 'amd64'
}
]
},
{
Name: 'mysql',
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
NewestImage: {
Tag: 'latest',
Description: '',
@ -129,12 +169,19 @@ const mockImageList = {
MaxSeverity: 'UNKNOWN',
Count: 10
}
}
},
Platforms: [
{
Os: 'linux',
Arch: 'amd64'
}
]
},
{
Name: 'base',
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
NewestImage: {
Tag: 'latest',
Description: '',
@ -146,12 +193,40 @@ const mockImageList = {
MaxSeverity: '',
Count: 10
}
}
},
Platforms: [
{
Os: 'linux',
Arch: 'amd64'
}
]
}
]
}
};
const filteredMockImageListWindows = () => {
const filteredRepos = mockImageList.GlobalSearch.Repos.filter((r) =>
r.Platforms.map((pf) => pf.Os).includes('windows')
);
return {
GlobalSearch: {
Page: { TotalCount: 1, ItemCount: 1 },
Repos: filteredRepos
}
};
};
const filteredMockImageListSigned = () => {
const filteredRepos = mockImageList.GlobalSearch.Repos.filter((r) => r.NewestImage.IsSigned);
return {
GlobalSearch: {
Page: { TotalCount: 6, ItemCount: 6 },
Repos: filteredRepos
}
};
};
beforeEach(() => {
// IntersectionObserver isn't available in test environment
const mockIntersectionObserver = jest.fn();
@ -235,4 +310,28 @@ describe('Explore component', () => {
const filterCheckboxes = await screen.findAllByRole('checkbox');
expect(filterCheckboxes[0]).toBeChecked();
});
it('should filter the images based on filter cards', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
render(<StateExploreWrapper />);
expect(await screen.findAllByTestId('repo-card')).toHaveLength(mockImageList.GlobalSearch.Repos.length);
const windowsCheckbox = (await screen.findAllByRole('checkbox'))[0];
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: filteredMockImageListWindows() } });
await userEvent.click(windowsCheckbox);
expect(windowsCheckbox).toBeChecked();
expect(await screen.findAllByTestId('repo-card')).toHaveLength(1);
const signedCheckboxLabel = await screen.findByText(/signed images/i);
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: filteredMockImageListSigned() } });
await userEvent.click(signedCheckboxLabel);
expect(await screen.findAllByTestId('repo-card')).toHaveLength(6);
});
it('should bookmark a repo if bookmark button is clicked', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
render(<StateExploreWrapper />);
const bookmarkButton = (await screen.findAllByTestId('bookmark-button'))[0];
jest.spyOn(api, 'put').mockResolvedValueOnce({ status: 200, data: {} });
await userEvent.click(bookmarkButton);
expect(await screen.findAllByTestId('bookmarked')).toHaveLength(1);
});
});

View File

@ -122,6 +122,48 @@ const mockImageListRecent = {
}
};
const mockImageListBookmarks = {
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();
});
@ -134,27 +176,27 @@ afterEach(() => {
describe('Home component', () => {
it('fetches image data and renders popular, bookmarks and recently updated', 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').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
render(<HomeWrapper />);
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(2));
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(2));
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(3));
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(3));
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
});
it('renders signature icons', 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').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
render(<HomeWrapper />);
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(2);
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(3);
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(3);
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(4);
});
it('renders vulnerability icons', 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').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
render(<HomeWrapper />);
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(2);
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(2);
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(3);
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(3);
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
});
@ -162,15 +204,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(2));
await waitFor(() => expect(error).toBeCalledTimes(3));
});
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 } });
render(<HomeWrapper />);
const viewAllButtons = await screen.findAllByText(/view all/i);
expect(viewAllButtons).toHaveLength(2);
expect(viewAllButtons).toHaveLength(3);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: [] } });
fireEvent.click(viewAllButtons[0]);
expect(mockedUsedNavigate).toHaveBeenCalledWith({
pathname: `/explore`,
@ -181,5 +225,10 @@ describe('Home component', () => {
pathname: `/explore`,
search: createSearchParams({ sortby: sortByCriteria.updateTime.value }).toString()
});
fireEvent.click(viewAllButtons[2]);
expect(mockedUsedNavigate).toHaveBeenCalledWith({
pathname: `/explore`,
search: createSearchParams({ filter: 'IsBookmarked' }).toString()
});
});
});

View File

@ -4,6 +4,7 @@ import React from 'react';
import { api } from 'api';
import { createSearchParams } from 'react-router-dom';
import MockThemeProvier from '__mocks__/MockThemeProvider';
import userEvent from '@testing-library/user-event';
const RepoDetailsThemeWrapper = () => {
return (
@ -45,6 +46,7 @@ const mockRepoDetailsData = {
LastUpdated: '2023-01-30T15:05:35.420124619Z',
Size: '451554070',
Vendors: ['[The Node.js Docker Team](https://github.com/nodejs/docker-node)\n'],
IsBookmarked: false,
NewestImage: {
RepoName: 'mongo',
IsSigned: true,
@ -298,4 +300,13 @@ describe('Repo details component', () => {
search: createSearchParams({ filter: 'linux' }).toString()
});
});
it('should bookmark a repo if bookmark button is clicked', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockRepoDetailsData } });
render(<RepoDetailsThemeWrapper />);
const bookmarkButton = await screen.findByTestId('bookmark-button');
jest.spyOn(api, 'put').mockResolvedValue({ status: 200, data: {} });
await userEvent.click(bookmarkButton);
expect(await screen.findByTestId('bookmarked')).toBeInTheDocument();
});
});

View File

@ -368,7 +368,51 @@ const mockDependenciesList = {
}
};
const mockDependentsList = mockDependenciesList;
const mockDependentsList = {
data: {
DerivedImageList: {
Page: { ItemCount: 4, TotalCount: 4 },
Results: [
{
RepoName: 'project-stacker/c3/static-ubuntu-amd64',
Tag: 'tag1',
Manifests: [],
Vulnerabilities: {
MaxSeverity: 'HIGH',
Count: 5
}
},
{
RepoName: 'tag2',
Tag: 'tag2',
Manifests: [],
Vulnerabilities: {
MaxSeverity: 'CRITICAL',
Count: 2
}
},
{
RepoName: 'tag3',
Tag: 'tag3',
Manifests: [],
Vulnerabilities: {
MaxSeverity: 'LOW',
Count: 7
}
},
{
RepoName: 'tag4',
Tag: 'tag4',
Manifests: [],
Vulnerabilities: {
MaxSeverity: 'HIGH',
Count: 5
}
}
]
}
}
};
const mockCVEList = {
CVEListForImage: {

View File

@ -24,7 +24,7 @@ const api = {
'Content-Type': 'application/json'
};
const token = localStorage.getItem('token');
if (token) {
if (token && token !== '-') {
const authHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json',
@ -78,9 +78,9 @@ const endpoints = {
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 Documentation Vendor Labels} DownloadCount}}}`,
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned Documentation Vendor Labels} IsStarred IsBookmarked 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 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 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 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 = '') => {
@ -119,9 +119,10 @@ const endpoints = {
if (filter.Os) filterParam += ` Os:${!isEmpty(filter.Os) ? `${JSON.stringify(filter.Os)}` : '""'}`;
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}`;
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 } 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 Licenses Vendor Labels } DownloadCount}}}`;
},
imageSuggestions: ({ searchQuery = '""', pageNumber = 1, pageSize = 15 }) => {
const searchParam = searchQuery !== '' ? `query:"${searchQuery}"` : `query:""`;
@ -129,7 +130,8 @@ const endpoints = {
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam}) {Images {RepoName Tag}}}`;
},
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`
};
export { api, endpoints };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -1,6 +0,0 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.6046 49.1571C19.5123 49.1571 8.66797 47.1782 8.66797 42.8246C8.66797 38.4711 19.5123 36.4922 29.6046 36.4922C39.697 36.4922 50.5413 38.4711 50.5413 42.8246C50.5809 47.1782 39.697 49.1571 29.6046 49.1571ZM29.6046 38.9856C24.4595 38.9856 19.631 39.5001 15.9899 40.45C12.0321 41.479 11.1614 42.5872 11.1614 42.8246C11.1614 43.0621 12.0321 44.1703 15.9899 45.1993C19.5915 46.1492 24.4595 46.6637 29.6046 46.6637C34.7498 46.6637 39.5783 46.1492 43.2194 45.1993C47.1772 44.1703 48.0479 43.0621 48.0479 42.8246C48.0479 42.5872 47.1772 41.479 43.2194 40.45C39.6178 39.5001 34.7498 38.9856 29.6046 38.9856Z" fill="white"/>
<path d="M29.6046 23.5477C19.5123 23.5477 8.66797 21.5688 8.66797 17.2153C8.66797 12.8617 19.5123 10.8828 29.6046 10.8828C39.697 10.8828 50.5413 12.8617 50.5413 17.2153C50.5809 21.5688 39.697 23.5477 29.6046 23.5477ZM29.6046 13.3762C24.4595 13.3762 19.631 13.8907 15.9899 14.8406C12.0321 15.8696 11.1614 16.9778 11.1614 17.2153C11.1614 17.4527 12.0321 18.5609 15.9899 19.5899C19.5915 20.5398 24.4595 21.0543 29.6046 21.0543C34.7498 21.0543 39.5783 20.5398 43.2194 19.5899C47.1772 18.5609 48.0479 17.4527 48.0479 17.2153C48.0479 16.9778 47.1772 15.8696 43.2194 14.8406C39.6178 13.8907 34.7498 13.3762 29.6046 13.3762Z" fill="white"/>
<path d="M47.0194 28.2188C32.7318 44.9602 23.8664 47.2557 21.8875 47.2557C21.7688 47.2557 11.1223 44.9206 11.0828 44.9206L10.7266 42.823C19.2358 41.9127 34.869 39.0631 47.0194 28.2188Z" fill="white"/>
<path d="M32.6133 34.5499C35.5816 33.1647 42.9431 26.3177 42.112 21.5684L50.542 17.2148C50.5816 28.4945 35.1067 33.2043 32.6133 34.5499Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,18 +0,0 @@
<svg width="489" height="152" viewBox="0 0 489 152" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1434_24034)">
<path d="M75.8 151.6C117.663 151.6 151.6 117.663 151.6 75.8C151.6 33.9368 117.663 0 75.8 0C33.9368 0 0 33.9368 0 75.8C0 117.663 33.9368 151.6 75.8 151.6Z" fill="#231F20"/>
<path d="M74.7999 124.2C49.2999 124.2 21.8999 119.2 21.8999 108.2C21.8999 97.2002 49.2999 92.2002 74.7999 92.2002C100.3 92.2002 127.7 97.2002 127.7 108.2C127.8 119.2 100.3 124.2 74.7999 124.2ZM74.7999 98.5002C61.7999 98.5002 49.5999 99.8002 40.3999 102.2C30.3999 104.8 28.1999 107.6 28.1999 108.2C28.1999 108.8 30.3999 111.6 40.3999 114.2C49.4999 116.6 61.7999 117.9 74.7999 117.9C87.7999 117.9 99.9999 116.6 109.2 114.2C119.2 111.6 121.4 108.8 121.4 108.2C121.4 107.6 119.2 104.8 109.2 102.2C100.1 99.8002 87.7999 98.5002 74.7999 98.5002Z" fill="white"/>
<path d="M46.8999 100.8C55.0999 99.4998 75.0999 93.3998 84.3999 89.2998L103.3 92.0998C99.3999 95.6998 96.6999 98.8998 93.1999 101.8L46.8999 100.8Z" fill="#231F20"/>
<path d="M74.7999 59.5C49.2999 59.5 21.8999 54.5 21.8999 43.5C21.8999 32.5 49.2999 27.5 74.7999 27.5C100.3 27.5 127.7 32.5 127.7 43.5C127.8 54.5 100.3 59.5 74.7999 59.5ZM74.7999 33.8C61.7999 33.8 49.5999 35.1 40.3999 37.5C30.3999 40.1 28.1999 42.9 28.1999 43.5C28.1999 44.1 30.3999 46.9 40.3999 49.5C49.4999 51.9 61.7999 53.2 74.7999 53.2C87.7999 53.2 99.9999 51.9 109.2 49.5C119.2 46.9 121.4 44.1 121.4 43.5C121.4 42.9 119.2 40.1 109.2 37.5C100.1 35.1 87.7999 33.8 74.7999 33.8Z" fill="white"/>
<path d="M118.8 71.2998C82.7001 113.6 60.3001 119.4 55.3001 119.4C55.0001 119.4 28.1001 113.5 28.0001 113.5L27.1001 108.2C48.6001 105.9 88.1001 98.6998 118.8 71.2998Z" fill="white"/>
<path d="M82.3999 87.3C89.8999 83.8 108.5 66.5 106.4 54.5L127.7 43.5C127.8 72 88.6999 83.9 82.3999 87.3Z" fill="white"/>
<path d="M195.6 99.4002H241.4V95.6002C241.4 91.4002 241.9 88.7002 242.8 87.5002C243.8 86.3002 245.5 85.6002 248 85.6002C250.1 85.6002 251.7 86.1002 252.8 87.1002C253.8 88.1002 254.4 89.5002 254.4 91.5002V103.6C254.4 105.8 253.6 107.4 252 108.3C250.4 109.3 247.6 109.7 243.5 109.7H187.8C184.4 109.7 181.8 109.1 180 108C178.2 106.9 177.3 105.2 177.3 103.1C177.3 101.8 177.7 100.5 178.4 99.4002C179.2 98.2002 181 96.5002 183.8 94.3002L236.4 52.5002H192.5V56.2002C192.5 60.5002 192 63.2002 191.1 64.4002C190.1 65.6002 188.4 66.2002 185.9 66.2002C183.7 66.2002 182 65.7002 180.9 64.7002C179.8 63.7002 179.2 62.3002 179.2 60.3002V48.0002C179.2 46.0002 180.2 44.5002 182.3 43.6002C184.4 42.7002 187.6 42.2002 192 42.2002H241.2C245.5 42.2002 248.7 42.8002 250.9 44.1002C253.1 45.3002 254.2 47.2002 254.2 49.6002C254.2 51.0002 253.5 52.6002 252.1 54.4002C250.7 56.2002 248.7 58.1002 245.9 60.2002L195.6 99.4002Z" fill="#231F20"/>
<path d="M376.9 75.9996C376.9 86.3996 372.4 94.9996 363.5 101.6C354.5 108.2 342.9 111.5 328.6 111.5C314.4 111.5 302.8 108.2 293.8 101.6C284.8 94.9996 280.4 86.3996 280.4 75.9996C280.4 65.5996 284.9 57.0996 293.8 50.4996C302.8 43.8996 314.3 40.5996 328.6 40.5996C342.8 40.5996 354.4 43.8996 363.4 50.4996C372.4 57.0996 376.9 65.5996 376.9 75.9996ZM328.6 101.2C338 101.2 345.8 98.7996 351.8 94.0996C357.8 89.3996 360.9 83.2996 360.9 75.9996C360.9 68.6996 357.9 62.5996 351.8 57.7996C345.7 52.9996 338 50.5996 328.6 50.5996C319.2 50.5996 311.5 52.9996 305.5 57.7996C299.5 62.5996 296.4 68.6996 296.4 75.9996C296.4 83.3996 299.4 89.4996 305.4 94.1996C311.5 98.7996 319.2 101.2 328.6 101.2Z" fill="#231F20"/>
<path d="M466.8 46.2998C469.8 46.2998 472 46.6998 473.4 47.4998C474.8 48.2998 475.5 49.5998 475.5 51.3998C475.5 53.0998 474.9 54.3998 473.6 55.1998C472.3 56.0998 470.4 56.4998 467.8 56.4998H430.5V80.9998C430.5 88.9998 431.8 94.2998 434.4 96.7998C437 99.2998 441.5 100.6 447.8 100.6C453.4 100.6 460.1 99.2998 467.9 96.6998C475.7 94.0998 480.5 92.7998 482.3 92.7998C484 92.7998 485.5 93.2998 486.7 94.2998C487.9 95.2998 488.5 96.4998 488.5 97.8998C488.5 99.4998 487.7 100.9 486.2 102.1C484.7 103.3 482.1 104.5 478.5 105.6C472.5 107.5 467.1 108.9 462.3 109.8C457.5 110.7 453 111.1 448.7 111.1C441.5 111.1 435.5 110.2 430.6 108.4C425.7 106.6 422.1 103.9 419.7 100.3C418.5 98.5998 417.6 96.5998 417.1 94.1998C416.6 91.8998 416.3 88.2998 416.3 83.3998V80.9998V56.5998H400.7C398.1 56.5998 396.3 56.1998 395.1 55.3998C393.9 54.5998 393.4 53.2998 393.4 51.4998C393.4 49.4998 394.2 48.0998 395.8 47.3998C397.4 46.6998 400.8 46.2998 406 46.2998H416.4V31.2998V27.3998C416.4 25.2998 417 23.7998 418.1 22.7998C419.2 21.7998 421 21.2998 423.4 21.2998C426.1 21.2998 428 21.8998 429 23.0998C430 24.2998 430.6 27.0998 430.6 31.3998V46.1998H466.8V46.2998Z" fill="#231F20"/>
</g>
<defs>
<clipPath id="clip0_1434_24034">
<rect width="488.4" height="151.6" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -70,7 +70,7 @@ function Explore({ searchInputValue }) {
const [queryParams] = useSearchParams();
const search = queryParams.get('search');
// filtercard filters
const [imageFilters, setImageFilters] = useState(false);
const [imageFilters, setImageFilters] = useState({});
const [osFilters, setOSFilters] = useState([]);
const [archFilters, setArchFilters] = useState([]);
// pagination props
@ -88,8 +88,8 @@ function Explore({ searchInputValue }) {
let filter = {};
filter = !isEmpty(osFilters) ? { ...filter, Os: osFilters } : filter;
filter = !isEmpty(archFilters) ? { ...filter, Arch: archFilters } : filter;
if (imageFilters) {
filter = { ...filter, HasToBeSigned: imageFilters };
if (!isEmpty(Object.keys(imageFilters))) {
filter = { ...filter, ...imageFilters };
}
return filter;
};
@ -101,6 +101,8 @@ function Explore({ searchInputValue }) {
setOSFilters([...osFilters, preselectedFilter]);
} else if (filterConstants.archFilters.map((f) => f.value).includes(preselectedFilter)) {
setArchFilters([...archFilters, preselectedFilter]);
} else if (filterConstants.imageFilters.map((f) => f.value).includes(preselectedFilter)) {
setImageFilters({ ...imageFilters, [preselectedFilter]: true });
}
queryParams.delete('filter');
}
@ -219,6 +221,7 @@ function Explore({ searchInputValue }) {
description={item.description}
downloads={item.downloads}
isSigned={item.isSigned}
isBookmarked={item.isBookmarked}
vendor={item.vendor}
platforms={item.platforms}
key={index}

View File

@ -8,7 +8,8 @@ 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 } from 'utilities/paginationConstants';
import { HOME_POPULAR_PAGE_SIZE, HOME_RECENT_PAGE_SIZE, HOME_BOOKMARKS_PAGE_SIZE } from 'utilities/paginationConstants';
import { isEmpty } from 'lodash';
const useStyles = makeStyles((theme) => ({
gridWrapper: {
@ -82,13 +83,18 @@ const useStyles = makeStyles((theme) => ({
function Home() {
const [isLoading, setIsLoading] = useState(true);
const [popularData, setPopularData] = useState([]);
const [isLoadingPopular, setIsLoadingPopular] = useState(true);
const [recentData, setRecentData] = useState([]);
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
const [bookmarkData, setBookmarkData] = useState([]);
const [isLoadingBookmarks, setIsLoadingBookmarks] = useState(true);
const navigate = useNavigate();
const abortController = useMemo(() => new AbortController(), []);
const classes = useStyles();
const getPopularData = () => {
setIsLoading(true);
setIsLoadingPopular(true);
api
.get(
`${host()}${endpoints.globalSearch({
@ -107,15 +113,18 @@ function Home() {
});
setPopularData(repoData);
setIsLoading(false);
setIsLoadingPopular(false);
}
})
.catch((e) => {
console.error(e);
setIsLoading(false);
setIsLoadingPopular(false);
});
};
const getRecentData = () => {
setIsLoading(true);
setIsLoadingRecent(true);
api
.get(
`${host()}${endpoints.globalSearch({
@ -134,56 +143,66 @@ function Home() {
});
setRecentData(repoData);
setIsLoading(false);
setIsLoadingRecent(false);
}
})
.catch((e) => {
setIsLoading(false);
setIsLoadingRecent(false);
console.error(e);
});
};
const getBookmarks = () => {
setIsLoadingBookmarks(true);
api
.get(
`${host()}${endpoints.globalSearch({
searchQuery: '',
pageNumber: 1,
pageSize: HOME_BOOKMARKS_PAGE_SIZE,
sortBy: sortByCriteria.relevance?.value,
filter: { IsBookmarked: 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);
});
setBookmarkData(repoData);
setIsLoading(false);
setIsLoadingBookmarks(false);
}
})
.catch((e) => {
setIsLoading(false);
setIsLoadingBookmarks(false);
console.error(e);
});
};
useEffect(() => {
window.scrollTo(0, 0);
setIsLoading(true);
getPopularData();
getRecentData();
getBookmarks();
return () => {
abortController.abort();
};
}, []);
const handleClickViewAll = (target) => {
navigate({ pathname: `/explore`, search: createSearchParams({ sortby: target }).toString() });
const handleClickViewAll = (type, value) => {
navigate({ pathname: `/explore`, search: createSearchParams({ [type]: value }).toString() });
};
const renderMostPopular = () => {
const renderCards = (cardArray) => {
return (
popularData &&
popularData.map((item, index) => {
return (
<RepoCard
name={item.name}
version={item.latestVersion}
description={item.description}
downloads={item.downloads}
isSigned={item.isSigned}
vendor={item.vendor}
platforms={item.platforms}
key={index}
vulnerabilityData={{
vulnerabilitySeverity: item.vulnerabiltySeverity,
count: item.vulnerabilityCount
}}
lastUpdated={item.lastUpdated}
logo={item.logo}
/>
);
})
);
};
const renderRecentlyUpdated = () => {
return (
recentData &&
recentData.map((item, index) => {
cardArray &&
cardArray.map((item, index) => {
return (
<RepoCard
name={item.name}
@ -191,6 +210,7 @@ function Home() {
description={item.description}
downloads={item.downloads}
isSigned={item.isSigned}
isBookmarked={item.isBookmarked}
vendor={item.vendor}
platforms={item.platforms}
key={index}
@ -218,13 +238,13 @@ function Home() {
Most popular images
</Typography>
</div>
<div onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}>
<div onClick={() => handleClickViewAll('sortby', sortByCriteria.downloads.value)}>
<Typography variant="body2" className={classes.viewAll}>
View all
</Typography>
</div>
</Stack>
{renderMostPopular()}
{isLoadingPopular ? <Loading /> : renderCards(popularData)}
{/* currently most popular will be by downloads until stars are implemented */}
<Stack className={classes.sectionHeaderContainer}>
<div>
@ -236,13 +256,34 @@ function Home() {
<Typography
variant="body2"
className={classes.viewAll}
onClick={() => handleClickViewAll(sortByCriteria.updateTime.value)}
onClick={() => handleClickViewAll('sortby', sortByCriteria.updateTime.value)}
>
View all
</Typography>
</div>
</Stack>
{renderRecentlyUpdated()}
{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>
)}
</>

View File

@ -3,16 +3,18 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
// external
import { DateTime } from 'luxon';
import { isEmpty, uniq } from 'lodash';
// utility
import { api, endpoints } from '../../api';
import { host } from '../../host';
import { useParams, useNavigate, createSearchParams } from 'react-router-dom';
// components
import Tags from './Tabs/Tags.jsx';
import { Card, CardContent, CardMedia, Chip, Grid, Stack, Tooltip, Typography } from '@mui/material';
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 makeStyles from '@mui/styles/makeStyles';
import { host } from '../../host';
// placeholder images
import repocube1 from '../../assets/repocube-1.png';
@ -20,12 +22,13 @@ import repocube2 from '../../assets/repocube-2.png';
import repocube3 from '../../assets/repocube-3.png';
import repocube4 from '../../assets/repocube-4.png';
import Tags from './Tabs/Tags.jsx';
import RepoDetailsMetadata from './RepoDetailsMetadata';
import Loading from '../Shared/Loading';
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
import { isEmpty, uniq } from 'lodash';
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
import { isAuthenticated } from 'utilities/authUtilities';
const useStyles = makeStyles((theme) => ({
pageWrapper: {
@ -216,6 +219,17 @@ function RepoDetails() {
));
};
const handleBookmarkClick = () => {
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
if (response.status === 200) {
setRepoDetailData((prevState) => ({
...prevState,
isBookmarked: !prevState.isBookmarked
}));
}
});
};
const getVendor = () => {
return `${repoDetailData.newestTag?.Vendor || 'Vendor not available'}`;
};
@ -256,12 +270,18 @@ function RepoDetails() {
</Typography>
</Stack>
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
<VulnerabilityIconCheck
vulnerabilitySeverity={repoDetailData.vulnerabiltySeverity}
count={repoDetailData?.vulnerabilityCount}
/>
<VulnerabilityIconCheck vulnerabilitySeverity={repoDetailData?.vulnerabilitySeverity} />
<SignatureIconCheck isSigned={repoDetailData.isSigned} />
</Stack>
{isAuthenticated() && (
<IconButton component="span" onClick={handleBookmarkClick} data-testid="bookmark-button">
{repoDetailData?.isBookmarked ? (
<BookmarkIcon data-testid="bookmarked" />
) : (
<BookmarkBorderIcon data-testid="not-bookmarked" />
)}
</IconButton>
)}
</Stack>
<Typography gutterBottom className={classes.repoTitle}>
{repoDetailData?.title || 'Title not available'}

View File

@ -1,6 +1,6 @@
import { Card, CardContent, Checkbox, FormControlLabel, Stack, Tooltip, Typography } from '@mui/material';
import { makeStyles } from '@mui/styles';
import { isBoolean, isArray } from 'lodash';
import { isArray, isNil } from 'lodash';
import React from 'react';
const useStyles = makeStyles((theme) => ({
@ -42,17 +42,17 @@ function FilterCard(props) {
const classes = useStyles();
const { title, filters, updateFilters, filterValue, wrapperLoading } = props;
const handleFilterClicked = (event, changedFilterLabel, changedFilterValue) => {
const handleFilterClicked = (event, changedFilterValue) => {
const { checked } = event.target;
if (checked) {
if (filters[0]?.type === 'boolean') {
updateFilters(checked);
if (!isArray(filterValue)) {
updateFilters({ ...filterValue, [changedFilterValue]: true });
} else {
updateFilters([...filterValue, changedFilterValue]);
}
} else {
if (filters[0]?.type === 'boolean') {
updateFilters(checked);
if (!isArray(filterValue)) {
updateFilters({ ...filterValue, [changedFilterValue]: false });
} else {
updateFilters(filterValue.filter((e) => e !== changedFilterValue));
}
@ -60,12 +60,14 @@ function FilterCard(props) {
}
};
const getCheckboxStatus = (label) => {
if (isArray(filterValue)) {
return filterValue?.includes(label);
} else if (isBoolean(filterValue)) {
return filterValue;
const getCheckboxStatus = (filter) => {
if (isNil(filter)) {
return false;
}
if (isArray(filterValue)) {
return filterValue?.includes(filter.label);
}
return filterValue[filter.value] || false;
};
const getFilterRows = () => {
@ -79,8 +81,8 @@ function FilterCard(props) {
control={<Checkbox sx={{ padding: '0.188rem', color: '#52637A' }} />}
label={filter.label}
id={title}
checked={getCheckboxStatus(filter.label)}
onChange={() => handleFilterClicked(event, filter.label, filter.value)}
checked={getCheckboxStatus(filter)}
onChange={() => handleFilterClicked(event, filter.value)}
disabled={wrapperLoading}
/>
</Tooltip>

View File

@ -1,9 +1,16 @@
// react global
import React, { useRef } from 'react';
import React, { useRef, useMemo, useState } from 'react';
import { useNavigate, createSearchParams } from 'react-router-dom';
// utility
import { DateTime } from 'luxon';
import { uniq } from 'lodash';
// api module
import { api, endpoints } from '../../api';
import { host } from '../../host';
import { isAuthenticated } from '../../utilities/authUtilities';
// components
import {
Card,
@ -15,9 +22,13 @@ import {
Chip,
Grid,
Tooltip,
IconButton,
useMediaQuery
} from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import BookmarkIcon from '@mui/icons-material/Bookmark';
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
import { useTheme } from '@emotion/react';
// placeholder images
import repocube1 from '../../assets/repocube-1.png';
@ -27,8 +38,6 @@ import repocube4 from '../../assets/repocube-4.png';
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
import { uniq } from 'lodash';
import { useTheme } from '@emotion/react';
// temporary utility to get image
const randomIntFromInterval = (min, max) => {
@ -89,7 +98,8 @@ const useStyles = makeStyles((theme) => ({
}
},
contentRight: {
height: '100%'
justifyContent: 'flex-end',
textAlign: 'end'
},
contentRightLabel: {
fontSize: '0.75rem',
@ -105,6 +115,10 @@ const useStyles = makeStyles((theme) => ({
textAlign: 'end',
marginLeft: '0.5rem'
},
contentRightActions: {
alignItems: 'flex-end',
justifyContent: 'flex-end'
},
signedBadge: {
color: '#9ccc65',
height: '1.375rem',
@ -161,7 +175,23 @@ function RepoCard(props) {
const isXsSize = useMediaQuery(theme.breakpoints.down('md'));
const MAX_PLATFORM_CHIPS = isXsSize ? 3 : 6;
const { name, vendor, platforms, description, downloads, isSigned, lastUpdated, version, vulnerabilityData } = props;
const abortController = useMemo(() => new AbortController(), []);
const {
name,
vendor,
platforms,
description,
downloads,
isSigned,
lastUpdated,
version,
vulnerabilityData,
isBookmarked
} = props;
// keep a local bookmark state to display in the ui dynamically on updates
const [currentBookmarkValue, setCurrentBookmarkValue] = useState(isBookmarked);
const goToDetails = () => {
navigate(`/image/${encodeURIComponent(name)}`);
@ -174,6 +204,16 @@ function RepoCard(props) {
navigate({ pathname: `/explore`, search: createSearchParams({ filter: textContent }).toString() });
};
const handleBookmarkClick = (event) => {
event.stopPropagation();
event.preventDefault();
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
if (response.status === 200) {
setCurrentBookmarkValue((prevState) => !prevState);
}
});
};
const platformChips = () => {
const filteredPlatforms = uniq(platforms?.flatMap((platform) => [platform.Os, platform.Arch]));
const hiddenChips = filteredPlatforms.length - MAX_PLATFORM_CHIPS;
@ -205,8 +245,22 @@ function RepoCard(props) {
return lastDate;
};
const renderBookmark = () => {
return (
isAuthenticated() && (
<IconButton component="span" onClick={handleBookmarkClick} data-testid="bookmark-button">
{currentBookmarkValue ? (
<BookmarkIcon data-testid="bookmarked" />
) : (
<BookmarkBorderIcon data-testid="not-bookmarked" />
)}
</IconButton>
)
);
};
return (
<Card variant="outlined" className={classes.card}>
<Card variant="outlined" className={classes.card} data-testid="repo-card">
<CardActionArea
onClick={goToDetails}
classes={{
@ -265,17 +319,16 @@ function RepoCard(props) {
</Tooltip>
</Stack>
</Grid>
<Grid item xs={2} md={2} className={`hide-on-mobile ${classes.contentRight}`}>
<Grid container item justifyContent="flex-end" textAlign="end">
<Grid item xs={12}>
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
Downloads
</Typography>
<Typography variant="body2" component="span" className={classes.contentRightValue}>
{!isNaN(downloads) ? downloads : `not available`}
</Typography>
</Grid>
{/* <Grid item xs={12}>
<Grid item container xs={2} md={2} className={`hide-on-mobile ${classes.contentRight}`}>
<Grid item xs={12}>
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
Downloads
</Typography>
<Typography variant="body2" component="span" className={classes.contentRightValue}>
{!isNaN(downloads) ? downloads : `not available`}
</Typography>
</Grid>
{/* <Grid item xs={12}>
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
Rating
</Typography>
@ -283,6 +336,8 @@ function RepoCard(props) {
#1
</Typography>
</Grid> */}
<Grid container item xs={12} className={classes.contentRightActions}>
<Grid item>{renderBookmark()}</Grid>
</Grid>
</Grid>
</Grid>

View File

@ -60,7 +60,6 @@ body {
min-height: 100vh;
overflow-x: hidden;
/* background-image: url(./assets/background.png); */
background-color: #f6f7f9 !important;
}

View File

@ -0,0 +1,5 @@
const isAuthenticated = () => {
return localStorage.getItem('token') !== '-';
};
export { isAuthenticated };

View File

@ -14,6 +14,11 @@ const imageFilters = [
label: 'Signed Images',
value: 'HasToBeSigned',
type: 'boolean'
},
{
label: 'Bookmarks',
value: 'IsBookmarked',
type: 'boolean'
}
];

View File

@ -5,6 +5,8 @@ const mapToRepo = (responseRepo) => {
tags: responseRepo.NewestImage?.Labels,
description: responseRepo.NewestImage?.Description,
isSigned: responseRepo.NewestImage?.IsSigned,
isBookmarked: responseRepo.IsBookmarked,
isStarred: responseRepo.IsStarred,
platforms: responseRepo.Platforms,
licenses: responseRepo.NewestImage?.Licenses,
size: responseRepo.Size,
@ -32,9 +34,11 @@ const mapToRepoFromRepoInfo = (responseRepoInfo) => {
downloads: responseRepoInfo.Summary?.NewestImage?.DownloadCount,
overview: responseRepoInfo.Summary?.NewestImage?.Documentation,
license: responseRepoInfo.Summary?.NewestImage?.Licenses,
vulnerabiltySeverity: responseRepoInfo.Summary?.NewestImage?.Vulnerabilities?.MaxSeverity,
vulnerabilitySeverity: responseRepoInfo.Summary?.NewestImage?.Vulnerabilities?.MaxSeverity,
vulnerabilityCount: responseRepoInfo.Summary?.NewestImage?.Vulnerabilities?.Count,
isSigned: responseRepoInfo.Summary?.NewestImage?.IsSigned,
isBookmarked: responseRepoInfo.Summary?.IsBookmarked,
isStarred: responseRepoInfo.Summary?.IsStarred,
logo: responseRepoInfo.Summary?.NewestImage?.Logo
};
};

View File

@ -3,6 +3,7 @@ const EXPLORE_PAGE_SIZE = 10;
const HOME_PAGE_SIZE = 10;
const HOME_POPULAR_PAGE_SIZE = 3;
const HOME_RECENT_PAGE_SIZE = 2;
const HOME_BOOKMARKS_PAGE_SIZE = 2;
const CVE_FIXEDIN_PAGE_SIZE = 5;
export {
@ -11,5 +12,6 @@ export {
HOME_PAGE_SIZE,
CVE_FIXEDIN_PAGE_SIZE,
HOME_POPULAR_PAGE_SIZE,
HOME_RECENT_PAGE_SIZE
HOME_RECENT_PAGE_SIZE,
HOME_BOOKMARKS_PAGE_SIZE
};