patch: ux update for repo page
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
parent
ecd584c4e2
commit
c1a51afede
@ -248,7 +248,7 @@ describe('Repo details component', () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsWithMissingData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findByText('test')).toBeInTheDocument();
|
||||
expect(await screen.findByText(/timestamp n\/a/i)).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(/timestamp n\/a/i)).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders vulnerability icons', async () => {
|
||||
@ -288,15 +288,6 @@ describe('Repo details component', () => {
|
||||
await waitFor(() => expect(mockUseNavigate).toBeCalledWith('/home'));
|
||||
});
|
||||
|
||||
it('should switch between tabs', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findByTestId('overview-container')).toBeInTheDocument();
|
||||
fireEvent.click(await screen.findByText(/tags/i));
|
||||
expect(await screen.findByTestId('tags-container')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('overview-container')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render platform chips and they should redirect to explore page', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
|
@ -2,6 +2,15 @@ import { fireEvent, waitFor, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Tags from 'components/Repo/Tabs/Tags';
|
||||
import React from 'react';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
|
||||
const TagsThemeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<Tags tags={mockedTagsData} />
|
||||
</MockThemeProvier>
|
||||
);
|
||||
};
|
||||
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
@ -59,7 +68,7 @@ const mockedTagsData = [
|
||||
|
||||
describe('Tags component', () => {
|
||||
it('should open and close details dropdown for tags', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
render(<TagsThemeWrapper />);
|
||||
const openBtn = screen.getAllByText(/digest/i);
|
||||
fireEvent.click(openBtn[0]);
|
||||
expect(screen.getByText(/OS\/ARCH/i)).toBeInTheDocument();
|
||||
@ -68,7 +77,7 @@ describe('Tags component', () => {
|
||||
});
|
||||
|
||||
it('should navigate to tag page details when tag is clicked', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
render(<TagsThemeWrapper />);
|
||||
const tagLink = await screen.findByText('latest');
|
||||
fireEvent.click(tagLink);
|
||||
await waitFor(() => {
|
||||
@ -77,7 +86,7 @@ describe('Tags component', () => {
|
||||
});
|
||||
|
||||
it('should navigate to specific manifest when clicking the digest', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
render(<TagsThemeWrapper />);
|
||||
const openBtn = screen.getAllByText(/digest/i);
|
||||
await fireEvent.click(openBtn[0]);
|
||||
const tagLink = await screen.findByText(/sha256:adca4/i);
|
||||
@ -90,8 +99,8 @@ describe('Tags component', () => {
|
||||
});
|
||||
|
||||
it('should filter tag list based on user input', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
const tagFilterInput = await screen.findByPlaceholderText(/Search for Tags/i);
|
||||
render(<TagsThemeWrapper />);
|
||||
const tagFilterInput = await screen.findByPlaceholderText(/Search Tags/i);
|
||||
expect(await screen.findByText(/latest/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/bullseye/i)).toBeInTheDocument();
|
||||
userEvent.type(tagFilterInput, 'bull');
|
||||
@ -100,7 +109,7 @@ describe('Tags component', () => {
|
||||
});
|
||||
|
||||
it('should sort tags based on the picked sort criteria', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
render(<TagsThemeWrapper />);
|
||||
const selectFilter = await screen.findByText('Newest');
|
||||
expect(selectFilter).toBeInTheDocument();
|
||||
userEvent.click(selectFilter);
|
||||
|
@ -3,6 +3,7 @@ import { api } from 'api';
|
||||
import DependsOn from 'components/Tag/Tabs/DependsOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
|
||||
const mockDependenciesList = {
|
||||
data: {
|
||||
@ -52,11 +53,13 @@ const mockDependenciesList = {
|
||||
|
||||
const RouterDependsWrapper = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<DependsOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<MockThemeProvier>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<DependsOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvier>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { api } from 'api';
|
||||
import IsDependentOn from 'components/Tag/Tabs/IsDependentOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
|
||||
const mockDependentsList = {
|
||||
data: {
|
||||
@ -52,11 +53,13 @@ const mockDependentsList = {
|
||||
|
||||
const RouterDependsWrapper = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<IsDependentOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<MockThemeProvier>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<IsDependentOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvier>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -76,7 +76,7 @@ const endpoints = {
|
||||
(pageNumber - 1) * pageSize
|
||||
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned Documentation Vendor Labels} DownloadCount}}}`,
|
||||
detailedRepoInfo: (name) =>
|
||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName IsSigned Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName IsSigned Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||
detailedImageInfo: (name, tag) =>
|
||||
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
||||
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '') => {
|
||||
|
@ -34,11 +34,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
exploreText: {
|
||||
color: '#C0C0C0',
|
||||
display: 'flex',
|
||||
alignItems: 'left'
|
||||
},
|
||||
resultsRow: {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
|
@ -13,26 +13,31 @@ import React from 'react';
|
||||
const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
exploreHeader: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: 'transparent',
|
||||
minHeight: 50,
|
||||
paddingLeft: '3rem',
|
||||
padding: '2.75rem 0 1.25rem 0',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '2rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '1rem'
|
||||
}
|
||||
},
|
||||
explore: {
|
||||
color: '#52637A',
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '0.813rem',
|
||||
fontWeight: '600',
|
||||
letterSpacing: '0.009375rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
fontSize: '0.8rem'
|
||||
}
|
||||
},
|
||||
arrowIcon: {
|
||||
color: theme.palette.secondary.dark,
|
||||
marginRight: '1.75rem',
|
||||
fontSize: { xs: '1.5rem', md: '2rem' },
|
||||
cursor: 'pointer'
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -49,10 +54,7 @@ function ExploreHeader() {
|
||||
|
||||
return (
|
||||
<div className={classes.exploreHeader}>
|
||||
<ArrowBackIcon
|
||||
sx={{ color: '#52637A', marginRight: '1.75rem', fontSize: { xs: '1.5rem', md: '2rem' }, cursor: 'pointer' }}
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
<ArrowBackIcon className={classes.arrowIcon} onClick={() => navigate(-1)} />
|
||||
<Breadcrumbs separator="/" aria-label="breadcrumb">
|
||||
<Link to="/">
|
||||
<Typography variant="body1" className={classes.explore}>
|
||||
|
@ -283,6 +283,7 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
||||
<InputBase
|
||||
placeholder={'Search for content...'}
|
||||
className={`${classes.input} ${isComponentFocused && classes.inputFocused}`}
|
||||
sx={{ input: { '&::placeholder': { opacity: 1 } } }}
|
||||
onKeyUp={handleSearch}
|
||||
onFocus={() => openMenu()}
|
||||
{...getInputProps()}
|
||||
|
@ -1,13 +1,16 @@
|
||||
// react global
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
// external
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../api';
|
||||
import { useParams, useNavigate, createSearchParams } from 'react-router-dom';
|
||||
|
||||
// components
|
||||
import Tags from './Tabs/Tags.jsx';
|
||||
import { Box, Card, CardContent, CardMedia, Chip, Grid, Stack, Tab, Typography } from '@mui/material';
|
||||
import { Card, CardContent, CardMedia, Chip, Grid, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../../host';
|
||||
|
||||
@ -16,19 +19,17 @@ import repocube1 from '../../assets/repocube-1.png';
|
||||
import repocube2 from '../../assets/repocube-2.png';
|
||||
import repocube3 from '../../assets/repocube-3.png';
|
||||
import repocube4 from '../../assets/repocube-4.png';
|
||||
import { TabContext, TabList, TabPanel } from '@mui/lab';
|
||||
|
||||
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';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
pageWrapper: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
backgroundColor: 'transparent',
|
||||
height: '100%'
|
||||
},
|
||||
container: {
|
||||
@ -38,13 +39,14 @@ const useStyles = makeStyles((theme) => ({
|
||||
backgroundColor: '#FFFFFF'
|
||||
},
|
||||
repoName: {
|
||||
fontWeight: '700',
|
||||
fontWeight: '600',
|
||||
fontSize: '1.5rem',
|
||||
color: '#0F2139',
|
||||
textAlign: 'left'
|
||||
},
|
||||
avatar: {
|
||||
height: '3rem',
|
||||
width: '3rem',
|
||||
height: '1.438rem',
|
||||
width: '1.438rem',
|
||||
objectFit: 'fill'
|
||||
},
|
||||
cardBtn: {
|
||||
@ -54,31 +56,16 @@ const useStyles = makeStyles((theme) => ({
|
||||
media: {
|
||||
borderRadius: '3.125em'
|
||||
},
|
||||
tabs: {
|
||||
marginTop: '3rem',
|
||||
padding: '0.5rem',
|
||||
tags: {
|
||||
marginTop: '1.5rem',
|
||||
height: '100%',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0'
|
||||
}
|
||||
},
|
||||
tabContent: {
|
||||
height: '100%'
|
||||
},
|
||||
selectedTab: {
|
||||
background: '#D83C0E',
|
||||
borderRadius: '1.5rem'
|
||||
},
|
||||
tabPanel: {
|
||||
height: '100%',
|
||||
paddingLeft: '0rem!important',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '1.5rem 0'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
marginTop: '8rem',
|
||||
paddingLeft: '1.5rem',
|
||||
marginTop: '1.5rem',
|
||||
paddingLeft: '1.25rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
marginTop: '1rem',
|
||||
paddingLeft: '0'
|
||||
@ -88,11 +75,10 @@ const useStyles = makeStyles((theme) => ({
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'start',
|
||||
alignItems: 'flex-start',
|
||||
background: '#FFFFFF',
|
||||
border: '0.0625rem solid #E0E5EB',
|
||||
borderRadius: '2rem',
|
||||
flex: 'none',
|
||||
borderRadius: '0.75rem',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
@ -117,7 +103,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
boxShadow: 'none!important'
|
||||
},
|
||||
header: {
|
||||
paddingLeft: '2rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0'
|
||||
}
|
||||
@ -127,17 +112,41 @@ const useStyles = makeStyles((theme) => ({
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5rem',
|
||||
color: 'rgba(0, 0, 0, 0.6)',
|
||||
padding: '0.5rem 0 0 4rem',
|
||||
padding: '1rem 0 0 0',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0.5rem 0 0 0'
|
||||
}
|
||||
},
|
||||
platformChipsContainer: {
|
||||
alignItems: 'center',
|
||||
padding: '0.5rem 0 0 1rem',
|
||||
padding: '0.15rem 0 0 0',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0.5rem 0 0 0'
|
||||
}
|
||||
},
|
||||
platformChips: {
|
||||
backgroundColor: '#E0E5EB',
|
||||
color: '#52637A',
|
||||
fontSize: '0.813rem',
|
||||
lineHeight: '0.813rem',
|
||||
borderRadius: '0.375rem',
|
||||
padding: '0.313rem 0.625rem'
|
||||
},
|
||||
chipLabel: {
|
||||
padding: '0'
|
||||
},
|
||||
vendor: {
|
||||
color: theme.palette.primary,
|
||||
fontSize: '0.75rem',
|
||||
maxWidth: '50%',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: '1.125rem'
|
||||
},
|
||||
versionLast: {
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem',
|
||||
textOverflow: 'ellipsis'
|
||||
}
|
||||
}));
|
||||
|
||||
@ -156,7 +165,6 @@ function RepoDetails() {
|
||||
const [tags, setTags] = useState([]);
|
||||
const placeholderImage = useRef(randomImage());
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedTab, setSelectedTab] = useState('Overview');
|
||||
// get url param from <Route here (i.e. image name)
|
||||
const { name } = useParams();
|
||||
const navigate = useNavigate();
|
||||
@ -205,38 +213,25 @@ function RepoDetails() {
|
||||
key={`${name}${platform}${index}`}
|
||||
label={platform}
|
||||
onClick={handlePlatformChipClick}
|
||||
sx={{
|
||||
backgroundColor: '#E0E5EB',
|
||||
color: '#52637A',
|
||||
fontSize: '0.625rem'
|
||||
className={classes.platformChips}
|
||||
classes={{
|
||||
label: classes.chipLabel
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setSelectedTab(newValue);
|
||||
const getVendor = () => {
|
||||
return `${repoDetailData.newestTag?.Vendor || 'Vendor not available'} •`;
|
||||
};
|
||||
|
||||
const renderOverview = () => {
|
||||
return (
|
||||
<Card className={classes.card} data-testid="overview-container">
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: 'rgba(0, 0, 0, 0.6)',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '150%',
|
||||
marginTop: '1.3rem',
|
||||
alignSelf: 'stretch'
|
||||
}}
|
||||
>
|
||||
{repoDetailData.description || 'Description not available'}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
const getVersion = () => {
|
||||
return `published ${repoDetailData.newestTag?.Tag} •`;
|
||||
};
|
||||
const getLast = () => {
|
||||
const lastDate = repoDetailData.lastUpdated
|
||||
? DateTime.fromISO(repoDetailData.lastUpdated).toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] })
|
||||
: `Timestamp N/A`;
|
||||
return lastDate;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -244,87 +239,87 @@ function RepoDetails() {
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<div className={classes.pageWrapper}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent>
|
||||
<Grid container className={classes.header}>
|
||||
<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
|
||||
<Grid container className={classes.pageWrapper}>
|
||||
<Grid item xs={12} md={12}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent>
|
||||
<Grid container className={classes.header}>
|
||||
<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}`
|
||||
: placeholderImage.current
|
||||
}
|
||||
alt="icon"
|
||||
/>
|
||||
<Typography variant="h4" className={classes.repoName}>
|
||||
{name}
|
||||
</Typography>
|
||||
!isEmpty(repoDetailData?.logo)
|
||||
? `data:image/png;base64, ${repoDetailData?.logo}`
|
||||
: placeholderImage.current
|
||||
}
|
||||
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} />
|
||||
</Stack>
|
||||
</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"}}/> */}
|
||||
<Typography gutterBottom className={classes.repoTitle}>
|
||||
{repoDetailData?.title || 'Title not available'}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} className={classes.platformChipsContainer}>
|
||||
{platformChips()}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Typography gutterBottom className={classes.repoTitle}>
|
||||
{repoDetailData?.title || 'Title not available'}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} className={classes.platformChipsContainer}>
|
||||
{platformChips()}
|
||||
</Stack>
|
||||
<Stack alignItems="center" direction="row" spacing={1} pt={'0.5rem'}>
|
||||
<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" className="hide-on-mobile">
|
||||
<Typography className={classes.versionLast} variant="body2" noWrap>
|
||||
{getVersion()}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Tooltip title={repoDetailData.lastUpdated?.slice(0, 16) || ' '} placement="top">
|
||||
<Typography className={classes.versionLast} variant="body2" noWrap>
|
||||
{getLast()}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<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' } }}
|
||||
>
|
||||
<Tab value="Overview" label="Overview" className={classes.tabContent} />
|
||||
<Tab value="Tags" label="Tags" className={classes.tabContent} />
|
||||
</TabList>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<TabPanel value="Overview" className={classes.tabPanel}>
|
||||
{renderOverview()}
|
||||
</TabPanel>
|
||||
<TabPanel value="Tags" className={classes.tabPanel}>
|
||||
<Tags tags={tags} />
|
||||
</TabPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</TabContext>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} className={classes.metadata}>
|
||||
<RepoDetailsMetadata
|
||||
totalDownloads={repoDetailData?.downloads}
|
||||
repoURL={repoDetailData?.source}
|
||||
lastUpdated={repoDetailData?.lastUpdated}
|
||||
size={repoDetailData?.size}
|
||||
latestTag={repoDetailData?.newestTag}
|
||||
license={repoDetailData?.license}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8} className={classes.tags}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent>
|
||||
<Tags tags={tags} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} className={classes.metadata}>
|
||||
<RepoDetailsMetadata
|
||||
totalDownloads={repoDetailData?.downloads}
|
||||
repoURL={repoDetailData?.source}
|
||||
lastUpdated={repoDetailData?.lastUpdated}
|
||||
size={repoDetailData?.size}
|
||||
latestTag={repoDetailData?.newestTag}
|
||||
license={repoDetailData?.license}
|
||||
description={repoDetailData?.description}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -5,26 +5,33 @@ import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import React from 'react';
|
||||
import transform from '../../utilities/transform';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'start',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.5rem',
|
||||
border: '0',
|
||||
borderRadius: '0.5rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%'
|
||||
},
|
||||
cardContent: {
|
||||
'&:last-child': {
|
||||
padding: '0.5rem 1rem'
|
||||
}
|
||||
},
|
||||
metadataHeader: {
|
||||
color: 'rgba(0, 0, 0, 0.6)'
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem'
|
||||
},
|
||||
metadataBody: {
|
||||
color: 'rgba(0, 0, 0, 0.87)',
|
||||
color: theme.palette.primary,
|
||||
fontFamily: 'Roboto',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: 400,
|
||||
@ -36,7 +43,7 @@ const useStyles = makeStyles(() => ({
|
||||
|
||||
function RepoDetailsMetadata(props) {
|
||||
const classes = useStyles();
|
||||
const { repoURL, totalDownloads, lastUpdated, size, license } = props;
|
||||
const { repoURL, totalDownloads, lastUpdated, size, license, description } = props;
|
||||
|
||||
const lastDate = lastUpdated
|
||||
? DateTime.fromISO(lastUpdated).toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] })
|
||||
@ -45,7 +52,7 @@ function RepoDetailsMetadata(props) {
|
||||
<Grid container spacing={1}>
|
||||
<Grid container item xs={12}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Repository
|
||||
</Typography>
|
||||
@ -57,7 +64,7 @@ function RepoDetailsMetadata(props) {
|
||||
</Grid>
|
||||
<Grid container item xs={12}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Total downloads
|
||||
</Typography>
|
||||
@ -70,7 +77,7 @@ function RepoDetailsMetadata(props) {
|
||||
<Grid container item xs={12} spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Last publish
|
||||
</Typography>
|
||||
@ -84,7 +91,7 @@ function RepoDetailsMetadata(props) {
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Total size
|
||||
</Typography>
|
||||
@ -98,7 +105,7 @@ function RepoDetailsMetadata(props) {
|
||||
<Grid container item xs={12} spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
License
|
||||
</Typography>
|
||||
@ -111,6 +118,20 @@ function RepoDetailsMetadata(props) {
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container item xs={12} spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Description
|
||||
</Typography>
|
||||
<Typography variant="body1" align="left" className={classes.metadataBody}>
|
||||
{description ? <Markdown>{description}</Markdown> : `Description not available`}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ import React, { useState } from 'react';
|
||||
|
||||
// components
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Card, CardContent, Divider, Stack, InputBase, FormControl, Select, InputLabel, MenuItem } from '@mui/material';
|
||||
import { Card, CardContent, Stack, InputBase, FormControl, Select, InputLabel, MenuItem } from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import TagCard from '../../Shared/TagCard';
|
||||
import { tagsSortByCriteria } from 'utilities/sortCriteria';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
tagCard: {
|
||||
tagContainer: {
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
@ -24,20 +24,6 @@ const useStyles = makeStyles(() => ({
|
||||
order: 0,
|
||||
width: '100%'
|
||||
},
|
||||
card: {
|
||||
marginBottom: '2rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.875rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%'
|
||||
},
|
||||
content: {
|
||||
textAlign: 'left',
|
||||
color: '#606060',
|
||||
@ -49,13 +35,15 @@ const useStyles = makeStyles(() => ({
|
||||
},
|
||||
search: {
|
||||
position: 'relative',
|
||||
minWidth: '100%',
|
||||
maxWidth: '100%',
|
||||
flexDirection: 'row',
|
||||
marginBottom: '1.7rem',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
border: '0.125rem solid #E7E7E7',
|
||||
borderRadius: '1rem',
|
||||
zIndex: 1155
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: '1rem',
|
||||
marginBottom: '1rem',
|
||||
boxShadow: 'none',
|
||||
border: '0.063rem solid #E7E7E7',
|
||||
borderRadius: '0.625rem'
|
||||
},
|
||||
searchIcon: {
|
||||
color: '#52637A',
|
||||
@ -63,8 +51,12 @@ const useStyles = makeStyles(() => ({
|
||||
},
|
||||
input: {
|
||||
color: '#464141',
|
||||
marginLeft: 1,
|
||||
width: '90%'
|
||||
fontSize: '1rem',
|
||||
paddingLeft: '1rem',
|
||||
width: '90%',
|
||||
'&::placeholder': {
|
||||
opacity: '1'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@ -73,6 +65,7 @@ export default function Tags(props) {
|
||||
const { tags } = props;
|
||||
const [tagsFilter, setTagsFilter] = useState('');
|
||||
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
|
||||
|
||||
const renderTags = (tags) => {
|
||||
const selectedSort = Object.values(tagsSortByCriteria).find((sc) => sc.value === sortFilter);
|
||||
const filteredTags = tags.filter((t) => t.tag?.includes(tagsFilter));
|
||||
@ -106,7 +99,7 @@ export default function Tags(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={classes.tagCard} data-testid="tags-container">
|
||||
<Card className={classes.tagContainer} data-testid="tags-container">
|
||||
<CardContent className={classes.content}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography
|
||||
@ -136,26 +129,12 @@ export default function Tags(props) {
|
||||
</FormControl>
|
||||
</div>
|
||||
</Stack>
|
||||
<Divider
|
||||
variant="fullWidth"
|
||||
sx={{
|
||||
margin: '5% 0% 5% 0%',
|
||||
background: 'rgba(0, 0, 0, 0.38)',
|
||||
height: '0.00625rem',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
className={classes.search}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
spacing={2}
|
||||
>
|
||||
<Stack className={classes.search}>
|
||||
<InputBase
|
||||
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
|
||||
placeholder={'Search for Tags...'}
|
||||
className={classes.input}
|
||||
style={{ paddingLeft: 10, height: 40, color: 'rgba(0, 0, 0, 0.6)' }}
|
||||
placeholder={'Search tags...'}
|
||||
// className={classes.input}
|
||||
classes={{ input: classes.input }}
|
||||
value={tagsFilter}
|
||||
onChange={handleTagsFilterChange}
|
||||
/>
|
||||
|
@ -40,7 +40,7 @@ const randomImage = () => {
|
||||
return imageArray[randomIntFromInterval(0, 3)];
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
marginBottom: '1rem',
|
||||
display: 'flex',
|
||||
@ -112,7 +112,7 @@ const useStyles = makeStyles(() => ({
|
||||
marginLeft: 10
|
||||
},
|
||||
vendor: {
|
||||
color: '#14191F',
|
||||
color: theme.palette.primary,
|
||||
fontSize: '0.75rem',
|
||||
maxWidth: '50%',
|
||||
textOverflow: 'ellipsis',
|
||||
@ -127,7 +127,7 @@ const useStyles = makeStyles(() => ({
|
||||
paddingTop: '1rem'
|
||||
},
|
||||
versionLast: {
|
||||
color: '#52637A',
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem',
|
||||
textOverflow: 'ellipsis'
|
||||
|
@ -1,35 +1,22 @@
|
||||
import React, { useState } from 'react';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Card, CardContent, Collapse, Grid, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { Box, Card, CardContent, Collapse, Grid, Stack, Tooltip, Typography, Divider } from '@mui/material';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import transform from 'utilities/transform';
|
||||
import { DateTime } from 'luxon';
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
tagCard: {
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: 'none!important',
|
||||
borderRadius: '1.875rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%'
|
||||
},
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
marginBottom: '2rem',
|
||||
marginBottom: '1rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.875rem',
|
||||
boxShadow: 'none',
|
||||
border: '1px solid #E0E5EB',
|
||||
borderRadius: '0.75rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
@ -56,6 +43,30 @@ const useStyles = makeStyles(() => ({
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
},
|
||||
tagHeading: {
|
||||
color: '#828282',
|
||||
fontSize: '1rem',
|
||||
marginBottom: '0.5rem'
|
||||
},
|
||||
tagName: {
|
||||
color: '#1479FF',
|
||||
fontSize: '1rem',
|
||||
marginBottom: '0.5rem',
|
||||
textDecorationLine: 'underline',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
cardDivider: {
|
||||
marginTop: '1rem',
|
||||
marginBottom: '1rem',
|
||||
border: '1px solid #E0E5EB'
|
||||
},
|
||||
manifsetsTable: {
|
||||
marginTop: '1rem'
|
||||
},
|
||||
tableHeaderText: {
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '1rem'
|
||||
}
|
||||
}));
|
||||
|
||||
@ -81,22 +92,17 @@ export default function TagCard(props) {
|
||||
return (
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent className={classes.content}>
|
||||
<Typography variant="body1" align="left" sx={{ color: '#828282', fontSize: '1rem', paddingBottom: '0.5rem' }}>
|
||||
<Typography variant="body1" align="left" className={classes.tagHeading}>
|
||||
Tag
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
align="left"
|
||||
sx={{ color: '#1479FF', fontSize: '1rem', textDecorationLine: 'underline', cursor: 'pointer' }}
|
||||
onClick={() => goToTags()}
|
||||
>
|
||||
<Typography variant="body1" align="left" className={classes.tagName} onClick={() => goToTags()}>
|
||||
{repoName && `${repoName}:`}
|
||||
{tag}
|
||||
</Typography>
|
||||
|
||||
<Stack sx={{ display: 'inline' }} direction="row" spacing={0.5}>
|
||||
<Typography variant="caption" sx={{ fontWeight: '400', fontSize: '0.8125rem' }}>
|
||||
Pushed
|
||||
Created
|
||||
</Typography>
|
||||
<Tooltip title={lastUpdated?.slice(0, 16) || ' '} placement="top">
|
||||
<Typography variant="caption" sx={{ fontWeight: '600', fontSize: '0.8125rem' }}>
|
||||
@ -104,6 +110,7 @@ export default function TagCard(props) {
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Divider variant="fullWidth" className={classes.cardDivider} />
|
||||
<Stack direction="row" onClick={() => setOpen(!open)}>
|
||||
{!open ? (
|
||||
<KeyboardArrowRight className={classes.dropdownText} />
|
||||
@ -123,22 +130,30 @@ export default function TagCard(props) {
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box>
|
||||
<Box className={classes.manifsetsTable}>
|
||||
<Grid container item xs={12} direction={'row'}>
|
||||
<Grid item xs={6} md={4}>
|
||||
<Typography variant="body1">DIGEST</Typography>
|
||||
<Grid item xs={6} md={6}>
|
||||
<Typography variant="body1" className={classes.tableHeaderText}>
|
||||
DIGEST
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={4} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid item xs={6} md={3} className={classes.tableHeaderText}>
|
||||
<Typography variant="body1">OS/Arch</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={0} md={4} className="hide-on-mobile" sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Typography variant="body1"> Size </Typography>
|
||||
<Grid
|
||||
item
|
||||
xs={0}
|
||||
md={3}
|
||||
className={`${classes.tableHeaderText} hide-on-mobile`}
|
||||
sx={{ display: 'flex', justifyContent: 'flex-end' }}
|
||||
>
|
||||
<Typography variant="body1"> COMPRESSED SIZE </Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{manifests.map((el) => (
|
||||
<Grid container item xs={12} key={el.digest} direction={'row'}>
|
||||
<Grid item xs={6} md={4}>
|
||||
<Grid item xs={6} md={6}>
|
||||
<Tooltip title={el.digest || ''} placement="top">
|
||||
<Typography
|
||||
variant="body1"
|
||||
@ -149,19 +164,19 @@ export default function TagCard(props) {
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={4} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Typography variant="body1">
|
||||
<Grid item xs={6} md={3}>
|
||||
<Typography variant="body1" color="primary">
|
||||
{el.platform?.Os}/{el.platform?.Arch}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={0}
|
||||
md={4}
|
||||
md={3}
|
||||
className="hide-on-mobile"
|
||||
sx={{ display: 'flex', justifyContent: 'flex-end' }}
|
||||
>
|
||||
<Typography sx={{ textAlign: 'right' }} variant="body1">
|
||||
<Typography sx={{ textAlign: 'right' }} variant="body1" color="primary">
|
||||
{transform.formatBytes(el.size)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
@ -26,9 +26,7 @@ const useStyles = makeStyles(() => ({
|
||||
height: '100vh'
|
||||
},
|
||||
gridWrapper: {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: 'transparent',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
|
Loading…
Reference in New Issue
Block a user