handleClickViewAll(sortByCriteria.downloads.value)}>
+
handleClickViewAll('sortby', sortByCriteria.downloads.value)}>
View all
- {renderMostPopular()}
+ {isLoadingPopular ?
: renderCards(popularData)}
{/* currently most popular will be by downloads until stars are implemented */}
@@ -236,13 +256,34 @@ function Home() {
handleClickViewAll(sortByCriteria.updateTime.value)}
+ onClick={() => handleClickViewAll('sortby', sortByCriteria.updateTime.value)}
>
View all
- {renderRecentlyUpdated()}
+ {isLoadingRecent ?
: renderCards(recentData)}
+ {!isEmpty(bookmarkData) && (
+ <>
+
+
+
+ Bookmarks
+
+
+
+ handleClickViewAll('filter', 'IsBookmarked')}
+ >
+ View all
+
+
+
+ {isLoadingBookmarks ?
: renderCards(bookmarkData)}
+ >
+ )}
)}
>
diff --git a/src/components/Repo/RepoDetails.jsx b/src/components/Repo/RepoDetails.jsx
index d15e307a..95bc8289 100644
--- a/src/components/Repo/RepoDetails.jsx
+++ b/src/components/Repo/RepoDetails.jsx
@@ -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() {
-
+
+ {isAuthenticated() && (
+
+ {repoDetailData?.isBookmarked ? (
+
+ ) : (
+
+ )}
+
+ )}
{repoDetailData?.title || 'Title not available'}
diff --git a/src/components/Shared/FilterCard.jsx b/src/components/Shared/FilterCard.jsx
index af7712d6..ffa60140 100644
--- a/src/components/Shared/FilterCard.jsx
+++ b/src/components/Shared/FilterCard.jsx
@@ -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) => {
+ const getCheckboxStatus = (filter) => {
+ if (isNil(filter)) {
+ return false;
+ }
if (isArray(filterValue)) {
- return filterValue?.includes(label);
- } else if (isBoolean(filterValue)) {
- return filterValue;
+ return filterValue?.includes(filter.label);
}
+ return filterValue[filter.value] || false;
};
const getFilterRows = () => {
@@ -79,8 +81,8 @@ function FilterCard(props) {
control={}
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}
/>
diff --git a/src/components/Shared/RepoCard.jsx b/src/components/Shared/RepoCard.jsx
index 8b4ef58b..5060e323 100644
--- a/src/components/Shared/RepoCard.jsx
+++ b/src/components/Shared/RepoCard.jsx
@@ -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() && (
+
+ {currentBookmarkValue ? (
+
+ ) : (
+
+ )}
+
+ )
+ );
+ };
+
return (
-
+
-
-
-
-
- Downloads •
-
-
- {!isNaN(downloads) ? downloads : `not available`}
-
-
- {/*
+
+
+
+ Downloads •
+
+
+ {!isNaN(downloads) ? downloads : `not available`}
+
+
+ {/*
Rating •
@@ -283,6 +336,8 @@ function RepoCard(props) {
#1
*/}
+
+ {renderBookmark()}
diff --git a/src/index.css b/src/index.css
index 68c85d28..fa5ef1f6 100644
--- a/src/index.css
+++ b/src/index.css
@@ -60,7 +60,6 @@ body {
min-height: 100vh;
overflow-x: hidden;
- /* background-image: url(./assets/background.png); */
background-color: #f6f7f9 !important;
}
diff --git a/src/utilities/authUtilities.js b/src/utilities/authUtilities.js
new file mode 100644
index 00000000..55678cf5
--- /dev/null
+++ b/src/utilities/authUtilities.js
@@ -0,0 +1,5 @@
+const isAuthenticated = () => {
+ return localStorage.getItem('token') !== '-';
+};
+
+export { isAuthenticated };
diff --git a/src/utilities/filterConstants.js b/src/utilities/filterConstants.js
index 01054f5f..5ae78784 100644
--- a/src/utilities/filterConstants.js
+++ b/src/utilities/filterConstants.js
@@ -14,6 +14,11 @@ const imageFilters = [
label: 'Signed Images',
value: 'HasToBeSigned',
type: 'boolean'
+ },
+ {
+ label: 'Bookmarks',
+ value: 'IsBookmarked',
+ type: 'boolean'
}
];
diff --git a/src/utilities/objectModels.js b/src/utilities/objectModels.js
index cefaffd9..b548bd3e 100644
--- a/src/utilities/objectModels.js
+++ b/src/utilities/objectModels.js
@@ -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
};
};
diff --git a/src/utilities/paginationConstants.js b/src/utilities/paginationConstants.js
index 757f60f2..9d407daa 100644
--- a/src/utilities/paginationConstants.js
+++ b/src/utilities/paginationConstants.js
@@ -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
};