Compare commits
2 Commits
commit-089
...
commit-63f
Author | SHA1 | Date | |
---|---|---|---|
63ff8dabc0 | |||
f9cafd0b90 |
7
.github/workflows/end-to-end-test.yml
vendored
7
.github/workflows/end-to-end-test.yml
vendored
@ -61,6 +61,13 @@ jobs:
|
|||||||
sudo mv cosign /usr/local/bin/cosign
|
sudo mv cosign /usr/local/bin/cosign
|
||||||
which cosign
|
which cosign
|
||||||
cosign version
|
cosign version
|
||||||
|
pushd $(mktemp -d)
|
||||||
|
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.38.3/trivy_0.38.3_Linux-64bit.tar.gz -o trivy.tar.gz
|
||||||
|
tar -xzvf trivy.tar.gz
|
||||||
|
sudo mv trivy /usr/local/bin/trivy
|
||||||
|
popd
|
||||||
|
which trivy
|
||||||
|
trivy version
|
||||||
cd $GITHUB_WORKSPACE
|
cd $GITHUB_WORKSPACE
|
||||||
|
|
||||||
- name: Install go
|
- name: Install go
|
||||||
|
@ -11,6 +11,14 @@ jest.mock(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'components/Header/Header',
|
||||||
|
() =>
|
||||||
|
function Header() {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it('renders the explore page component', () => {
|
it('renders the explore page component', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
@ -4,6 +4,7 @@ import Home from 'components/Home/Home';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createSearchParams } from 'react-router-dom';
|
import { createSearchParams } from 'react-router-dom';
|
||||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||||
|
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||||
|
|
||||||
// useNavigate mock
|
// useNavigate mock
|
||||||
const mockedUsedNavigate = jest.fn();
|
const mockedUsedNavigate = jest.fn();
|
||||||
@ -12,6 +13,14 @@ jest.mock('react-router-dom', () => ({
|
|||||||
useNavigate: () => mockedUsedNavigate
|
useNavigate: () => mockedUsedNavigate
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const HomeWrapper = () => {
|
||||||
|
return (
|
||||||
|
<MockThemeProvier>
|
||||||
|
<Home />
|
||||||
|
</MockThemeProvier>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const mockImageList = {
|
const mockImageList = {
|
||||||
GlobalSearch: {
|
GlobalSearch: {
|
||||||
Page: { TotalCount: 6, ItemCount: 3 },
|
Page: { TotalCount: 6, ItemCount: 3 },
|
||||||
@ -126,7 +135,7 @@ describe('Home component', () => {
|
|||||||
it('fetches image data and renders popular, bookmarks and recently updated', async () => {
|
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: mockImageList } });
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||||
render(<Home />);
|
render(<HomeWrapper />);
|
||||||
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(2));
|
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(2));
|
||||||
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(2));
|
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(2));
|
||||||
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
|
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
|
||||||
@ -135,7 +144,7 @@ describe('Home component', () => {
|
|||||||
it('renders signature icons', async () => {
|
it('renders signature icons', async () => {
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||||
render(<Home />);
|
render(<HomeWrapper />);
|
||||||
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(2);
|
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(2);
|
||||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(3);
|
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(3);
|
||||||
});
|
});
|
||||||
@ -143,7 +152,7 @@ describe('Home component', () => {
|
|||||||
it('renders vulnerability icons', async () => {
|
it('renders vulnerability icons', async () => {
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||||
render(<Home />);
|
render(<HomeWrapper />);
|
||||||
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(2);
|
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(2);
|
||||||
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(2);
|
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(2);
|
||||||
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
|
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
|
||||||
@ -152,14 +161,14 @@ describe('Home component', () => {
|
|||||||
it("should log an error when data can't be fetched", async () => {
|
it("should log an error when data can't be fetched", async () => {
|
||||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
render(<Home />);
|
render(<HomeWrapper />);
|
||||||
await waitFor(() => expect(error).toBeCalledTimes(2));
|
await waitFor(() => expect(error).toBeCalledTimes(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to explore page when clicking view all popular', async () => {
|
it('should redirect to explore page when clicking view all popular', async () => {
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||||
render(<Home />);
|
render(<HomeWrapper />);
|
||||||
const viewAllButtons = await screen.findAllByText(/view all/i);
|
const viewAllButtons = await screen.findAllByText(/view all/i);
|
||||||
expect(viewAllButtons).toHaveLength(2);
|
expect(viewAllButtons).toHaveLength(2);
|
||||||
fireEvent.click(viewAllButtons[0]);
|
fireEvent.click(viewAllButtons[0]);
|
||||||
|
@ -11,6 +11,14 @@ jest.mock(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'components/Header/Header',
|
||||||
|
() =>
|
||||||
|
function Header() {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it('renders the homepage component', () => {
|
it('renders the homepage component', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import RepoCard from 'components/Shared/RepoCard';
|
import RepoCard from 'components/Shared/RepoCard';
|
||||||
import { createSearchParams } from 'react-router-dom';
|
import { createSearchParams } from 'react-router-dom';
|
||||||
|
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||||
|
|
||||||
// usenavigate mock
|
// usenavigate mock
|
||||||
const mockedUsedNavigate = jest.fn();
|
const mockedUsedNavigate = jest.fn();
|
||||||
@ -24,6 +25,23 @@ const mockImage = {
|
|||||||
platforms: [{ Os: 'linux', Arch: 'amd64' }]
|
platforms: [{ Os: 'linux', Arch: 'amd64' }]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RepoCardWrapper = (props) => {
|
||||||
|
const { image } = props;
|
||||||
|
return (
|
||||||
|
<MockThemeProvier>
|
||||||
|
<RepoCard
|
||||||
|
name={image.name}
|
||||||
|
version={image.latestVersion}
|
||||||
|
description={image.description}
|
||||||
|
vendor={image.vendor}
|
||||||
|
key={1}
|
||||||
|
lastUpdated={image.lastUpdated}
|
||||||
|
platforms={image.platforms}
|
||||||
|
/>
|
||||||
|
</MockThemeProvier>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// restore the spy created with spyOn
|
// restore the spy created with spyOn
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
@ -31,16 +49,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
describe('Repo card component', () => {
|
describe('Repo card component', () => {
|
||||||
it('navigates to repo page when clicked', async () => {
|
it('navigates to repo page when clicked', async () => {
|
||||||
render(
|
render(<RepoCardWrapper image={mockImage} />);
|
||||||
<RepoCard
|
|
||||||
name={mockImage.name}
|
|
||||||
version={mockImage.latestVersion}
|
|
||||||
description={mockImage.description}
|
|
||||||
vendor={mockImage.vendor}
|
|
||||||
key={1}
|
|
||||||
lastUpdated={mockImage.lastUpdated}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const cardTitle = await screen.findByText('alpine');
|
const cardTitle = await screen.findByText('alpine');
|
||||||
expect(cardTitle).toBeInTheDocument();
|
expect(cardTitle).toBeInTheDocument();
|
||||||
userEvent.click(cardTitle);
|
userEvent.click(cardTitle);
|
||||||
@ -48,15 +57,7 @@ describe('Repo card component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders placeholders for missing data', async () => {
|
it('renders placeholders for missing data', async () => {
|
||||||
render(
|
render(<RepoCardWrapper image={{ ...mockImage, lastUpdated: '' }} />);
|
||||||
<RepoCard
|
|
||||||
name={mockImage.name}
|
|
||||||
version={mockImage.latestVersion}
|
|
||||||
description={mockImage.description}
|
|
||||||
vendor={mockImage.vendor}
|
|
||||||
key={1}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const cardTitle = await screen.findByText('alpine');
|
const cardTitle = await screen.findByText('alpine');
|
||||||
expect(cardTitle).toBeInTheDocument();
|
expect(cardTitle).toBeInTheDocument();
|
||||||
userEvent.click(cardTitle);
|
userEvent.click(cardTitle);
|
||||||
@ -65,17 +66,7 @@ describe('Repo card component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('navigates to explore page when platform chip is clicked', async () => {
|
it('navigates to explore page when platform chip is clicked', async () => {
|
||||||
render(
|
render(<RepoCardWrapper image={mockImage} />);
|
||||||
<RepoCard
|
|
||||||
name={mockImage.name}
|
|
||||||
version={mockImage.latestVersion}
|
|
||||||
description={mockImage.description}
|
|
||||||
vendor={mockImage.vendor}
|
|
||||||
key={1}
|
|
||||||
lastUpdated={mockImage.lastUpdated}
|
|
||||||
platforms={mockImage.platforms}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const osChip = await screen.findByText(/linux/i);
|
const osChip = await screen.findByText(/linux/i);
|
||||||
fireEvent.click(osChip);
|
fireEvent.click(osChip);
|
||||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||||
|
@ -82,6 +82,5 @@ describe('Referred by tab', () => {
|
|||||||
await userEvent.click(firstAnnotations);
|
await userEvent.click(firstAnnotations);
|
||||||
expect(await screen.findByText(/demo: true/i)).toBeInTheDocument();
|
expect(await screen.findByText(/demo: true/i)).toBeInTheDocument();
|
||||||
await userEvent.click(firstAnnotations);
|
await userEvent.click(firstAnnotations);
|
||||||
expect(await screen.findByText(/demo: true/i)).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,6 +24,14 @@ jest.mock(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'components/Header/Header',
|
||||||
|
() =>
|
||||||
|
function Header() {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it('renders the tags page component', async () => {
|
it('renders the tags page component', async () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
@ -525,7 +525,7 @@ describe('Vulnerabilties page', () => {
|
|||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1)
|
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1)
|
||||||
);
|
);
|
||||||
fireEvent.click(openText[0]);
|
await fireEvent.click(openText[0]);
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.queryByText(/CPAN 2.28 allows Signature Verification Bypass./i)).not.toBeInTheDocument()
|
expect(screen.queryByText(/CPAN 2.28 allows Signature Verification Bypass./i)).not.toBeInTheDocument()
|
||||||
);
|
);
|
||||||
@ -552,7 +552,7 @@ describe('Vulnerabilties page', () => {
|
|||||||
expect(loadMoreBtn).toBeInTheDocument();
|
expect(loadMoreBtn).toBeInTheDocument();
|
||||||
await fireEvent.click(loadMoreBtn);
|
await fireEvent.click(loadMoreBtn);
|
||||||
await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument());
|
await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument());
|
||||||
await expect(await screen.findByText('latest')).toBeInTheDocument();
|
expect(await screen.findByText('latest')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle fixed CVE query errors', async () => {
|
it('should handle fixed CVE query errors', async () => {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 30 KiB |
14
src/assets/zotLogoWhite.svg
Normal file
14
src/assets/zotLogoWhite.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 32 KiB |
8
src/assets/zotLogoWhiteSmall.svg
Normal file
8
src/assets/zotLogoWhiteSmall.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
@ -60,6 +60,11 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up('md')]: {
|
||||||
display: 'none'
|
display: 'none'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
filterCardsContainer: {
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -311,7 +316,7 @@ function Explore({ searchInputValue }) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid container item xs={12} spacing={5} pt={1}>
|
<Grid container item xs={12} spacing={5} pt={1}>
|
||||||
<Grid item xs={3} md={3} className="hide-on-mobile">
|
<Grid item xs={3} md={3} className={classes.filterCardsContainer}>
|
||||||
<Sticky>{renderFilterCards()}</Sticky>
|
<Sticky>{renderFilterCards()}</Sticky>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={9}>
|
<Grid item xs={12} md={9}>
|
||||||
|
@ -19,7 +19,7 @@ const useStyles = makeStyles((theme) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'flex-start',
|
||||||
padding: '2rem',
|
padding: '2rem',
|
||||||
[theme.breakpoints.down('md')]: {
|
[theme.breakpoints.down('md')]: {
|
||||||
padding: '1rem'
|
padding: '1rem'
|
||||||
@ -27,7 +27,8 @@ const useStyles = makeStyles((theme) => {
|
|||||||
},
|
},
|
||||||
explore: {
|
explore: {
|
||||||
color: '#52637A',
|
color: '#52637A',
|
||||||
fontSize: '1rem',
|
fontSize: '0.813rem',
|
||||||
|
fontWeight: '600',
|
||||||
letterSpacing: '0.009375rem',
|
letterSpacing: '0.009375rem',
|
||||||
[theme.breakpoints.down('md')]: {
|
[theme.breakpoints.down('md')]: {
|
||||||
fontSize: '0.8rem'
|
fontSize: '0.8rem'
|
||||||
@ -49,7 +50,7 @@ function ExploreHeader() {
|
|||||||
return (
|
return (
|
||||||
<div className={classes.exploreHeader}>
|
<div className={classes.exploreHeader}>
|
||||||
<ArrowBackIcon
|
<ArrowBackIcon
|
||||||
sx={{ color: '#14191F', fontSize: { xs: '1.5rem', md: '2rem' }, cursor: 'pointer' }}
|
sx={{ color: '#52637A', marginRight: '1.75rem', fontSize: { xs: '1.5rem', md: '2rem' }, cursor: 'pointer' }}
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
/>
|
/>
|
||||||
<Breadcrumbs separator="/" aria-label="breadcrumb">
|
<Breadcrumbs separator="/" aria-label="breadcrumb">
|
||||||
|
@ -3,16 +3,17 @@ import React from 'react';
|
|||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { AppBar, Toolbar, Stack, Grid } from '@mui/material';
|
import { AppBar, Toolbar, Grid } from '@mui/material';
|
||||||
|
|
||||||
// styling
|
// styling
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
import logo from '../../assets/zotLogo.svg';
|
import logo from '../../assets/zotLogoWhite.svg';
|
||||||
import logoxs from '../../assets/zotLogoSmall.png';
|
import logoxs from '../../assets/zotLogoWhiteSmall.svg';
|
||||||
|
import githubLogo from '../../assets/Git.png';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import SearchSuggestion from './SearchSuggestion';
|
import SearchSuggestion from './SearchSuggestion';
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
barOpen: {
|
barOpen: {
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
minHeight: '10%'
|
minHeight: '10%'
|
||||||
@ -28,7 +29,7 @@ const useStyles = makeStyles(() => ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#0F2139',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
borderBottom: '0.0625rem solid #BDBDBD',
|
borderBottom: '0.0625rem solid #BDBDBD',
|
||||||
@ -58,20 +59,41 @@ const useStyles = makeStyles(() => ({
|
|||||||
logoWrapper: {},
|
logoWrapper: {},
|
||||||
logo: {
|
logo: {
|
||||||
maxWidth: '130px',
|
maxWidth: '130px',
|
||||||
maxHeight: '50px'
|
maxHeight: '30px'
|
||||||
},
|
},
|
||||||
userAvatar: {
|
headerLinkContainer: {
|
||||||
height: 46,
|
[theme.breakpoints.down('md')]: {
|
||||||
width: 46
|
display: 'none'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
color: '#000'
|
color: '#F6F7F9',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 600
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '2.875rem',
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gridItem: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
signInBtn: {
|
||||||
|
border: '1px solid #F6F7F9',
|
||||||
|
borderRadius: '0.625rem',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: '#F6F7F9',
|
||||||
|
fontSize: '1rem',
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: 600
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -109,26 +131,45 @@ function Header({ setSearchCurrentValue = () => {} }) {
|
|||||||
const path = useLocation().pathname;
|
const path = useLocation().pathname;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '10vh' }}>
|
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '5rem' }}>
|
||||||
<Toolbar className={classes.header}>
|
<Toolbar className={classes.header}>
|
||||||
<Stack direction="row" alignItems="center" justifyContent="space-between" className={classes.headerContainer}>
|
<Grid container className={classes.grid}>
|
||||||
<Grid container className={classes.grid}>
|
<Grid item container xs={3} md={4} spacing="1.5rem" className={classes.gridItem}>
|
||||||
<Grid item xs={2} sx={{ display: 'flex', justifyContent: 'start' }}>
|
<Grid item>
|
||||||
<Link to="/home" className={classes.grid}>
|
<Link to="/home">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(min-width:600px)" srcSet={logo} />
|
<source media="(min-width:600px)" srcSet={logo} />
|
||||||
<img alt="zot" src={logoxs} className={classes.logo} />
|
<img alt="zot" src={logoxs} className={classes.logo} />
|
||||||
</picture>
|
</picture>
|
||||||
</Link>
|
</Link>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={8}>
|
<Grid item className={classes.headerLinkContainer}>
|
||||||
{path !== '/' && <SearchSuggestion setSearchCurrentValue={setSearchCurrentValue} />}
|
<a className={classes.link} href="https://zotregistry.io" target="_blank" rel="noreferrer">
|
||||||
|
Product
|
||||||
|
</a>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={2} xs={0}>
|
<Grid item className={classes.headerLinkContainer}>
|
||||||
<div>{''}</div>
|
<a
|
||||||
|
className={classes.link}
|
||||||
|
href="https://zotregistry.io/v1.4.3/general/concepts/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Docs
|
||||||
|
</a>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
<Grid item xs={6} md={4} className={classes.gridItem}>
|
||||||
|
{path !== '/' && <SearchSuggestion setSearchCurrentValue={setSearchCurrentValue} />}
|
||||||
|
</Grid>
|
||||||
|
<Grid item container xs={2} md={3} spacing="1.5rem" className={`${classes.gridItem}`}>
|
||||||
|
<Grid item className={classes.headerLinkContainer}>
|
||||||
|
<a className={classes.link} href="https://github.com/project-zot/zot" target="_blank" rel="noreferrer">
|
||||||
|
<img alt="github repository" src={githubLogo} className={classes.logo} />
|
||||||
|
</a>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
|
@ -14,31 +14,30 @@ import { HEADER_SEARCH_PAGE_SIZE } from 'utilities/paginationConstants';
|
|||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles(() => ({
|
||||||
searchContainer: {
|
searchContainer: {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#2B3A4E',
|
||||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
boxShadow: '0 0.313rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||||
borderRadius: '2.5rem',
|
borderRadius: '0.625rem',
|
||||||
minWidth: '60%',
|
minWidth: '100%',
|
||||||
marginLeft: 16,
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 1150
|
zIndex: 1150
|
||||||
},
|
},
|
||||||
|
searchContainerFocused: {
|
||||||
|
backgroundColor: '#FFFFFF'
|
||||||
|
},
|
||||||
search: {
|
search: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
minWidth: '100%',
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||||
border: '0.125rem solid #E7E7E7',
|
border: '0.063rem solid #8A96A8',
|
||||||
borderRadius: '2.5rem',
|
borderRadius: '0.625rem',
|
||||||
zIndex: 1155
|
zIndex: 1155
|
||||||
},
|
},
|
||||||
|
searchFocused: {
|
||||||
|
border: '0.125rem solid #E0E5EB',
|
||||||
|
backgroundColor: '#FFFFF'
|
||||||
|
},
|
||||||
searchFailed: {
|
searchFailed: {
|
||||||
position: 'relative',
|
border: '0.125rem solid #ff0303'
|
||||||
minWidth: '100%',
|
|
||||||
flexDirection: 'row',
|
|
||||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
|
||||||
border: '0.125rem solid #ff0303',
|
|
||||||
borderRadius: '2.5rem',
|
|
||||||
zIndex: 1155
|
|
||||||
},
|
},
|
||||||
resultsWrapper: {
|
resultsWrapper: {
|
||||||
margin: '0',
|
margin: '0',
|
||||||
@ -47,16 +46,19 @@ const useStyles = makeStyles(() => ({
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#2B3A4E',
|
||||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||||
borderBottomLeftRadius: '2.5rem',
|
borderBottomLeftRadius: '0.625rem',
|
||||||
borderBottomRightRadius: '2.5rem',
|
borderBottomRightRadius: '0.625rem',
|
||||||
// border: '0.125rem solid #E7E7E7',
|
// border: '0.125rem solid #E7E7E7',
|
||||||
borderTop: 0,
|
borderTop: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
zIndex: 1
|
zIndex: 1
|
||||||
},
|
},
|
||||||
|
resultsWrapperFocused: {
|
||||||
|
backgroundColor: '#FFFFFF'
|
||||||
|
},
|
||||||
resultsWrapperHidden: {
|
resultsWrapperHidden: {
|
||||||
display: 'none'
|
display: 'none'
|
||||||
},
|
},
|
||||||
@ -66,9 +68,19 @@ const useStyles = makeStyles(() => ({
|
|||||||
cursor: 'pointer'
|
cursor: 'pointer'
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
color: '#464141',
|
|
||||||
marginLeft: 1,
|
marginLeft: 1,
|
||||||
width: '90%'
|
width: '90%',
|
||||||
|
paddingLeft: 10,
|
||||||
|
height: '40px',
|
||||||
|
fontSize: '1rem',
|
||||||
|
backgroundColor: '#2B3A4E',
|
||||||
|
borderRadius: '0.625rem',
|
||||||
|
color: '#8A96A8'
|
||||||
|
},
|
||||||
|
inputFocused: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: '0.625rem',
|
||||||
|
color: 'rgba(0, 0, 0, 0.6);'
|
||||||
},
|
},
|
||||||
searchItem: {
|
searchItem: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -102,6 +114,7 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
|||||||
const search = queryParams.get('search') || '';
|
const search = queryParams.get('search') || '';
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isFailedSearch, setIsFailedSearch] = useState(false);
|
const [isFailedSearch, setIsFailedSearch] = useState(false);
|
||||||
|
const [isComponentFocused, setIsComponentFocused] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const abortController = useMemo(() => new AbortController(), []);
|
const abortController = useMemo(() => new AbortController(), []);
|
||||||
|
|
||||||
@ -217,15 +230,18 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
|||||||
getComboboxProps,
|
getComboboxProps,
|
||||||
isOpen,
|
isOpen,
|
||||||
openMenu
|
openMenu
|
||||||
// closeMenu
|
|
||||||
} = useCombobox({
|
} = useCombobox({
|
||||||
items: suggestionData,
|
items: suggestionData,
|
||||||
onInputValueChange: handleSeachChange,
|
onInputValueChange: handleSeachChange,
|
||||||
onSelectedItemChange: handleSuggestionSelected,
|
onSelectedItemChange: handleSuggestionSelected,
|
||||||
initialInputValue: !isEmpty(searchQuery) ? searchQuery : search,
|
initialInputValue: !isEmpty(searchQuery) ? searchQuery : search,
|
||||||
itemToString: (item) => item.name ?? item
|
itemToString: (item) => item?.name || item
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsComponentFocused(isOpen);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const renderSuggestions = () => {
|
const renderSuggestions = () => {
|
||||||
return suggestionData.map((suggestion, index) => (
|
return suggestionData.map((suggestion, index) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -253,9 +269,11 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.searchContainer}>
|
<div className={`${classes.searchContainer} ${isComponentFocused && classes.searchContainerFocused}`}>
|
||||||
<Stack
|
<Stack
|
||||||
className={isFailedSearch && !isLoading ? classes.searchFailed : classes.search}
|
className={`${classes.search} ${isComponentFocused && classes.searchFocused} ${
|
||||||
|
isFailedSearch && !isLoading && classes.searchFailed
|
||||||
|
}`}
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
@ -263,9 +281,8 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
|||||||
{...getComboboxProps()}
|
{...getComboboxProps()}
|
||||||
>
|
>
|
||||||
<InputBase
|
<InputBase
|
||||||
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
|
|
||||||
placeholder={'Search for content...'}
|
placeholder={'Search for content...'}
|
||||||
className={classes.input}
|
className={`${classes.input} ${isComponentFocused && classes.inputFocused}`}
|
||||||
onKeyUp={handleSearch}
|
onKeyUp={handleSearch}
|
||||||
onFocus={() => openMenu()}
|
onFocus={() => openMenu()}
|
||||||
{...getInputProps()}
|
{...getInputProps()}
|
||||||
@ -276,7 +293,11 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
<List
|
<List
|
||||||
{...getMenuProps()}
|
{...getMenuProps()}
|
||||||
className={isOpen && !isLoading && !isFailedSearch ? classes.resultsWrapper : classes.resultsWrapperHidden}
|
className={
|
||||||
|
isOpen && !isLoading && !isFailedSearch
|
||||||
|
? `${classes.resultsWrapper} ${isComponentFocused && classes.resultsWrapperFocused}`
|
||||||
|
: classes.resultsWrapperHidden
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isOpen && suggestionData?.length > 0 && renderSuggestions()}
|
{isOpen && suggestionData?.length > 0 && renderSuggestions()}
|
||||||
{isOpen && isEmpty(searchQuery) && isEmpty(suggestionData) && (
|
{isOpen && isEmpty(searchQuery) && isEmpty(suggestionData) && (
|
||||||
|
@ -40,8 +40,13 @@ const useStyles = makeStyles(() => ({
|
|||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#000000DE',
|
color: '#0F2139',
|
||||||
width: '100%'
|
width: '100%',
|
||||||
|
fontSize: '2rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: '2.375rem',
|
||||||
|
letterSpacing: '-0.01rem',
|
||||||
|
marginLeft: '0.5rem'
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
color: '#00000099',
|
color: '#00000099',
|
||||||
@ -53,9 +58,12 @@ const useStyles = makeStyles(() => ({
|
|||||||
width: '65%'
|
width: '65%'
|
||||||
},
|
},
|
||||||
viewAll: {
|
viewAll: {
|
||||||
color: '#00000099',
|
color: '#52637A',
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: '1rem',
|
||||||
|
lineHeight: '1.5rem',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
textAlign: 'left'
|
marginRight: '0.5rem'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -191,7 +199,7 @@ function Home() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
<Stack spacing={4} alignItems="center" className={classes.gridWrapper}>
|
<Stack alignItems="center" className={classes.gridWrapper}>
|
||||||
<Stack
|
<Stack
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems={{ xs: 'flex-start', md: 'flex-end' }}
|
alignItems={{ xs: 'flex-start', md: 'flex-end' }}
|
||||||
@ -203,8 +211,10 @@ function Home() {
|
|||||||
Most popular images
|
Most popular images
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.viewAll} onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}>
|
<div onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}>
|
||||||
<Typography variant="body2">View all</Typography>
|
<Typography variant="body2" className={classes.viewAll}>
|
||||||
|
View all
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
{renderMostPopular()}
|
{renderMostPopular()}
|
||||||
|
@ -5,7 +5,18 @@ import { useNavigate, createSearchParams } from 'react-router-dom';
|
|||||||
// utility
|
// utility
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
// components
|
// components
|
||||||
import { Card, CardActionArea, CardMedia, CardContent, Typography, Stack, Chip, Grid, Tooltip } from '@mui/material';
|
import {
|
||||||
|
Card,
|
||||||
|
CardActionArea,
|
||||||
|
CardMedia,
|
||||||
|
CardContent,
|
||||||
|
Typography,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
Grid,
|
||||||
|
Tooltip,
|
||||||
|
useMediaQuery
|
||||||
|
} from '@mui/material';
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
|
||||||
// placeholder images
|
// placeholder images
|
||||||
@ -17,6 +28,7 @@ import repocube4 from '../../assets/repocube-4.png';
|
|||||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||||
import { isEmpty, uniq } from 'lodash';
|
import { isEmpty, uniq } from 'lodash';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
// temporary utility to get image
|
// temporary utility to get image
|
||||||
const randomIntFromInterval = (min, max) => {
|
const randomIntFromInterval = (min, max) => {
|
||||||
@ -30,22 +42,22 @@ const randomImage = () => {
|
|||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles(() => ({
|
||||||
card: {
|
card: {
|
||||||
marginBottom: 2,
|
marginTop: '1rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
borderColor: '#FFFFFF',
|
borderColor: '#FFFFFF',
|
||||||
borderRadius: '1.5rem',
|
borderRadius: '0.75rem',
|
||||||
|
boxShadow: '0rem 0.313rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||||
flex: 'none',
|
flex: 'none',
|
||||||
alignSelf: 'stretch',
|
alignSelf: 'stretch',
|
||||||
flexGrow: 0,
|
flexGrow: 0,
|
||||||
order: 0,
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: '72rem',
|
maxWidth: '72rem',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
boxShadow: '0rem 1.1875rem 1.4375rem rgba(131, 131, 131, 0.19)',
|
boxShadow: '0rem 1.1875rem 1.4375rem rgba(131, 131, 131, 0.19)',
|
||||||
borderRadius: '1.5rem'
|
borderRadius: '0.75rem'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
@ -56,7 +68,7 @@ const useStyles = makeStyles(() => ({
|
|||||||
cardBtn: {
|
cardBtn: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
borderRadius: '1.5rem',
|
borderRadius: '0.75rem',
|
||||||
borderColor: '#FFFFFF',
|
borderColor: '#FFFFFF',
|
||||||
'&:hover $focusHighlight': {
|
'&:hover $focusHighlight': {
|
||||||
opacity: 0
|
opacity: 0
|
||||||
@ -71,6 +83,7 @@ const useStyles = makeStyles(() => ({
|
|||||||
color: '#606060',
|
color: '#606060',
|
||||||
maxHeight: '9.25rem',
|
maxHeight: '9.25rem',
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
|
padding: '1.188rem 1rem',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#FFFFFF'
|
backgroundColor: '#FFFFFF'
|
||||||
}
|
}
|
||||||
@ -78,6 +91,20 @@ const useStyles = makeStyles(() => ({
|
|||||||
contentRight: {
|
contentRight: {
|
||||||
height: '100%'
|
height: '100%'
|
||||||
},
|
},
|
||||||
|
contentRightLabel: {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
lineHeight: '1.125rem',
|
||||||
|
color: '#52637A',
|
||||||
|
textAlign: 'end'
|
||||||
|
},
|
||||||
|
contentRightValue: {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
lineHeight: '1.125rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#14191F',
|
||||||
|
textAlign: 'end',
|
||||||
|
marginLeft: '0.5rem'
|
||||||
|
},
|
||||||
signedBadge: {
|
signedBadge: {
|
||||||
color: '#9ccc65',
|
color: '#9ccc65',
|
||||||
height: '1.375rem',
|
height: '1.375rem',
|
||||||
@ -86,18 +113,42 @@ const useStyles = makeStyles(() => ({
|
|||||||
},
|
},
|
||||||
vendor: {
|
vendor: {
|
||||||
color: '#14191F',
|
color: '#14191F',
|
||||||
fontSize: '1rem',
|
fontSize: '0.75rem',
|
||||||
maxWidth: '50%',
|
maxWidth: '50%',
|
||||||
textOverflow: 'ellipsis'
|
textOverflow: 'ellipsis',
|
||||||
|
lineHeight: '1.125rem'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
color: '#52637A',
|
||||||
|
fontSize: '1rem',
|
||||||
|
lineHeight: '1.5rem',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
marginBottom: 0,
|
||||||
|
paddingTop: '1rem'
|
||||||
},
|
},
|
||||||
versionLast: {
|
versionLast: {
|
||||||
color: '#52637A',
|
color: '#52637A',
|
||||||
fontSize: '1rem',
|
fontSize: '0.75rem',
|
||||||
|
lineHeight: '1.125rem',
|
||||||
textOverflow: 'ellipsis'
|
textOverflow: 'ellipsis'
|
||||||
},
|
},
|
||||||
cardTitle: {
|
cardTitle: {
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
maxWidth: '70%'
|
maxWidth: '70%',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#0F2139',
|
||||||
|
lineHeight: '2rem'
|
||||||
|
},
|
||||||
|
platformChips: {
|
||||||
|
backgroundColor: '#E0E5EB',
|
||||||
|
color: '#52637A',
|
||||||
|
fontSize: '0.813rem',
|
||||||
|
lineHeight: '0.813rem',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
padding: '0.313rem 0.625rem'
|
||||||
|
},
|
||||||
|
chipLabel: {
|
||||||
|
padding: '0'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -105,6 +156,11 @@ function RepoCard(props) {
|
|||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const placeholderImage = useRef(randomImage());
|
const placeholderImage = useRef(randomImage());
|
||||||
|
// dynamically check device size with mui media query hook
|
||||||
|
const theme = useTheme();
|
||||||
|
const isXsSize = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const MAX_PLATFORM_CHIPS = isXsSize ? 3 : 6;
|
||||||
|
|
||||||
const { name, vendor, platforms, description, downloads, isSigned, lastUpdated, logo, version, vulnerabilityData } =
|
const { name, vendor, platforms, description, downloads, isSigned, lastUpdated, logo, version, vulnerabilityData } =
|
||||||
props;
|
props;
|
||||||
|
|
||||||
@ -120,16 +176,18 @@ function RepoCard(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const platformChips = () => {
|
const platformChips = () => {
|
||||||
const filteredPlatforms = platforms?.flatMap((platform) => [platform.Os, platform.Arch]);
|
const filteredPlatforms = uniq(platforms?.flatMap((platform) => [platform.Os, platform.Arch]));
|
||||||
return uniq(filteredPlatforms).map((platform, index) => (
|
const hiddenChips = filteredPlatforms.length - MAX_PLATFORM_CHIPS;
|
||||||
|
const displayedPlatforms = filteredPlatforms.slice(0, MAX_PLATFORM_CHIPS + 1);
|
||||||
|
if (hiddenChips > 0) displayedPlatforms.push(`+${hiddenChips} more`);
|
||||||
|
return displayedPlatforms.map((platform, index) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={`${name}${platform}${index}`}
|
key={`${name}${platform}${index}`}
|
||||||
label={platform}
|
label={platform}
|
||||||
onClick={handlePlatformChipClick}
|
onClick={handlePlatformChipClick}
|
||||||
sx={{
|
className={classes.platformChips}
|
||||||
backgroundColor: '#E0E5EB',
|
classes={{
|
||||||
color: '#52637A',
|
label: classes.chipLabel
|
||||||
fontSize: '0.625rem'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
@ -183,14 +241,14 @@ function RepoCard(props) {
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Tooltip title={description || 'Description not available'} placement="top">
|
<Tooltip title={description || 'Description not available'} placement="top">
|
||||||
<Typography className={classes.versionLast} pt={1} sx={{ fontSize: 12 }} gutterBottom noWrap>
|
<Typography className={classes.description} pt={1} sx={{ fontSize: 12 }} gutterBottom noWrap>
|
||||||
{description || 'Description not available'}
|
{description || 'Description not available'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Stack alignItems="center" direction="row" spacing={1} pt={1}>
|
<Stack alignItems="center" direction="row" spacing={1} pt={1}>
|
||||||
{platformChips()}
|
{platformChips()}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack alignItems="center" direction="row" spacing={1} pt={2}>
|
<Stack alignItems="center" direction="row" spacing={1} pt={'0.5rem'}>
|
||||||
<Tooltip title={getVendor()} placement="top" className="hide-on-mobile">
|
<Tooltip title={getVendor()} placement="top" className="hide-on-mobile">
|
||||||
<Typography className={classes.vendor} variant="body2" noWrap>
|
<Typography className={classes.vendor} variant="body2" noWrap>
|
||||||
{<Markdown options={{ forceInline: true }}>{getVendor()}</Markdown>}
|
{<Markdown options={{ forceInline: true }}>{getVendor()}</Markdown>}
|
||||||
@ -208,19 +266,25 @@ function RepoCard(props) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={2} md={2} className="hide-on-mobile">
|
<Grid item xs={2} md={2} className={`hide-on-mobile ${classes.contentRight}`}>
|
||||||
<Stack
|
<Grid container item justifyContent="flex-end" textAlign="end">
|
||||||
alignItems="flex-end"
|
<Grid item xs={12}>
|
||||||
justifyContent="space-between"
|
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
|
||||||
direction="column"
|
Downloads •
|
||||||
className={classes.contentRight}
|
</Typography>
|
||||||
>
|
<Typography variant="body2" component="span" className={classes.contentRightValue}>
|
||||||
<Stack direction="column" alignItems="flex-end">
|
{!isNaN(downloads) ? downloads : `not available`}
|
||||||
<Typography variant="body2">Downloads • {!isNaN(downloads) ? downloads : `not available`}</Typography>
|
</Typography>
|
||||||
{/* <Typography variant="body2">Rating • {rating || '-'}</Typography> */}
|
</Grid>
|
||||||
</Stack>
|
{/* <Grid item xs={12}>
|
||||||
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
|
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
|
||||||
</Stack>
|
Rating •
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" component="span" className={classes.contentRightValue}>
|
||||||
|
#1
|
||||||
|
</Typography>
|
||||||
|
</Grid> */}
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
@ -85,10 +85,41 @@ def pull_modify_push_image(logger, registry, image_name, tag, cosign_password,
|
|||||||
|
|
||||||
with open(metafile) as f:
|
with open(metafile) as f:
|
||||||
image_metadata = json.load(f)
|
image_metadata = json.load(f)
|
||||||
image_metadata[image_name][tag]["multiarch"] = multiarch
|
logger.debug("raw image metadata")
|
||||||
|
logger.debug(image_metadata)
|
||||||
|
image_metadata["multiarch"] = multiarch
|
||||||
|
image_metadata["cves"] = getCVEInfo(image_metadata.pop("trivy"))
|
||||||
|
|
||||||
|
logger.debug("processed image metadata")
|
||||||
|
logger.debug(image_metadata)
|
||||||
return image_metadata
|
return image_metadata
|
||||||
|
|
||||||
|
def getCVEInfo(trivy_results):
|
||||||
|
cve_dict = {}
|
||||||
|
|
||||||
|
for result in trivy_results:
|
||||||
|
for vulnerability in result.get("Vulnerabilities", []):
|
||||||
|
cve_id = vulnerability["VulnerabilityID"]
|
||||||
|
|
||||||
|
package = {
|
||||||
|
"PackageName": vulnerability.get("PkgName"),
|
||||||
|
"InstalledVersion": vulnerability.get("InstalledVersion"),
|
||||||
|
"FixedVersion": vulnerability.get("FixedVersion", "Not Specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cve_dict.get(cve_id):
|
||||||
|
cve_dict[cve_id]["PackageList"].append(package)
|
||||||
|
else:
|
||||||
|
cve_dict[cve_id] = {
|
||||||
|
"ID": cve_id,
|
||||||
|
"Title": vulnerability.get("Title"),
|
||||||
|
"Description": vulnerability.get("Description"),
|
||||||
|
"Severity": vulnerability.get("Severity"),
|
||||||
|
"PackageList": [package]
|
||||||
|
}
|
||||||
|
|
||||||
|
return cve_dict
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
||||||
@ -137,7 +168,7 @@ def main():
|
|||||||
image_metadata = pull_modify_push_image(logger, registry, image_name, tag, cosign_password, multiarch, username, password, debug, data_dir)
|
image_metadata = pull_modify_push_image(logger, registry, image_name, tag, cosign_password, multiarch, username, password, debug, data_dir)
|
||||||
|
|
||||||
metadata.setdefault(image_name, {})
|
metadata.setdefault(image_name, {})
|
||||||
metadata[image_name][tag] = image_metadata[image_name][tag]
|
metadata[image_name][tag] = image_metadata
|
||||||
|
|
||||||
with open(metadata_file, "w") as f:
|
with open(metadata_file, "w") as f:
|
||||||
json.dump(metadata, f, indent=2)
|
json.dump(metadata, f, indent=2)
|
||||||
|
@ -125,6 +125,11 @@ function verify_prerequisites {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! command -v trivy ] &>/dev/null; then
|
||||||
|
echo "you need to install trivy as a prerequisite" >&3
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ ! command -v jq ] &>/dev/null; then
|
if [ ! command -v jq ] &>/dev/null; then
|
||||||
echo "you need to install jq as a prerequisite" >&3
|
echo "you need to install jq as a prerequisite" >&3
|
||||||
return 1
|
return 1
|
||||||
@ -160,6 +165,7 @@ doc=$(cat ${docker_docs_dir}/${image}/content.md)
|
|||||||
|
|
||||||
local_image_ref_skopeo=oci:${images_dir}:${image}-${tag}
|
local_image_ref_skopeo=oci:${images_dir}:${image}-${tag}
|
||||||
local_image_ref_regtl=ocidir://${images_dir}:${image}-${tag}
|
local_image_ref_regtl=ocidir://${images_dir}:${image}-${tag}
|
||||||
|
local_image_ref_trivy=${images_dir}:${image}-${tag}
|
||||||
remote_src_image_ref=docker://${image}:${tag}
|
remote_src_image_ref=docker://${image}:${tag}
|
||||||
remote_dest_image_ref=${registry}/${image}:${tag}
|
remote_dest_image_ref=${registry}/${image}:${tag}
|
||||||
|
|
||||||
@ -209,13 +215,24 @@ if [ $? -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
trivy_out_file=trivy-${image}-${tag}.json
|
||||||
|
if [ ! -z "${multiarch}" ]; then
|
||||||
|
trivy image --scanners vuln --format json --input ${local_image_ref_trivy} -o ${trivy_out_file}
|
||||||
|
jq -n --argfile trivy_file ${trivy_out_file} '.trivy=$trivy_file.Results' > ${trivy_out_file}.tmp
|
||||||
|
mv ${trivy_out_file}.tmp ${trivy_out_file}
|
||||||
|
else
|
||||||
|
echo '{"trivy":[]}' > ${trivy_out_file}
|
||||||
|
fi
|
||||||
|
|
||||||
# Sign new updated image
|
# Sign new updated image
|
||||||
COSIGN_PASSWORD=${cosign_password} cosign sign ${remote_dest_image_ref} --key ${cosign_key_path} --allow-insecure-registry
|
COSIGN_PASSWORD=${cosign_password} cosign sign ${remote_dest_image_ref} --key ${cosign_key_path} --allow-insecure-registry
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
details=$(jq -n \
|
details_file=details-${image}-${tag}.json
|
||||||
|
|
||||||
|
jq -n \
|
||||||
--arg org.opencontainers.image.title "${image}" \
|
--arg org.opencontainers.image.title "${image}" \
|
||||||
--arg org.opencontainers.image.description " $description" \
|
--arg org.opencontainers.image.description " $description" \
|
||||||
--arg org.opencontainers.image.url "${repo}" \
|
--arg org.opencontainers.image.url "${repo}" \
|
||||||
@ -223,7 +240,7 @@ details=$(jq -n \
|
|||||||
--arg org.opencontainers.image.licenses "${license}" \
|
--arg org.opencontainers.image.licenses "${license}" \
|
||||||
--arg org.opencontainers.image.vendor "${vendor}" \
|
--arg org.opencontainers.image.vendor "${vendor}" \
|
||||||
--arg org.opencontainers.image.documentation "${description}" \
|
--arg org.opencontainers.image.documentation "${description}" \
|
||||||
'$ARGS.named'
|
'$ARGS.named' > ${details_file}
|
||||||
)
|
|
||||||
|
|
||||||
jq -n --arg image "${image}" --arg tag "${tag}" --argjson details "${details}" '.[$image][$tag]=$details' > ${metafile}
|
jq -c -s add ${details_file} ${trivy_out_file} > ${metafile}
|
||||||
|
rm ${details_file} ${trivy_out_file}
|
||||||
|
Reference in New Issue
Block a user