Skip to content

Commit

Permalink
feat: starred repos implementation (project-zot#399)
Browse files Browse the repository at this point in the history
Signed-off-by: Andreea-Lupu <[email protected]>
  • Loading branch information
Andreea-Lupu authored Dec 7, 2023
1 parent e97e04e commit d9370fb
Show file tree
Hide file tree
Showing 13 changed files with 248 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/end-to-end-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ jobs:
- name: Build zot
run: |
cd $GITHUB_WORKSPACE/zot
make binary
make binary ZUI_BUILD_PATH=$GITHUB_WORKSPACE/build
ls -l bin/
- name: Bringup zot server
Expand Down
16 changes: 16 additions & 0 deletions src/__tests__/Explore/Explore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const mockImageList = {
Size: '2806985',
LastUpdated: '2022-08-09T17:19:53.274069586Z',
IsBookmarked: false,
IsStarred: false,
NewestImage: {
Tag: 'latest',
Description: 'w',
Expand All @@ -58,6 +59,7 @@ const mockImageList = {
Size: '231383863',
LastUpdated: '2022-08-02T01:30:49.193203152Z',
IsBookmarked: false,
IsStarred: false,
NewestImage: {
Tag: 'latest',
Description: '',
Expand All @@ -82,6 +84,7 @@ const mockImageList = {
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
IsStarred: false,
NewestImage: {
Tag: 'latest',
Description: '',
Expand All @@ -106,6 +109,7 @@ const mockImageList = {
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
IsStarred: false,
NewestImage: {
Tag: 'latest',
Description: '',
Expand All @@ -130,6 +134,7 @@ const mockImageList = {
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
IsStarred: false,
NewestImage: {
Tag: 'latest',
Description: '',
Expand Down Expand Up @@ -158,6 +163,7 @@ const mockImageList = {
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
IsStarred: false,
NewestImage: {
Tag: 'latest',
Description: '',
Expand All @@ -182,6 +188,7 @@ const mockImageList = {
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
IsBookmarked: false,
IsStarred: false,
NewestImage: {
Tag: 'latest',
Description: '',
Expand Down Expand Up @@ -338,4 +345,13 @@ describe('Explore component', () => {
await userEvent.click(bookmarkButton);
expect(await screen.findAllByTestId('bookmarked')).toHaveLength(1);
});

it('should star a repo if star button is clicked', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
render(<StateExploreWrapper />);
const starButton = (await screen.findAllByTestId('star-button'))[0];
jest.spyOn(api, 'put').mockResolvedValueOnce({ status: 200, data: {} });
await userEvent.click(starButton);
expect(await screen.findAllByTestId('starred')).toHaveLength(1);
});
});
64 changes: 56 additions & 8 deletions src/__tests__/HomePage/Home.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,48 @@ const mockImageListBookmarks = {
}
};

const mockImageListStars = {
GlobalSearch: {
Page: { TotalCount: 3, ItemCount: 2 },
Repos: [
{
Name: 'alpine',
Size: '2806985',
LastUpdated: '2022-08-09T17:19:53.274069586Z',
NewestImage: {
Tag: 'latest',
Description: 'w',
IsSigned: false,
Licenses: '',
Vendor: '',
Labels: '',
Vulnerabilities: {
MaxSeverity: 'LOW',
Count: 7
}
}
},
{
Name: 'mongo',
Size: '231383863',
LastUpdated: '2022-08-02T01:30:49.193203152Z',
NewestImage: {
Tag: 'latest',
Description: '',
IsSigned: true,
Licenses: '',
Vendor: '',
Labels: '',
Vulnerabilities: {
MaxSeverity: 'HIGH',
Count: 2
}
}
}
]
}
};

beforeEach(() => {
window.scrollTo = jest.fn();
});
Expand All @@ -178,42 +220,43 @@ describe('Home component', () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
render(<HomeWrapper />);
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(3));
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(3));
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(4));
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(4));
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
});

it('renders signature icons', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
render(<HomeWrapper />);
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(3);
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(4);
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(4);
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(5);
});

it('renders vulnerability icons', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
render(<HomeWrapper />);
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(3);
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(3);
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(4);
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(4);
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
});

it("should log an error when data can't be fetched", async () => {
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<HomeWrapper />);
await waitFor(() => expect(error).toBeCalledTimes(3));
await waitFor(() => expect(error).toBeCalledTimes(4));
});

it('should redirect to explore page when clicking view all popular', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListBookmarks } });
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListStars } });
render(<HomeWrapper />);
const viewAllButtons = await screen.findAllByText(/view all/i);
expect(viewAllButtons).toHaveLength(3);
expect(viewAllButtons).toHaveLength(4);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: [] } });
fireEvent.click(viewAllButtons[0]);
expect(mockedUsedNavigate).toHaveBeenCalledWith({
Expand All @@ -230,5 +273,10 @@ describe('Home component', () => {
pathname: `/explore`,
search: createSearchParams({ filter: 'IsBookmarked' }).toString()
});
fireEvent.click(viewAllButtons[3]);
expect(mockedUsedNavigate).toHaveBeenCalledWith({
pathname: `/explore`,
search: createSearchParams({ filter: 'IsStarred' }).toString()
});
});
});
10 changes: 10 additions & 0 deletions src/__tests__/RepoPage/Repo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const mockRepoDetailsData = {
Size: '451554070',
Vendors: ['[The Node.js Docker Team](https://github.com/nodejs/docker-node)\n'],
IsBookmarked: false,
IsStarred: false,
NewestImage: {
RepoName: 'mongo',
IsSigned: true,
Expand Down Expand Up @@ -316,4 +317,13 @@ describe('Repo details component', () => {
await userEvent.click(bookmarkButton);
expect(await screen.findByTestId('bookmarked')).toBeInTheDocument();
});

it('should star a repo if star button is clicked', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockRepoDetailsData } });
render(<RepoDetailsThemeWrapper />);
const starButton = await screen.findByTestId('star-button');
jest.spyOn(api, 'put').mockResolvedValue({ status: 200, data: {} });
await userEvent.click(starButton);
expect(await screen.findByTestId('starred')).toBeInTheDocument();
});
});
8 changes: 5 additions & 3 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ 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 SignatureInfo { Tool IsTrusted Author } Documentation Vendor Labels} IsStarred IsBookmarked DownloadCount}}}`,
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned SignatureInfo { Tool IsTrusted Author } Documentation Vendor Labels} IsStarred IsBookmarked StarCount DownloadCount}}}`,
detailedRepoInfo: (name) =>
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
detailedImageInfo: (name, tag) =>
Expand Down Expand Up @@ -134,9 +134,10 @@ const endpoints = {
if (filter.Arch) filterParam += ` Arch:${!isEmpty(filter.Arch) ? `${JSON.stringify(filter.Arch)}` : '""'}`;
if (filter.HasToBeSigned) filterParam += ` HasToBeSigned: ${filter.HasToBeSigned}`;
if (filter.IsBookmarked) filterParam += ` IsBookmarked: ${filter.IsBookmarked}`;
if (filter.IsStarred) filterParam += ` IsStarred: ${filter.IsStarred}`;
filterParam += '}';
if (Object.keys(filter).length === 0) filterParam = '';
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam} ${filterParam}) {Page {TotalCount ItemCount} Repos {Name LastUpdated Size Platforms { Os Arch } IsStarred IsBookmarked NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description IsSigned SignatureInfo { Tool IsTrusted Author } Licenses Vendor Labels } DownloadCount}}}`;
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam} ${filterParam}) {Page {TotalCount ItemCount} Repos {Name LastUpdated Size Platforms { Os Arch } IsStarred IsBookmarked NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description IsSigned SignatureInfo { Tool IsTrusted Author } Licenses Vendor Labels } StarCount DownloadCount}}}`;
},
imageSuggestions: ({ searchQuery = '""', pageNumber = 1, pageSize = 15 }) => {
const searchParam = searchQuery !== '' ? `query:"${searchQuery}"` : `query:""`;
Expand All @@ -145,7 +146,8 @@ const endpoints = {
},
referrers: ({ repo, digest, type = '' }) =>
`/v2/_zot/ext/search?query={Referrers(repo: "${repo}" digest: "${digest}" type: "${type}"){MediaType ArtifactType Size Digest Annotations{Key Value}}}`,
bookmarkToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleBookmark`
bookmarkToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleBookmark`,
starToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleStar`
};

export { api, endpoints };
2 changes: 2 additions & 0 deletions src/components/Explore/Explore.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,11 @@ function Explore({ searchInputValue }) {
version={item.latestVersion}
description={item.description}
downloads={item.downloads}
stars={item.stars}
isSigned={item.isSigned}
signatureInfo={item.signatureInfo}
isBookmarked={item.isBookmarked}
isStarred={item.isStarred}
vendor={item.vendor}
platforms={item.platforms}
key={index}
Expand Down
66 changes: 65 additions & 1 deletion src/components/Home/Home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { mapToRepo } from 'utilities/objectModels';
import Loading from '../Shared/Loading';
import { useNavigate, createSearchParams } from 'react-router-dom';
import { sortByCriteria } from 'utilities/sortCriteria';
import { HOME_POPULAR_PAGE_SIZE, HOME_RECENT_PAGE_SIZE, HOME_BOOKMARKS_PAGE_SIZE } from 'utilities/paginationConstants';
import {
HOME_POPULAR_PAGE_SIZE,
HOME_RECENT_PAGE_SIZE,
HOME_BOOKMARKS_PAGE_SIZE,
HOME_STARS_PAGE_SIZE
} from 'utilities/paginationConstants';
import { isEmpty } from 'lodash';
import NoDataComponent from 'components/Shared/NoDataComponent';

Expand Down Expand Up @@ -89,6 +94,8 @@ function Home() {
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
const [bookmarkData, setBookmarkData] = useState([]);
const [isLoadingBookmarks, setIsLoadingBookmarks] = useState(true);
const [starData, setStarData] = useState([]);
const [isLoadingStars, setIsLoadingStars] = useState(true);

const navigate = useNavigate();
const abortController = useMemo(() => new AbortController(), []);
Expand Down Expand Up @@ -185,12 +192,44 @@ function Home() {
});
};

const getStars = () => {
setIsLoadingStars(true);
api
.get(
`${host()}${endpoints.globalSearch({
searchQuery: '',
pageNumber: 1,
pageSize: HOME_STARS_PAGE_SIZE,
sortBy: sortByCriteria.relevance?.value,
filter: { IsStarred: true }
})}`,
abortController.signal
)
.then((response) => {
if (response.data && response.data.data) {
let repoList = response.data.data.GlobalSearch.Repos;
let repoData = repoList.map((responseRepo) => {
return mapToRepo(responseRepo);
});
setStarData(repoData);
setIsLoading(false);
setIsLoadingStars(false);
}
})
.catch((e) => {
setIsLoading(false);
setIsLoadingStars(false);
console.error(e);
});
};

useEffect(() => {
window.scrollTo(0, 0);
setIsLoading(true);
getPopularData();
getRecentData();
getBookmarks();
getStars();
return () => {
abortController.abort();
};
Expand All @@ -203,9 +242,11 @@ function Home() {
const isNoData = () =>
!isLoading &&
!isLoadingBookmarks &&
!isLoadingStars &&
!isLoadingPopular &&
!isLoadingRecent &&
bookmarkData.length === 0 &&
starData.length === 0 &&
popularData.length === 0 &&
recentData.length === 0;

Expand All @@ -219,9 +260,11 @@ function Home() {
version={item.latestVersion}
description={item.description}
downloads={item.downloads}
stars={item.stars}
isSigned={item.isSigned}
signatureInfo={item.signatureInfo}
isBookmarked={item.isBookmarked}
isStarred={item.isStarred}
vendor={item.vendor}
platforms={item.platforms}
key={index}
Expand Down Expand Up @@ -294,6 +337,27 @@ function Home() {
{isLoadingBookmarks ? <Loading /> : renderCards(bookmarkData, isLoadingBookmarks)}
</>
)}
{!isEmpty(starData) && (
<>
<Stack className={classes.sectionHeaderContainer}>
<div>
<Typography variant="h4" align="left" className={classes.sectionTitle}>
Stars
</Typography>
</div>
<div>
<Typography
variant="body2"
className={classes.viewAll}
onClick={() => handleClickViewAll('filter', 'IsStarred')}
>
View all
</Typography>
</div>
</Stack>
{isLoadingStars ? <Loading /> : renderCards(starData, isLoadingStars)}
</>
)}
</Stack>
);
};
Expand Down
Loading

0 comments on commit d9370fb

Please sign in to comment.