feat: Implemented mobile responsiveness across app

Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Raul Kele 2022-12-13 12:17:54 +02:00
parent 1c5efb6e40
commit 1bfee961f5
19 changed files with 400 additions and 271 deletions

View File

@ -72,3 +72,9 @@
-webkit-animation-name: bounce;
animation-name: bounce;
}
@media (max-width: 480px) {
.hide-on-mobile {
display: none;
}
}

View File

@ -0,0 +1,10 @@
import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme();
function MockThemeProvier({ children }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
export default MockThemeProvier;

View File

@ -6,6 +6,7 @@ import React from 'react';
import { createSearchParams, MemoryRouter } from 'react-router-dom';
import filterConstants from 'utilities/filterConstants.js';
import { sortByCriteria } from 'utilities/sortCriteria.js';
import MockThemeProvier from '__mocks__/MockThemeProvider';
// router mock
const mockedUsedNavigate = jest.fn();
@ -17,9 +18,11 @@ jest.mock('react-router-dom', () => ({
const StateExploreWrapper = (props) => {
const queryString = props.search || '';
return (
<MemoryRouter initialEntries={[`/explore?${queryString.toString()}`]}>
<Explore />
</MemoryRouter>
<MockThemeProvier>
<MemoryRouter initialEntries={[`/explore?${queryString.toString()}`]}>
<Explore />
</MemoryRouter>
</MockThemeProvier>
);
};
const mockImageList = {

View File

@ -3,6 +3,15 @@ import RepoDetails from 'components/RepoDetails';
import React from 'react';
import { api } from 'api';
import { createSearchParams } from 'react-router-dom';
import MockThemeProvier from '__mocks__/MockThemeProvider';
const RepoDetailsThemeWrapper = () => {
return (
<MockThemeProvier>
<RepoDetails />
</MockThemeProvier>
);
};
// uselocation mock
const mockUseLocationValue = {
@ -190,44 +199,44 @@ afterEach(() => {
describe('Repo details component', () => {
it('fetches repo detailed data and renders', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
expect(await screen.findByText('test')).toBeInTheDocument();
});
it('renders vulnerability icons', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsNone } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
expect(await screen.findAllByTestId('none-vulnerability-icon')).toHaveLength(1);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsUnknown } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
expect(await screen.findAllByTestId('unknown-vulnerability-icon')).toHaveLength(1);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsFailed } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
expect(await screen.findAllByTestId('failed-vulnerability-icon')).toHaveLength(1);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsLow } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(1);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsMedium } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
expect(await screen.findAllByTestId('medium-vulnerability-icon')).toHaveLength(1);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsHigh } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(1);
});
it("should log error if data can't be fetched", async () => {
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
await waitFor(() => expect(error).toBeCalledTimes(1));
});
it('should switch between tabs', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
expect(await screen.findByTestId('overview-container')).toBeInTheDocument();
fireEvent.click(await screen.findByText(/tags/i));
expect(await screen.findByTestId('tags-container')).toBeInTheDocument();
@ -236,7 +245,7 @@ describe('Repo details component', () => {
it('should render platform chips and they should redirect to explore page', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
render(<RepoDetails />);
render(<RepoDetailsThemeWrapper />);
const osChip = await screen.findByText(/linux/i);
fireEvent.click(osChip);
expect(mockUseNavigate).toHaveBeenCalledWith({

View File

@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
import RepoPage from 'pages/RepoPage';
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import MockThemeProvier from '__mocks__/MockThemeProvider';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@ -28,7 +29,14 @@ it('renders the repository page component', () => {
render(
<BrowserRouter>
<Routes>
<Route path="*" element={<RepoPage />} />
<Route
path="*"
element={
<MockThemeProvier>
<RepoPage />
</MockThemeProvier>
}
/>
</Routes>
</BrowserRouter>
);

View File

@ -1,8 +1,17 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { api } from 'api';
import TagDetails from 'components/TagDetails';
import React from 'react';
import MockThemeProvier from '__mocks__/MockThemeProvider';
const TagDetailsThemeWrapper = () => {
return (
<MockThemeProvier>
<TagDetails />
</MockThemeProvier>
);
};
const mockImage = {
Image: {
@ -222,7 +231,7 @@ afterEach(() => {
describe('Tags details', () => {
it('should show tabs and allow nagivation between them', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
const dependenciesTab = await screen.findByTestId('dependencies-tab');
fireEvent.click(dependenciesTab);
expect(await screen.findByTestId('depends-on-container')).toBeInTheDocument();
@ -232,52 +241,49 @@ describe('Tags details', () => {
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(<TagDetails />);
render(<TagDetailsThemeWrapper />);
await waitFor(() => expect(error).toBeCalledTimes(1));
});
it('should show tag details metadata', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
expect(await screen.findByTestId('tagDetailsMetadata-container')).toBeInTheDocument();
});
it('renders vulnerability icons', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
expect(await screen.findByTestId('critical-vulnerability-icon')).toBeInTheDocument();
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageNone } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
expect(await screen.findByTestId('none-vulnerability-icon')).toBeInTheDocument();
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageUnknown } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
expect(await screen.findByTestId('unknown-vulnerability-icon')).toBeInTheDocument();
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageFailed } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
expect(await screen.findByTestId('failed-vulnerability-icon')).toBeInTheDocument();
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageLow } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
expect(await screen.findByTestId('low-vulnerability-icon')).toBeInTheDocument();
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageMedium } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
expect(await screen.findByTestId('medium-vulnerability-icon')).toBeInTheDocument();
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageHigh } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
expect(await screen.findByTestId('high-vulnerability-icon')).toBeInTheDocument();
});
it('should copy the docker pull string to clipboard', async () => {
jest
.spyOn(api, 'get')
.mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetails />);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetailsThemeWrapper />);
const dropdown = await screen.findByText('Pull Image');
expect(dropdown).toBeInTheDocument();
userEvent.click(dropdown);
@ -287,11 +293,8 @@ describe('Tags details', () => {
});
it('should copy the podman pull string to clipboard', async () => {
jest
.spyOn(api, 'get')
.mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetails />);
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetailsThemeWrapper />);
const dropdown = await screen.findByText('Pull Image');
expect(dropdown).toBeInTheDocument();
userEvent.click(dropdown);
@ -307,7 +310,7 @@ describe('Tags details', () => {
.spyOn(api, 'get')
.mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
const dropdown = await screen.findByText('Pull Image');
expect(dropdown).toBeInTheDocument();
userEvent.click(dropdown);
@ -320,7 +323,7 @@ describe('Tags details', () => {
it('should show pull tabs in dropdown and allow nagivation between them', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
const dropdown = await screen.findByText('Pull Image');
expect(dropdown).toBeInTheDocument();
userEvent.click(dropdown);
@ -333,7 +336,7 @@ describe('Tags details', () => {
it('should show the copied successfully button for 3 seconds', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
render(<TagDetails />);
render(<TagDetailsThemeWrapper />);
const dropdown = await screen.findByText('Pull Image');
expect(dropdown).toBeInTheDocument();
userEvent.click(dropdown);

View File

@ -16,6 +16,14 @@ jest.mock(
}
);
jest.mock(
'components/ExploreHeader',
() =>
function ExploreHeader() {
return <div />;
}
);
it('renders the tags page component', async () => {
render(
<BrowserRouter>

BIN
src/assets/zotLogoSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -7,7 +7,7 @@ import Loading from './Loading';
import Typography from '@mui/material/Typography';
import Sticky from 'react-sticky-el';
import Alert from '@mui/material/Alert';
import { Container, FormControl, Grid, InputLabel, MenuItem, Select, Stack } from '@mui/material';
import { Container, FormControl, Grid, InputLabel, MenuItem, Select, Stack, Button } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
@ -21,8 +21,9 @@ import { isEmpty } from 'lodash';
import filterConstants from 'utilities/filterConstants.js';
import { sortByCriteria } from 'utilities/sortCriteria.js';
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants.js';
import FilterDialog from './FilterDialog.jsx';
const useStyles = makeStyles(() => ({
const useStyles = makeStyles((theme) => ({
gridWrapper: {
paddingTop: '2rem',
paddingBottom: '2rem'
@ -52,6 +53,13 @@ const useStyles = makeStyles(() => ({
borderRadius: '0.375em',
width: '25%',
textAlign: 'left'
},
filterButton: {
borderRadius: '0.4rem',
marginBottom: '1rem',
[theme.breakpoints.up('md')]: {
display: 'none'
}
}
}));
@ -73,6 +81,9 @@ function Explore() {
const abortController = useMemo(() => new AbortController(), []);
const classes = useStyles();
// Filterdialog props
const [filterDialogOpen, setFilterDialogOpen] = useState(false);
const buildFilterQuery = () => {
let filter = {};
// workaround until backend bugfix
@ -194,6 +205,10 @@ function Explore() {
setSortFilter(event.target.value);
};
const handleFilterDialogOpen = () => {
setFilterDialogOpen(true);
};
const renderRepoCards = () => {
return (
exploreData &&
@ -250,7 +265,7 @@ function Explore() {
const renderListBottom = () => {
if (isLoading) {
return <Loading />;
return filterDialogOpen ? <div /> : <Loading />;
}
if (!isLoading && !isEndOfList) {
return <div ref={listBottom} />;
@ -262,16 +277,21 @@ function Explore() {
<Container maxWidth="lg">
<Grid container className={classes.gridWrapper}>
<Grid container item xs={12}>
<Grid item xs={3}></Grid>
<Grid item xs={9}>
<Grid item xs={3} className="hide-on-mobile"></Grid>
<Grid item xs={12} md={9}>
<Stack direction="row" className={classes.resultsRow}>
<Typography variant="body2" className={classes.results}>
<Typography variant="body2" className={`${classes.results} hide-on-mobile`}>
Showing {exploreData?.length} results out of {totalItems}
</Typography>
{!isLoading && (
<Button variant="contained" onClick={handleFilterDialogOpen} className={`${classes.filterButton}`}>
Filter results
</Button>
)}
<FormControl
sx={{ m: '1', minWidth: '4.6875rem' }}
sx={{ minWidth: '4.6875rem' }}
disabled={isLoading}
className={classes.sortForm}
className={`${classes.sortForm} hide-on-mobile`}
size="small"
>
<InputLabel>Sort</InputLabel>
@ -292,20 +312,20 @@ function Explore() {
</Grid>
</Grid>
<Grid container item xs={12} spacing={5} pt={1}>
<Grid item xs={3}>
<Grid item xs={3} md={3} className="hide-on-mobile">
<Sticky>{renderFilterCards()}</Sticky>
</Grid>
<Grid item xs={9}>
<Grid item xs={12} md={9}>
{!(exploreData && exploreData.length) && !isLoading ? (
<Grid container className={classes.nodataWrapper}>
<div style={{ marginTop: 20 }}>
<Alert style={{ marginTop: 10, width: '100%' }} variant="outlined" severity="warning">
<Alert style={{ marginTop: 10 }} variant="outlined" severity="warning">
Looks like we don&apos;t have anything matching that search. Try searching something else.
</Alert>
</div>
</Grid>
) : (
<Stack direction="column" spacing={2}>
<Stack direction="column" spacing={{ xs: 4, md: 2 }}>
{renderRepoCards()}
{renderListBottom()}
</Stack>
@ -313,6 +333,13 @@ function Explore() {
</Grid>
</Grid>
</Grid>
<FilterDialog
open={filterDialogOpen}
setOpen={setFilterDialogOpen}
sortValue={sortFilter}
setSortValue={setSortFilter}
renderFilterCards={renderFilterCards}
/>
</Container>
);
}

View File

@ -10,7 +10,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import makeStyles from '@mui/styles/makeStyles';
import React from 'react';
const useStyles = makeStyles(() => {
const useStyles = makeStyles((theme) => {
return {
exploreHeader: {
backgroundColor: '#FFFFFF',
@ -20,12 +20,18 @@ const useStyles = makeStyles(() => {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: '2rem'
padding: '2rem',
[theme.breakpoints.down('md')]: {
padding: '1rem'
}
},
explore: {
color: '#52637A',
fontSize: '1rem',
letterSpacing: '0.009375rem'
letterSpacing: '0.009375rem',
[theme.breakpoints.down('md')]: {
fontSize: '0.8rem'
}
}
};
});
@ -42,7 +48,10 @@ function ExploreHeader() {
return (
<div className={classes.exploreHeader}>
<ArrowBackIcon sx={{ color: '#14191F', fontSize: '2rem', cursor: 'pointer' }} onClick={() => navigate(-1)} />
<ArrowBackIcon
sx={{ color: '#14191F', fontSize: { xs: '1.5rem', md: '2rem' }, cursor: 'pointer' }}
onClick={() => navigate(-1)}
/>
<Breadcrumbs separator="/" aria-label="breadcrumb">
<Link to="/">
<Typography variant="body1" className={classes.explore}>

View File

@ -0,0 +1,66 @@
import React from 'react';
import { makeStyles } from '@mui/styles';
import {
Dialog,
DialogContent,
DialogContentText,
DialogTitle,
FormControl,
Select,
MenuItem,
DialogActions,
Button
} from '@mui/material';
import { sortByCriteria } from 'utilities/sortCriteria.js';
const useStyles = makeStyles(() => ({}));
function FilterDialog(props) {
const {
open,
setOpen,
sortValue,
setSortValue,
renderFilterCards
// imageFilters,
// setImageFilters,
// osFilters,
// setOsFilters,
// archFilters,
// setArchFilters
} = props;
const classes = useStyles();
const handleSortChange = (event) => {
setSortValue(event.target.value);
};
const handleClose = () => {
setOpen(false);
};
return (
<Dialog open={open} onClose={handleClose} fullScreen>
<DialogTitle>Filter</DialogTitle>
<DialogContent>
<DialogContentText>Sort results</DialogContentText>
<FormControl sx={{ m: '1', width: '80%' }} className={`${classes.sortForm}`} size="small">
<Select label="Sort" value={sortValue} onChange={handleSortChange} MenuProps={{ disableScrollLock: true }}>
{Object.values(sortByCriteria).map((el) => (
<MenuItem key={el.value} value={el.value}>
{el.label}
</MenuItem>
))}
</Select>
</FormControl>
{renderFilterCards()}
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Confirm</Button>
</DialogActions>
</Dialog>
);
}
export default FilterDialog;

View File

@ -3,25 +3,13 @@ import React from 'react';
import { Link, useLocation } from 'react-router-dom';
// components
import {
AppBar,
Toolbar,
Popper,
MenuList,
MenuItem,
ClickAwayListener,
Paper,
Grow,
Stack,
Grid
//IconButton
} from '@mui/material';
import { AppBar, Toolbar, Stack, Grid } from '@mui/material';
// styling
import makeStyles from '@mui/styles/makeStyles';
import logo from '../assets/zotLogo.svg';
//import placeholderProfileButton from '../assets/Profile_button_placeholder.svg';
import { useState, useRef, useEffect } from 'react';
import logoxs from '../assets/zotLogoSmall.png';
import { useState, useEffect } from 'react';
import SearchSuggestion from './SearchSuggestion';
const useStyles = makeStyles(() => ({
@ -39,13 +27,16 @@ const useStyles = makeStyles(() => ({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingLeft: 0,
padding: 0,
backgroundColor: '#fff',
height: '100%',
width: '100%',
borderBottom: '0.0625rem solid #BDBDBD',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)'
},
headerContainer: {
minWidth: '60%'
},
searchIcon: {
color: '#52637A',
paddingRight: '3%'
@ -66,7 +57,8 @@ const useStyles = makeStyles(() => ({
},
logoWrapper: {},
logo: {
width: '130px'
maxWidth: '130px',
maxHeight: '50px'
},
userAvatar: {
height: 46,
@ -78,7 +70,8 @@ const useStyles = makeStyles(() => ({
grid: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center'
justifyContent: 'center',
alignItems: 'center'
}
}));
@ -119,73 +112,32 @@ function Header() {
const path = useLocation().pathname;
// const navigate = useNavigate();
const [open, setOpen] = useState(false);
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
localStorage.removeItem('token');
window.location.reload();
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
return (
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '10vh' }}>
<Toolbar className={classes.header}>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ minWidth: '60%' }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" className={classes.headerContainer}>
<Grid container className={classes.grid}>
<Grid item xs={2} sx={{ display: 'flex', justifyContent: 'start' }}>
<Link to="/home" className={classes.grid}>
<img alt="zot" src={logo} className={classes.logo} />
{/* <img
alt="zot"
src={logo}
srcSet={`${logoxs} 192w, ${logo} 489w`}
sizes="(max-width: 480px) 192px, 489px"
className={classes.logo}
/> */}
<picture>
<source media="(min-width:600px)" srcSet={logo} />
<img alt="zot" src={logoxs} className={classes.logo} />
</picture>
</Link>
</Grid>
<Grid item xs={8}>
{path !== '/' && <SearchSuggestion />}
</Grid>
<Grid item xs={2}>
<Grid item md={2} xs={0}>
<div>{''}</div>
</Grid>
{/* <IconButton
ref={anchorRef}
id="composition-button"
aria-controls={open ? 'composition-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="true"
onClick={handleToggle}
>
<Avatar alt="profile" src={placeholderProfileButton} className={classes.userAvatar} variant="rounded" />
</IconButton> */}
<Popper
open={open}
anchorEl={anchorRef.current}
role={undefined}
placement="bottom-start"
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom-start' ? 'left top' : 'left bottom'
}}
>
<Paper>
<ClickAwayListener onClickAway={handleToggle}>
<MenuList autoFocusItem={open} id="composition-menu" aria-labelledby="composition-button">
<MenuItem onClick={handleClose}>Logout</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</Grid>
</Stack>
</Toolbar>

View File

@ -37,15 +37,6 @@ const useStyles = makeStyles(() => ({
textAlign: 'center',
letterSpacing: '-0.02rem'
},
titleRed: {
fontWeight: '700',
color: '#D83C0E',
width: '100%',
display: 'inline',
fontSize: '2.5rem',
textAlign: 'center',
letterSpacing: '-0.02rem'
},
sectionTitle: {
fontWeight: '700',
color: '#000000DE',
@ -63,8 +54,7 @@ const useStyles = makeStyles(() => ({
viewAll: {
color: '#00000099',
cursor: 'pointer',
textAlign: 'right',
width: '100%'
textAlign: 'left'
}
}));
@ -128,29 +118,6 @@ function Home() {
);
};
// const renderBookmarks = () => {
// return (
// homeData &&
// homeData.slice(0, 2).map((item, index) => {
// return (
// <RepoCard
// name={item.name}
// version={item.latestVersion}
// description={item.description}
// tags={item.tags}
// vendor={item.vendor}
// platforms={item.platforms}
// size={item.size}
// licenses={item.licenses}
// key={index}
// data={item}
// lastUpdated={item.lastUpdated}
// />
// );
// })
// );
// };
const renderRecentlyUpdated = () => {
return (
homeData &&
@ -185,31 +152,25 @@ function Home() {
<Stack spacing={4} alignItems="center" className={classes.gridWrapper}>
<Stack
justifyContent="space-between"
alignItems="end"
direction="row"
alignItems={{ xs: 'flex-start', md: 'flex-end' }}
direction={{ xs: 'column', md: 'row' }}
sx={{ width: '100%', paddingTop: '3rem' }}
>
<Typography variant="h4" align="left" className={classes.sectionTitle}>
Most popular images
</Typography>
<Typography
variant="body2"
className={classes.viewAll}
onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}
>
View all
</Typography>
<div>
<Typography variant="h4" align="left" className={classes.sectionTitle}>
Most popular images
</Typography>
</div>
<div className={classes.viewAll} onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}>
<Typography variant="body2">View all</Typography>
</div>
</Stack>
{renderMostPopular()}
{/* currently most popular will be by downloads until stars are implemented */}
{/* <Typography variant="h4" align="left" className={classes.sectionTitle}>
Bookmarks
</Typography>
{renderBookmarks()} */}
<Stack
justifyContent="space-between"
alignItems="end"
direction="row"
alignItems={{ xs: 'flex-start', md: 'flex-end' }}
direction={{ xs: 'column', md: 'row' }}
sx={{ width: '100%', paddingTop: '1rem' }}
>
<Typography variant="h4" align="left" className={classes.sectionTitle}>

View File

@ -168,7 +168,7 @@ function RepoCard(props) {
>
<CardContent className={classes.content}>
<Grid container>
<Grid item xs={10}>
<Grid item xs={12} md={10}>
<Stack alignItems="center" direction="row" spacing={2}>
<CardMedia
classes={{
@ -184,9 +184,12 @@ function RepoCard(props) {
{name}
</Typography>
</Tooltip>
<VulnerabilityIconCheck {...vulnerabilityData} />
<SignatureIconCheck isSigned={isSigned} />
{/* <Chip label="Verified licensee" sx={{ backgroundColor: "#E8F5E9", color: "#388E3C" }} variant="filled" onDelete={() => { return }} deleteIcon={vulnerabilityCheck()} /> */}
<div className="hide-on-mobile">
<VulnerabilityIconCheck {...vulnerabilityData} className="hide-on-mobile" />
</div>
<div className="hide-on-mobile">
<SignatureIconCheck isSigned={isSigned} className="hide-on-mobile" />
</div>
</Stack>
<Tooltip title={description || 'Description not available'} placement="top">
<Typography className={classes.versionLast} pt={1} sx={{ fontSize: 12 }} gutterBottom noWrap>
@ -197,12 +200,12 @@ function RepoCard(props) {
{platformChips()}
</Stack>
<Stack alignItems="center" direction="row" spacing={1} pt={2}>
<Tooltip title={getVendor()} placement="top">
<Tooltip title={getVendor()} placement="top" className="hide-on-mobile">
<Typography className={classes.vendor} variant="body2" noWrap>
{<Markdown options={{ forceInline: true }}>{getVendor()}</Markdown>}
</Typography>
</Tooltip>
<Tooltip title={getVersion()} placement="top">
<Tooltip title={getVersion()} placement="top" className="hide-on-mobile">
<Typography className={classes.versionLast} variant="body2" noWrap>
{getVersion()}
</Typography>
@ -214,7 +217,7 @@ function RepoCard(props) {
</Tooltip>
</Stack>
</Grid>
<Grid item xs={2}>
<Grid item xs={2} md={2} className="hide-on-mobile">
<Stack
alignItems="flex-end"
justifyContent="space-between"

View File

@ -24,7 +24,7 @@ import { isEmpty } from 'lodash';
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
const useStyles = makeStyles(() => ({
const useStyles = makeStyles((theme) => ({
pageWrapper: {
backgroundColor: '#FFFFFF',
height: '100vh'
@ -37,7 +37,6 @@ const useStyles = makeStyles(() => ({
},
repoName: {
fontWeight: '700',
fontSize: '2.5rem',
color: '#0F2139',
textAlign: 'left'
},
@ -56,7 +55,10 @@ const useStyles = makeStyles(() => ({
tabs: {
marginTop: '3rem',
padding: '0.5rem',
height: '100%'
height: '100%',
[theme.breakpoints.down('md')]: {
padding: '0'
}
},
tabContent: {
height: '100%'
@ -67,11 +69,18 @@ const useStyles = makeStyles(() => ({
},
tabPanel: {
height: '100%',
paddingLeft: '0rem!important'
paddingLeft: '0rem!important',
[theme.breakpoints.down('md')]: {
padding: '1.5rem 0'
}
},
metadata: {
marginTop: '8rem',
paddingLeft: '1.5rem'
paddingLeft: '1.5rem',
[theme.breakpoints.down('md')]: {
marginTop: '1rem',
paddingLeft: '0'
}
},
card: {
marginBottom: 2,
@ -106,7 +115,27 @@ const useStyles = makeStyles(() => ({
boxShadow: 'none!important'
},
header: {
paddingLeft: '2rem'
paddingLeft: '2rem',
[theme.breakpoints.down('md')]: {
padding: '0'
}
},
repoTitle: {
textAlign: 'left',
fontSize: '1rem',
lineHeight: '1.5rem',
color: 'rgba(0, 0, 0, 0.6)',
padding: '0.5rem 0 0 4rem',
[theme.breakpoints.down('md')]: {
padding: '0.5rem 0 0 0'
}
},
platformChipsContainer: {
alignItems: 'center',
padding: '0.5rem 0 0 4rem',
[theme.breakpoints.down('md')]: {
padding: '0.5rem 0 0 0'
}
}
}));
@ -205,7 +234,7 @@ function RepoDetails() {
color: 'rgba(0, 0, 0, 0.6)',
fontSize: '1rem',
lineHeight: '150%',
marginTop: '5%',
marginTop: '1.3rem',
alignSelf: 'stretch'
}}
>
@ -225,47 +254,46 @@ function RepoDetails() {
<Card className={classes.cardRoot}>
<CardContent>
<Grid container className={classes.header}>
<Grid item xs={8}>
<Stack alignItems="center" direction="row" spacing={2}>
<CardMedia
classes={{
root: classes.media,
img: classes.avatar
}}
component="img"
// eslint-disable-next-line prettier/prettier
<Grid item xs={12} md={8}>
<Stack alignItems="center" direction={{ xs: 'column', md: 'row' }} spacing={2}>
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
<CardMedia
classes={{
root: classes.media,
img: classes.avatar
}}
component="img"
// eslint-disable-next-line prettier/prettier
image={
!isEmpty(repoDetailData?.logo)
? `data:image/png;base64, ${repoDetailData?.logo}`
: randomImage()
}
alt="icon"
/>
<Typography variant="h3" className={classes.repoName}>
{name}
</Typography>
<VulnerabilityIconCheck
vulnerabilitySeverity={repoDetailData.vulnerabiltySeverity}
count={repoDetailData?.vulnerabilityCount}
/>
<SignatureIconCheck isSigned={repoDetailData.isSigned} />
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
!isEmpty(repoDetailData?.logo)
? `data:image/png;base64, ${repoDetailData?.logo}`
: randomImage()
}
alt="icon"
/>
<Typography variant="h4" className={classes.repoName}>
{name}
</Typography>
</Stack>
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
<VulnerabilityIconCheck
vulnerabilitySeverity={repoDetailData.vulnerabiltySeverity}
count={repoDetailData?.vulnerabilityCount}
/>
<SignatureIconCheck isSigned={repoDetailData.isSigned} />
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
</Stack>
</Stack>
<Typography
pt={1}
sx={{ fontSize: 16, lineHeight: '1.5rem', color: 'rgba(0, 0, 0, 0.6)', paddingLeft: '4rem' }}
gutterBottom
align="left"
>
<Typography gutterBottom className={classes.repoTitle}>
{repoDetailData?.title || 'Title not available'}
</Typography>
<Stack alignItems="center" sx={{ paddingLeft: '4rem' }} direction="row" spacing={2} pt={1}>
<Stack direction="row" spacing={2} className={classes.platformChipsContainer}>
{platformChips()}
</Stack>
</Grid>
</Grid>
<Grid container>
<Grid item xs={8} className={classes.tabs}>
<Grid item xs={12} md={8} className={classes.tabs}>
<TabContext value={selectedTab}>
<Box>
<TabList
@ -289,7 +317,7 @@ function RepoDetails() {
</Box>
</TabContext>
</Grid>
<Grid item xs={4} className={classes.metadata}>
<Grid item xs={12} md={4} className={classes.metadata}>
<RepoDetailsMetadata
totalDownloads={repoDetailData?.downloads}
repoURL={repoDetailData?.source}

View File

@ -127,28 +127,28 @@ export default function TagCard(props) {
<Collapse in={open} timeout="auto" unmountOnExit>
<Box>
<Grid container item xs={12} direction={'row'}>
<Grid item xs={4}>
<Grid item xs={6} md={4}>
<Typography variant="body1">DIGEST</Typography>
</Grid>
<Grid item xs={4} sx={{ display: 'flex', justifyContent: 'center' }}>
<Grid item xs={6} md={4} sx={{ display: 'flex', justifyContent: 'center' }}>
<Typography variant="body1">OS/Arch</Typography>
</Grid>
<Grid item xs={4} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Grid item xs={0} md={4} className="hide-on-mobile" sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Typography variant="body1"> Size </Typography>
</Grid>
</Grid>
<Grid container item xs={12} direction={'row'}>
<Grid item xs={4}>
<Grid item xs={6} md={4}>
<Tooltip title={digest || ''} placement="top">
<Typography variant="body1">{digest?.substr(0, 12)}</Typography>
</Tooltip>
</Grid>
<Grid item xs={4} sx={{ display: 'flex', justifyContent: 'center' }}>
<Grid item xs={6} md={4} sx={{ display: 'flex', justifyContent: 'center' }}>
<Typography variant="body1">
{platform?.Os}/{platform?.Arch}
</Typography>
</Grid>
<Grid item xs={4} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Grid item xs={0} md={4} className="hide-on-mobile" sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Typography sx={{ textAlign: 'right' }} variant="body1">
{transform.formatBytes(size)}
</Typography>

View File

@ -42,7 +42,7 @@ import Loading from './Loading';
import { dockerPull, podmanPull, skopeoPull } from 'utilities/pullStrings';
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
const useStyles = makeStyles(() => ({
const useStyles = makeStyles((theme) => ({
pageWrapper: {
backgroundColor: '#FFFFFF',
height: '100vh'
@ -55,7 +55,6 @@ const useStyles = makeStyles(() => ({
},
repoName: {
fontWeight: '700',
fontSize: '2.5rem',
color: '#0F2139',
textAlign: 'left'
},
@ -64,13 +63,28 @@ const useStyles = makeStyles(() => ({
width: '3rem',
objectFit: 'fill'
},
digest: {
textAlign: 'left',
fontSize: '1rem',
lineHeight: '1.5rem',
color: 'rgba(0, 0, 0, 0.6)',
padding: '0.5rem 0 0 4rem',
maxWidth: '100%',
[theme.breakpoints.down('md')]: {
padding: '0.5rem 0 0 0',
fontSize: '0.5rem'
}
},
media: {
borderRadius: '3.125em'
},
tabs: {
marginTop: '3rem',
padding: '0.5rem',
height: '100%'
height: '100%',
[theme.breakpoints.down('md')]: {
padding: '0'
}
},
tabContent: {
height: '100%'
@ -86,11 +100,17 @@ const useStyles = makeStyles(() => ({
tabPanel: {
height: '100%',
paddingLeft: '0rem!important',
marginRight: '2rem!important'
[theme.breakpoints.down('md')]: {
padding: '1.5rem 0'
}
},
metadata: {
marginTop: '8rem',
paddingLeft: '1.5rem'
paddingLeft: '1.5rem',
[theme.breakpoints.down('md')]: {
marginTop: '1rem',
paddingLeft: '0'
}
},
pull: {
paddingLeft: '1.5rem',
@ -123,7 +143,10 @@ const useStyles = makeStyles(() => ({
boxShadow: 'none!important'
},
header: {
paddingLeft: '2rem'
paddingLeft: '2rem',
[theme.breakpoints.down('md')]: {
padding: '0'
}
},
tabBox: {
padding: '0.5rem'
@ -259,41 +282,46 @@ function TagDetails() {
<Card className={classes.cardRoot}>
<CardContent>
<Grid container>
<Grid item xs={8} className={classes.header}>
<Stack alignItems="center" direction="row" spacing={2}>
<CardMedia
classes={{
root: classes.media,
img: classes.avatar
}}
component="img"
image={
!isEmpty(imageDetailData?.logo)
? `data:image/ png;base64, ${imageDetailData?.logo}`
: randomImage()
}
alt="icon"
/>
<Typography variant="h3" className={classes.repoName}>
{reponame}:{tag}
</Typography>
<VulnerabilityIconCheck
vulnerabilitySeverity={imageDetailData.vulnerabiltySeverity}
count={imageDetailData.vulnerabilityCount}
/>
<SignatureIconCheck isSigned={imageDetailData.isSigned} />
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
</Stack>
<Typography
pt={1}
sx={{ fontSize: 16, lineHeight: '1.5rem', color: 'rgba(0, 0, 0, 0.6)', paddingLeft: '4rem' }}
gutterBottom
align="left"
<Grid item xs={12} md={8} className={classes.header}>
<Stack
alignItems="center"
sx={{ width: { xs: '100%', md: 'auto' } }}
direction={{ xs: 'column', md: 'row' }}
spacing={2}
>
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
<CardMedia
classes={{
root: classes.media,
img: classes.avatar
}}
component="img"
image={
!isEmpty(imageDetailData?.logo)
? `data:image/ png;base64, ${imageDetailData?.logo}`
: randomImage()
}
alt="icon"
/>
<Typography variant="h4" className={classes.repoName}>
<span className="hide-on-mobile">{reponame}</span>:{tag}
</Typography>
</Stack>
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
<VulnerabilityIconCheck
vulnerabilitySeverity={imageDetailData.vulnerabiltySeverity}
count={imageDetailData.vulnerabilityCount}
/>
<SignatureIconCheck isSigned={imageDetailData.isSigned} />
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
</Stack>
</Stack>
<Typography gutterBottom className={classes.digest}>
DIGEST: {imageDetailData?.digest}
</Typography>
</Grid>
<Grid item xs={4} className={classes.pull}>
<Grid item xs={0} md={4} className={`${classes.pull} hide-on-mobile`}>
{isCopied ? (
<Button className={classes.pullStringBoxCopied} data-testid="successPulled-buton">
Copied Pull Command
@ -465,13 +493,14 @@ function TagDetails() {
</Grid>
</Grid>
<Grid container>
<Grid item xs={8} className={classes.tabs}>
<Grid item xs={12} md={8} className={classes.tabs}>
<TabContext value={selectedTab}>
<Box>
<TabList
onChange={handleTabChange}
TabIndicatorProps={{ className: classes.selectedTab }}
sx={{ '& button.Mui-selected': { color: '#14191F', fontWeight: '600' } }}
variant="scrollable"
>
<Tab value="Layers" label="Layers" className={classes.tabContent} />
<Tab
@ -502,7 +531,7 @@ function TagDetails() {
</Box>
</TabContext>
</Grid>
<Grid item xs={4} className={classes.metadata}>
<Grid item xs={12} md={4} className={classes.metadata}>
<TagDetailsMetadata
platform={getPlatform()}
size={imageDetailData?.size}

View File

@ -25,6 +25,13 @@ const theme = createTheme(
})
);
theme.typography.h4 = {
fontSize: '2.5rem',
[theme.breakpoints.down('sm')]: {
fontSize: '1.5rem'
}
};
ReactDOM.render(
<React.StrictMode>
<StyledEngineProvider injectFirst>