Dependencies tabs (#99)
feat: Added the isDependentOn and Depends on tabs on tag detail page Signed-off-by: Amelia-Maria Breda <ameliamaria.breda@dxc.com> Signed-off-by: Raul Kele <raulkeleblk@gmail.com> Co-authored-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
parent
e18279a32c
commit
ca1c9b00cc
@ -36,7 +36,7 @@
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --detectOpenHandles",
|
||||
"test:coverage": "react-scripts test --coverage",
|
||||
"test:coverage": "react-scripts test --detectOpenHandles --coverage",
|
||||
"lint": "eslint -c .eslintrc.json --ext .js,.jsx .",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc",
|
||||
|
@ -3,6 +3,14 @@ import ExplorePage from 'pages/ExplorePage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
jest.mock(
|
||||
'components/Explore',
|
||||
() =>
|
||||
function Explore() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the explore page component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
@ -3,6 +3,14 @@ import HomePage from 'pages/HomePage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
jest.mock(
|
||||
'components/Home',
|
||||
() =>
|
||||
function Home() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the homepage component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
@ -12,6 +12,14 @@ jest.mock('react-router-dom', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'components/RepoDetails',
|
||||
() =>
|
||||
function RepoDetails() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
@ -21,7 +29,7 @@ it('renders the repository page component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<RepoPage />} />
|
||||
<Route path="*" element={<RepoPage updateData={() => {}} />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
74
src/__tests__/TagPage/DependsOn.test.js
Normal file
74
src/__tests__/TagPage/DependsOn.test.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import DependsOn from 'components/DependsOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
|
||||
const mockDependenciesList = {
|
||||
data: {
|
||||
BaseImageList: [
|
||||
{
|
||||
RepoName: 'project-stacker/c3/static-ubuntu-amd64'
|
||||
},
|
||||
{
|
||||
RepoName: 'tag2'
|
||||
},
|
||||
{
|
||||
RepoName: 'tag3'
|
||||
},
|
||||
{
|
||||
RepoName: 'tag4'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const RouterDependsWrapper = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<DependsOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
// @ts-ignore
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Dependencies tab', () => {
|
||||
it('should render the dependencies if there are any', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependenciesList });
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findAllByRole('link')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('renders no dependencies if there are not any', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 200,
|
||||
data: { data: { BaseImageList: [] } }
|
||||
});
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should log an error when data can't be fetched", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<RouterDependsWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
});
|
59
src/__tests__/TagPage/HistoryLayers.test.js
Normal file
59
src/__tests__/TagPage/HistoryLayers.test.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import HistoryLayers from 'components/HistoryLayers';
|
||||
import React from 'react';
|
||||
|
||||
const mockLayersList = [
|
||||
{
|
||||
Layer: { Size: '2806054', Digest: '213ec9aee27d8be045c6a92b7eac22c9a64b44558193775a1a7f626352392b49', Score: null },
|
||||
HistoryDescription: {
|
||||
Created: '2022-08-09T17:19:53.274069586Z',
|
||||
CreatedBy: '/bin/sh -c #(nop) ADD file:2a949686d9886ac7c10582a6c29116fd29d3077d02755e87e111870d63607725 in / ',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: false
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2022-08-09T17:19:53.47374331Z',
|
||||
CreatedBy: '/bin/sh -c #(nop) CMD ["/bin/sh"]',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Layers page', () => {
|
||||
it('renders the layers if there are any', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: { Image: { History: mockLayersList } } } });
|
||||
render(<HistoryLayers name="alpine:latest" />);
|
||||
expect(await screen.findAllByTestId('layer-card-container')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders no layers if there are not any', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 200,
|
||||
data: { data: { History: { Tag: '', mockLayersList: [] } } }
|
||||
});
|
||||
render(<HistoryLayers name="alpine:latest" />);
|
||||
await waitFor(() => expect(screen.getAllByText('No Layers')).toHaveLength(1));
|
||||
});
|
||||
|
||||
it("should log an error when data can't be fetched", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<HistoryLayers name="alpine:latest" />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
});
|
74
src/__tests__/TagPage/IsDependentOn.test.js
Normal file
74
src/__tests__/TagPage/IsDependentOn.test.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import IsDependentOn from 'components/IsDependentOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
|
||||
const mockDependentsList = {
|
||||
data: {
|
||||
DerivedImageList: [
|
||||
{
|
||||
RepoName: 'project-stacker/c3/static-ubuntu-amd64'
|
||||
},
|
||||
{
|
||||
RepoName: 'tag2'
|
||||
},
|
||||
{
|
||||
RepoName: 'tag3'
|
||||
},
|
||||
{
|
||||
RepoName: 'tag4'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const RouterDependsWrapper = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<IsDependentOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
// @ts-ignore
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Dependents tab', () => {
|
||||
it('should render the dependents if there are any', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependentsList });
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findAllByRole('link')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('renders no dependents if there are not any', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 200,
|
||||
data: { data: { DerivedImageList: [] } }
|
||||
});
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should log an error when data can't be fetched", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<RouterDependsWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import TagDetails from 'components/TagDetails';
|
||||
import React from 'react';
|
||||
@ -79,10 +79,13 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('Tags details', () => {
|
||||
it('should show vulnerability tab', async () => {
|
||||
it('should show tabs and allow nagivation between them', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetails />);
|
||||
const dependenciesTab = await screen.findByTestId('dependencies-tab');
|
||||
fireEvent.click(dependenciesTab);
|
||||
expect(await screen.findByTestId('depends-on-container')).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(4));
|
||||
});
|
||||
|
||||
|
@ -70,6 +70,7 @@ const endpoints = {
|
||||
layersDetailsForImage: (name) =>
|
||||
`/v2/_zot/ext/search?query={Image(image: "${name}"){History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`,
|
||||
dependsOnForImage: (name) => `/v2/_zot/ext/search?query={BaseImageList(image: "${name}"){RepoName}}`,
|
||||
isDependentOnForImage: (name) => `/v2/_zot/ext/search?query={DerivedImageList(image: "${name}"){RepoName}}`,
|
||||
globalSearchPaginated: (searchQuery, pageNumber, pageSize) =>
|
||||
`/v2/_zot/ext/search?query={GlobalSearch(query:"${searchQuery}", requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
|
BIN
src/assets/Monitor.png
Normal file
BIN
src/assets/Monitor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
@ -4,15 +4,14 @@ import React, { useEffect, useState } from 'react';
|
||||
import { api, endpoints } from '../api';
|
||||
|
||||
// components
|
||||
import { Divider, Typography } from '@mui/material';
|
||||
import { Divider, Typography, Card, CardContent } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { host } from '../host';
|
||||
import Monitor from '../assets/Monitor.png';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.875rem',
|
||||
@ -22,7 +21,10 @@ const useStyles = makeStyles(() => ({
|
||||
order: 0,
|
||||
width: '100%',
|
||||
marginTop: '2rem',
|
||||
marginBottom: '2rem'
|
||||
marginBottom: '2rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
},
|
||||
content: {
|
||||
textAlign: 'left',
|
||||
@ -43,6 +45,23 @@ const useStyles = makeStyles(() => ({
|
||||
fontWeight: '600',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem'
|
||||
},
|
||||
link: {
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
letterSpacing: '0.009375rem',
|
||||
paddingRight: '1rem',
|
||||
textDecorationLine: 'underline'
|
||||
},
|
||||
monitor: {
|
||||
width: '27.25rem',
|
||||
height: '24.625rem',
|
||||
paddingTop: '2rem'
|
||||
},
|
||||
none: {
|
||||
color: '#52637A',
|
||||
fontSize: '1.4rem',
|
||||
fontWeight: '600'
|
||||
}
|
||||
}));
|
||||
|
||||
@ -50,6 +69,7 @@ function DependsOn(props) {
|
||||
const [images, setImages] = useState([]);
|
||||
const { name } = props;
|
||||
const classes = useStyles();
|
||||
// const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
@ -57,20 +77,16 @@ function DependsOn(props) {
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let images = response.data.data.BaseImageList;
|
||||
// let cveListData = {
|
||||
// cveList: cveInfo?.CVEList
|
||||
// }
|
||||
setImages(images);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setImages([]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="depends-on-container">
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
@ -85,7 +101,26 @@ function DependsOn(props) {
|
||||
variant="fullWidth"
|
||||
sx={{ margin: '5% 0% 5% 0%', background: 'rgba(0, 0, 0, 0.38)', height: '0.00625rem', width: '100%' }}
|
||||
/>
|
||||
{console.log(JSON.stringify(images))}
|
||||
{images?.length ? (
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent>
|
||||
<Typography className={classes.content}>
|
||||
{images.map((dependence, index) => {
|
||||
return (
|
||||
<Link key={index} className={classes.link} to={`/image/${encodeURIComponent(dependence.RepoName)}`}>
|
||||
{dependence.RepoName}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div>
|
||||
<img src={Monitor} alt="Monitor" className={classes.monitor}></img>
|
||||
<Typography className={classes.none}> Nothing found </Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
gridWrapper: {
|
||||
paddingTop: '2rem'
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem'
|
||||
},
|
||||
nodataWrapper: {
|
||||
backgroundColor: '#fff',
|
||||
|
@ -12,15 +12,15 @@ import {
|
||||
ClickAwayListener,
|
||||
Paper,
|
||||
Grow,
|
||||
Stack,
|
||||
IconButton
|
||||
Stack
|
||||
//IconButton
|
||||
} from '@mui/material';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
|
||||
// styling
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import logo from '../assets/Zot-white-text.svg';
|
||||
import placeholderProfileButton from '../assets/Profile_button_placeholder.svg';
|
||||
//import placeholderProfileButton from '../assets/Profile_button_placeholder.svg';
|
||||
import { useState, useRef } from 'react';
|
||||
import SearchSuggestion from './SearchSuggestion';
|
||||
|
||||
@ -98,7 +98,8 @@ function Header({ updateData }) {
|
||||
<Avatar alt="zot" src={logo} className={classes.logo} variant="square" />
|
||||
</Link>
|
||||
{path !== '/' && <SearchSuggestion updateData={updateData} />}
|
||||
<IconButton
|
||||
<div></div>
|
||||
{/* <IconButton
|
||||
ref={anchorRef}
|
||||
id="composition-button"
|
||||
aria-controls={open ? 'composition-menu' : undefined}
|
||||
@ -107,7 +108,7 @@ function Header({ updateData }) {
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<Avatar alt="profile" src={placeholderProfileButton} className={classes.userAvatar} variant="rounded" />
|
||||
</IconButton>
|
||||
</IconButton> */}
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
|
@ -8,6 +8,7 @@ import { api, endpoints } from '../api';
|
||||
import { Card, CardContent, Divider, Grid, Stack, Typography } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../host';
|
||||
import Monitor from '../assets/Monitor.png';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
@ -59,6 +60,16 @@ const useStyles = makeStyles(() => ({
|
||||
fontWeight: '400',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem'
|
||||
},
|
||||
monitor: {
|
||||
width: '27.25rem',
|
||||
height: '24.625rem',
|
||||
paddingTop: '2rem'
|
||||
},
|
||||
none: {
|
||||
color: '#52637A',
|
||||
fontSize: '1.4rem',
|
||||
fontWeight: '600'
|
||||
}
|
||||
}));
|
||||
|
||||
@ -77,7 +88,7 @@ function LayerCard(props) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid sx={isSelected ? { backgroundColor: '#F7F7F7' } : null} container>
|
||||
<Grid sx={isSelected ? { backgroundColor: '#F7F7F7' } : null} container data-testid="layer-card-container">
|
||||
<Grid item xs={10} container>
|
||||
<Grid item xs={1}>
|
||||
<Typography variant="body1" align="left" className={classes.title}>
|
||||
@ -139,10 +150,10 @@ function HistoryLayers(props) {
|
||||
variant="fullWidth"
|
||||
sx={{ margin: '5% 0% 0% 0%', background: 'rgba(0, 0, 0, 0.38)', height: '0.00625rem', width: '100%' }}
|
||||
/>
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent className={classes.content}>
|
||||
{historyData &&
|
||||
historyData.map((layer, index) => {
|
||||
{historyData ? (
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent className={classes.content}>
|
||||
{historyData.map((layer, index) => {
|
||||
return (
|
||||
<div key={`${layer?.Layer?.Size}${index}`} onClick={() => setSelectedIndex(index)}>
|
||||
<LayerCard
|
||||
@ -155,26 +166,30 @@ function HistoryLayers(props) {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div>
|
||||
<img src={Monitor} alt="Monitor" className={classes.monitor}></img>
|
||||
<Typography className={classes.none}> No Layers </Typography>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoaded && historyData && (
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent className={classes.content}>
|
||||
<Grid item xs={11}>
|
||||
<Stack sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body1" align="left" className={classes.title}>
|
||||
{' '}
|
||||
Command{' '}
|
||||
Command
|
||||
</Typography>
|
||||
<Typography variant="body1" align="left" className={classes.values}>
|
||||
{' '}
|
||||
{transform.formatBytes(historyData[selectedIndex].Layer?.Size)}{' '}
|
||||
{transform.formatBytes(historyData[selectedIndex].Layer?.Size)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Typography variant="body1" align="left" className={classes.title} sx={{ backgroundColor: '#F7F7F7' }}>
|
||||
{' '}
|
||||
{historyData[selectedIndex].HistoryDescription?.CreatedBy}{' '}
|
||||
{historyData[selectedIndex].HistoryDescription?.CreatedBy}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
@ -9,7 +9,8 @@ import { mapToRepo } from 'utilities/objectModels';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
gridWrapper: {
|
||||
marginTop: 10
|
||||
marginTop: 10,
|
||||
marginBottom: '5rem'
|
||||
},
|
||||
nodataWrapper: {
|
||||
backgroundColor: '#fff',
|
||||
|
131
src/components/IsDependentOn.jsx
Normal file
131
src/components/IsDependentOn.jsx
Normal file
@ -0,0 +1,131 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../api';
|
||||
|
||||
// components
|
||||
import { Divider, Typography, Card, CardContent } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { host } from '../host';
|
||||
import Monitor from '../assets/Monitor.png';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
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%',
|
||||
marginTop: '2rem',
|
||||
marginBottom: '2rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
},
|
||||
content: {
|
||||
textAlign: 'left',
|
||||
color: '#606060',
|
||||
padding: '2% 3% 2% 3%',
|
||||
width: '100%'
|
||||
},
|
||||
title: {
|
||||
color: '#828282',
|
||||
fontSize: '1rem',
|
||||
paddingRight: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem'
|
||||
},
|
||||
values: {
|
||||
color: '#000000',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '600',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem'
|
||||
},
|
||||
link: {
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
letterSpacing: '0.009375rem',
|
||||
paddingRight: '1rem',
|
||||
textDecorationLine: 'underline'
|
||||
},
|
||||
monitor: {
|
||||
width: '27.25rem',
|
||||
height: '24.625rem',
|
||||
paddingTop: '2rem'
|
||||
},
|
||||
none: {
|
||||
color: '#52637A',
|
||||
fontSize: '1.4rem',
|
||||
fontWeight: '600'
|
||||
}
|
||||
}));
|
||||
|
||||
function IsDependentOn(props) {
|
||||
const [images, setImages] = useState([]);
|
||||
const { name } = props;
|
||||
const classes = useStyles();
|
||||
//const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get(`${host()}${endpoints.isDependentOnForImage(name)}`)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let images = response.data.data.DerivedImageList;
|
||||
setImages(images);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
//setImages([]);
|
||||
});
|
||||
//setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
component="div"
|
||||
align="left"
|
||||
className={classes.title}
|
||||
style={{ color: 'rgba(0, 0, 0, 0.87)', fontSize: '1.5rem', fontWeight: '600', paddingTop: '0.5rem' }}
|
||||
>
|
||||
Is Dependent On
|
||||
</Typography>
|
||||
<Divider
|
||||
variant="fullWidth"
|
||||
sx={{ margin: '5% 0% 5% 0%', background: 'rgba(0, 0, 0, 0.38)', height: '0.00625rem', width: '100%' }}
|
||||
/>
|
||||
|
||||
{images?.length ? (
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent>
|
||||
<Typography className={classes.content}>
|
||||
{images.map((dependence, index) => {
|
||||
return (
|
||||
<Link key={index} to={`/image/${encodeURIComponent(dependence.RepoName)}`} className={classes.link}>
|
||||
{dependence.RepoName}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div>
|
||||
<img src={Monitor} alt="Monitor" className={classes.monitor}></img>
|
||||
<Typography className={classes.none}> Nothing found </Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IsDependentOn;
|
@ -20,6 +20,7 @@ import TagDetailsMetadata from './TagDetailsMetadata';
|
||||
import VulnerabilitiesDetails from './VulnerabilitiesDetails';
|
||||
import HistoryLayers from './HistoryLayers';
|
||||
import DependsOn from './DependsOn';
|
||||
import IsDependentOn from './IsDependentOn';
|
||||
|
||||
// @ts-ignore
|
||||
const useStyles = makeStyles(() => ({
|
||||
@ -136,14 +137,14 @@ function TagDetails() {
|
||||
let repoInfo = response.data.data.ExpandedRepoInfo;
|
||||
let imageData = {
|
||||
name: name,
|
||||
tags: repoInfo.Images[0].Tag,
|
||||
tags: repoInfo.Images[0]?.Tag,
|
||||
lastUpdated: repoInfo.Summary?.LastUpdated,
|
||||
size: repoInfo.Summary?.Size,
|
||||
latestDigest: repoInfo.Images[0].Digest,
|
||||
layers: repoInfo.Images[0].Layers,
|
||||
platforms: repoInfo.Summary?.Platforms,
|
||||
vendors: repoInfo.Summary?.Vendors,
|
||||
newestTag: repoInfo.Summary?.NewestImage.Tag
|
||||
newestTag: repoInfo.Summary?.NewestImage?.Tag
|
||||
};
|
||||
setRepoDetailData(imageData);
|
||||
setTagName(imageData.name + ':' + imageData.newestTag);
|
||||
@ -267,7 +268,12 @@ function TagDetails() {
|
||||
sx={{ '& button.Mui-selected': { color: '#14191F', fontWeight: '600' } }}
|
||||
>
|
||||
<Tab value="Layers" label="Layers" className={classes.tabContent} />
|
||||
<Tab value="DependsOn" label="Depends on" className={classes.tabContent} />
|
||||
<Tab
|
||||
value="DependsOn"
|
||||
label="Depends on"
|
||||
className={classes.tabContent}
|
||||
data-testid="dependencies-tab"
|
||||
/>
|
||||
<Tab value="IsDependentOn" label="Is dependent on" className={classes.tabContent} />
|
||||
<Tab value="Vulnerabilities" label="Vulnerabilities" className={classes.tabContent} />
|
||||
</TabList>
|
||||
@ -280,7 +286,7 @@ function TagDetails() {
|
||||
<DependsOn name={tagName} />
|
||||
</TabPanel>
|
||||
<TabPanel value="IsDependentOn" className={classes.tabPanel}>
|
||||
<Typography> Is Dependent On </Typography>
|
||||
<IsDependentOn name={tagName} />
|
||||
</TabPanel>
|
||||
<TabPanel value="Vulnerabilities" className={classes.tabPanel}>
|
||||
<VulnerabilitiesDetails name={name} />
|
||||
|
@ -10,6 +10,7 @@ import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../host';
|
||||
import PestControlOutlinedIcon from '@mui/icons-material/PestControlOutlined';
|
||||
import PestControlIcon from '@mui/icons-material/PestControl';
|
||||
import Monitor from '../assets/Monitor.png';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
@ -46,6 +47,16 @@ const useStyles = makeStyles(() => ({
|
||||
fontWeight: '600',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem'
|
||||
},
|
||||
monitor: {
|
||||
width: '27.25rem',
|
||||
height: '24.625rem',
|
||||
paddingTop: '2rem'
|
||||
},
|
||||
none: {
|
||||
color: '#52637A',
|
||||
fontSize: '1.4rem',
|
||||
fontWeight: '600'
|
||||
}
|
||||
}));
|
||||
|
||||
@ -194,6 +205,7 @@ function VulnerabilitiyCard(props) {
|
||||
}
|
||||
|
||||
function VulnerabilitiesDetails(props) {
|
||||
const classes = useStyles();
|
||||
const [cveData, setCveData] = useState({});
|
||||
// const [isLoading, setIsLoading] = useState(true);
|
||||
const { name } = props;
|
||||
@ -226,7 +238,12 @@ function VulnerabilitiesDetails(props) {
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return <Typography> No Vulnerabilities </Typography>;
|
||||
return (
|
||||
<div>
|
||||
<img src={Monitor} alt="Monitor" className={classes.monitor}></img>
|
||||
<Typography className={classes.none}> No Vulnerabilities </Typography>{' '}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -16,5 +16,5 @@ code {
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user