feat: multi-arch image features
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
parent
4db0c2ee8b
commit
9029b97b47
@ -11,38 +11,50 @@ jest.mock('react-router-dom', () => ({
|
||||
|
||||
const mockedTagsData = [
|
||||
{
|
||||
Digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
Tag: 'latest',
|
||||
LastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
Vendor: 'test1',
|
||||
Size: '569130088',
|
||||
Platform: {
|
||||
tag: 'latest',
|
||||
vendor: 'test1',
|
||||
manifests: [
|
||||
{
|
||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
size: '569130088',
|
||||
platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
Tag: 'bullseye',
|
||||
LastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
Vendor: 'test1',
|
||||
Size: '569130088',
|
||||
Platform: {
|
||||
tag: 'bullseye',
|
||||
vendor: 'test1',
|
||||
manifests: [
|
||||
{
|
||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
size: '569130088',
|
||||
platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
Tag: '1.5.2',
|
||||
LastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
Vendor: 'test1',
|
||||
Size: '569130088',
|
||||
Platform: {
|
||||
tag: '1.5.2',
|
||||
vendor: 'test1',
|
||||
manifests: [
|
||||
{
|
||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
size: '569130088',
|
||||
platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
describe('Tags component', () => {
|
||||
@ -60,7 +72,20 @@ describe('Tags component', () => {
|
||||
const tagLink = await screen.findByText('latest');
|
||||
fireEvent.click(tagLink);
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest');
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest', { state: { digest: null } });
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to specific manifest when clicking the digest', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
const openBtn = screen.getAllByText(/digest/i);
|
||||
await fireEvent.click(openBtn[0]);
|
||||
const tagLink = await screen.findByText(/sha256:adca4/i);
|
||||
fireEvent.click(tagLink);
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest', {
|
||||
state: { digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import TagDetails from 'components/Tag/TagDetails';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||
|
||||
const TagDetailsThemeWrapper = () => {
|
||||
return (
|
||||
@ -72,6 +72,102 @@ const mockImage = {
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf45etertdfg973e29',
|
||||
LastUpdated: '2020-12-08T00:22:52.526672082Z',
|
||||
Size: '75183423',
|
||||
ConfigDigest: 'sha256:8dd57e171a61368ffcfde38045ddb6ed74a32950c271c1da93eaddfb66a77e78',
|
||||
Platform: {
|
||||
Os: 'windows',
|
||||
Arch: 'amd64'
|
||||
},
|
||||
History: [
|
||||
{
|
||||
Layer: {
|
||||
Size: '75181999',
|
||||
Digest: 'sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621',
|
||||
Score: null
|
||||
},
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:52.526672082Z',
|
||||
CreatedBy:
|
||||
'/bin/sh -c #(nop) ADD file:bd7a2aed6ede423b719ceb2f723e4ecdfa662b28639c8429731c878e86fb138b in / ',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: false
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:52.895811646Z',
|
||||
CreatedBy:
|
||||
'/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20201204',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:53.076477777Z',
|
||||
CreatedBy: '/bin/sh -c #(nop) CMD ["/bin/bash"]',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e25',
|
||||
LastUpdated: '2020-12-08T00:22:52.526672082Z',
|
||||
Size: '75183423',
|
||||
ConfigDigest: 'sha256:8dd57e171a61368ffcfde38045ddb6ed74a32950c271c1da93eaddfb66a77e78',
|
||||
Platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'arm'
|
||||
},
|
||||
History: [
|
||||
{
|
||||
Layer: {
|
||||
Size: '75181999',
|
||||
Digest: 'sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621',
|
||||
Score: null
|
||||
},
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:52.526672082Z',
|
||||
CreatedBy:
|
||||
'/bin/sh -c #(nop) ADD file:bd7a2aed6ede423b719ceb2f723e4ecdfa662b28639c8429731c878e86fb138b in / ',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: false
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:52.895811646Z',
|
||||
CreatedBy:
|
||||
'/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20201204',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:53.076477777Z',
|
||||
CreatedBy: '/bin/sh -c #(nop) CMD ["/bin/bash"]',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
Vulnerabilities: {
|
||||
@ -285,7 +381,8 @@ jest.mock('react-router-dom', () => ({
|
||||
useParams: () => {
|
||||
return { name: 'test', tag: '1.0.1' };
|
||||
},
|
||||
useNavigate: () => mockUseNavigate
|
||||
useNavigate: () => mockUseNavigate,
|
||||
useLocation: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock('../../host', () => ({
|
||||
@ -328,6 +425,24 @@ describe('Tags details', () => {
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should show the data of the different manifests when switching between them', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
const manifestSelect = await screen.findByText(/linux\/amd64/i);
|
||||
await userEvent.click(manifestSelect);
|
||||
await userEvent.click(await screen.findByText(/windows\/amd64/i));
|
||||
expect(await screen.findByText(/windows\/amd64/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should preselect a manifest if data is received', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImage } });
|
||||
useLocation.mockImplementation(() => ({
|
||||
state: { digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e25' }
|
||||
}));
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
expect(await screen.findByText(/linux\/arm/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should redirect to homepage if it receives invalid data', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: null, errors: ['testerror'] } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
|
@ -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}} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor Size } 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 Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||
detailedImageInfo: (name, tag) =>
|
||||
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned Vulnerabilities {MaxSeverity Count} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
||||
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }) =>
|
||||
|
@ -6,7 +6,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { api, endpoints } from 'api';
|
||||
import { host } from 'host';
|
||||
import { mapToImage, mapToRepo } from 'utilities/objectModels';
|
||||
import { createSearchParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { createSearchParams, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { debounce, isEmpty } from 'lodash';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { HEADER_SEARCH_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||
@ -104,6 +104,7 @@ function SearchSuggestion() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isFailedSearch, setIsFailedSearch] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
|
||||
const classes = useStyles();
|
||||
@ -180,7 +181,9 @@ function SearchSuggestion() {
|
||||
};
|
||||
|
||||
const searchCall = (value) => {
|
||||
if (location.pathname?.includes('explore')) {
|
||||
setQueryParams((prevState) => createSearchParams({ ...prevState, search: searchQuery }));
|
||||
}
|
||||
if (value !== '') {
|
||||
// if search term inclused the ':' character, search for images, if not, search repos
|
||||
if (value?.includes(':')) {
|
||||
|
@ -20,7 +20,7 @@ import { TabContext, TabList, TabPanel } from '@mui/lab';
|
||||
|
||||
import RepoDetailsMetadata from './RepoDetailsMetadata';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, uniq } from 'lodash';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
|
||||
|
||||
@ -164,6 +164,7 @@ function RepoDetails() {
|
||||
const classes = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
api
|
||||
.get(`${host()}${endpoints.detailedRepoInfo(name)}`, abortController.signal)
|
||||
.then((response) => {
|
||||
@ -197,22 +198,13 @@ function RepoDetails() {
|
||||
|
||||
const platformChips = () => {
|
||||
const platforms = repoDetailData?.platforms || [];
|
||||
const filteredPlatforms = platforms?.flatMap((platform) => [platform.Os, platform.Arch]);
|
||||
|
||||
return platforms.map((platform, index) => (
|
||||
<Stack key={`stack${platform?.Os}${platform?.Arch}`} alignItems="center" direction="row" spacing={2}>
|
||||
return uniq(filteredPlatforms).map((platform, index) => (
|
||||
<Stack key={`stack${platform}`} alignItems="center" direction="row" spacing={2}>
|
||||
<Chip
|
||||
key={`${name}${platform?.Os}${index}`}
|
||||
label={platform?.Os}
|
||||
onClick={handlePlatformChipClick}
|
||||
sx={{
|
||||
backgroundColor: '#E0E5EB',
|
||||
color: '#52637A',
|
||||
fontSize: '0.8125rem'
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
key={`${name}${platform?.Arch}${index}`}
|
||||
label={platform?.Arch}
|
||||
key={`${name}${platform}${index}`}
|
||||
label={platform}
|
||||
onClick={handlePlatformChipClick}
|
||||
sx={{
|
||||
backgroundColor: '#E0E5EB',
|
||||
|
@ -1,8 +1,6 @@
|
||||
// react global
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { head } from 'lodash';
|
||||
|
||||
// components
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Card, CardContent, Divider, Stack, InputBase, FormControl, Select, InputLabel, MenuItem } from '@mui/material';
|
||||
@ -77,7 +75,7 @@ export default function Tags(props) {
|
||||
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));
|
||||
const filteredTags = tags.filter((t) => t.tag?.includes(tagsFilter));
|
||||
if (selectedSort) {
|
||||
filteredTags.sort(selectedSort.func);
|
||||
}
|
||||
@ -86,13 +84,11 @@ export default function Tags(props) {
|
||||
filteredTags.map((tag) => {
|
||||
return (
|
||||
<TagCard
|
||||
key={tag.Tag}
|
||||
tag={tag.Tag}
|
||||
lastUpdated={tag.LastUpdated}
|
||||
digest={head(tag.Manifests)?.Digest}
|
||||
vendor={tag.Vendor}
|
||||
size={tag.Size}
|
||||
platform={head(tag.Manifests)?.Platform}
|
||||
key={tag.tag}
|
||||
tag={tag.tag}
|
||||
lastUpdated={tag.lastUpdated}
|
||||
vendor={tag.vendor}
|
||||
manifests={tag.manifests}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -16,7 +16,7 @@ import repocube4 from '../../assets/repocube-4.png';
|
||||
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, uniq } from 'lodash';
|
||||
|
||||
// temporary utility to get image
|
||||
const randomIntFromInterval = (min, max) => {
|
||||
@ -119,22 +119,12 @@ function RepoCard(props) {
|
||||
};
|
||||
|
||||
const platformChips = () => {
|
||||
const platformsOsArch = platforms || [];
|
||||
return platformsOsArch.map((platform, index) => (
|
||||
<Stack key={`stack${platform?.Os}${platform?.Arch}`} alignItems="center" direction="row" spacing={2}>
|
||||
const filteredPlatforms = platforms?.flatMap((platform) => [platform.Os, platform.Arch]);
|
||||
return uniq(filteredPlatforms).map((platform, index) => (
|
||||
<Stack key={`stack${platform}`} alignItems="center" direction="row" spacing={2}>
|
||||
<Chip
|
||||
key={`${name}${platform?.Os}${index}`}
|
||||
label={platform?.Os}
|
||||
onClick={handlePlatformChipClick}
|
||||
sx={{
|
||||
backgroundColor: '#E0E5EB',
|
||||
color: '#52637A',
|
||||
fontSize: '0.8125rem'
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
key={`${name}${platform?.Arch}${index}`}
|
||||
label={platform?.Arch}
|
||||
key={`${name}${platform}${index}`}
|
||||
label={platform}
|
||||
onClick={handlePlatformChipClick}
|
||||
sx={{
|
||||
backgroundColor: '#E0E5EB',
|
||||
|
@ -60,7 +60,7 @@ const useStyles = makeStyles(() => ({
|
||||
}));
|
||||
|
||||
export default function TagCard(props) {
|
||||
const { repoName, tag, lastUpdated, vendor, digest, size, platform } = props;
|
||||
const { repoName, tag, lastUpdated, vendor, manifests } = props;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const classes = useStyles();
|
||||
@ -70,11 +70,11 @@ export default function TagCard(props) {
|
||||
: `Timestamp N/A`;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const goToTags = () => {
|
||||
const goToTags = (digest = null) => {
|
||||
if (repoName) {
|
||||
navigate(`/image/${encodeURIComponent(repoName)}/tag/${tag}`);
|
||||
navigate(`/image/${encodeURIComponent(repoName)}/tag/${tag}`, { state: { digest } });
|
||||
} else {
|
||||
navigate(`tag/${tag}`);
|
||||
navigate(`tag/${tag}`, { state: { digest } });
|
||||
}
|
||||
};
|
||||
|
||||
@ -135,23 +135,38 @@ export default function TagCard(props) {
|
||||
<Typography variant="body1"> Size </Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container item xs={12} direction={'row'}>
|
||||
|
||||
{manifests.map((el) => (
|
||||
<Grid container item xs={12} key={el.digest} direction={'row'}>
|
||||
<Grid item xs={6} md={4}>
|
||||
<Tooltip title={digest || ''} placement="top">
|
||||
<Typography variant="body1">{digest?.substr(0, 12)}</Typography>
|
||||
<Tooltip title={el.digest || ''} placement="top">
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: '#1479FF', textDecorationLine: 'underline', cursor: 'pointer' }}
|
||||
onClick={() => goToTags(el.digest)}
|
||||
>
|
||||
{el.digest?.substr(0, 12)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={4} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Typography variant="body1">
|
||||
{platform?.Os}/{platform?.Arch}
|
||||
{el.platform?.Os}/{el.platform?.Arch}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={0} md={4} className="hide-on-mobile" sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Grid
|
||||
item
|
||||
xs={0}
|
||||
md={4}
|
||||
className="hide-on-mobile"
|
||||
sx={{ display: 'flex', justifyContent: 'flex-end' }}
|
||||
>
|
||||
<Typography sx={{ textAlign: 'right' }} variant="body1">
|
||||
{transform.formatBytes(size)}
|
||||
{transform.formatBytes(el.size)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</CardContent>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { isEmpty, head } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../../api';
|
||||
@ -148,10 +148,8 @@ function DependsOn(props) {
|
||||
repoName={dependence.repoName}
|
||||
tag={dependence.tag}
|
||||
vendor={dependence.vendor}
|
||||
platform={head(dependence.manifests)?.platform}
|
||||
isSigned={dependence.isSigned}
|
||||
size={head(dependence.manifests)?.size}
|
||||
digest={head(dependence.manifests)?.digest}
|
||||
manifests={dependence.manifests}
|
||||
key={index}
|
||||
lastUpdated={dependence.lastUpdated}
|
||||
/>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { isEmpty, head } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../../api';
|
||||
@ -148,10 +148,8 @@ function IsDependentOn(props) {
|
||||
repoName={dependence.repoName}
|
||||
tag={dependence.tag}
|
||||
vendor={dependence.vendor}
|
||||
platform={head(dependence.manifests)?.platform}
|
||||
isSigned={dependence.isSigned}
|
||||
size={head(dependence.manifests)?.size}
|
||||
digest={head(dependence.manifests)?.digest}
|
||||
manifests={dependence.manifests}
|
||||
key={index}
|
||||
lastUpdated={dependence.lastUpdated}
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
|
||||
// utility
|
||||
@ -19,7 +19,8 @@ import {
|
||||
MenuItem,
|
||||
Tab,
|
||||
Typography,
|
||||
InputBase
|
||||
InputBase,
|
||||
InputLabel
|
||||
} from '@mui/material';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
@ -220,6 +221,10 @@ function TagDetails() {
|
||||
const mounted = useRef(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// check for optional preselected digest
|
||||
const { state } = useLocation() || {};
|
||||
const { digest } = state || '';
|
||||
|
||||
// get url param from <Route here (i.e. image name)
|
||||
const { reponame, tag } = useParams();
|
||||
|
||||
@ -240,7 +245,16 @@ function TagDetails() {
|
||||
let imageInfo = response.data.data.Image;
|
||||
let imageData = mapToImage(imageInfo);
|
||||
setImageDetailData(imageData);
|
||||
if (!isEmpty(digest)) {
|
||||
const preselectedManifest = imageData.manifests?.find((el) => el.digest === digest);
|
||||
if (preselectedManifest) {
|
||||
setSelectedManifest(preselectedManifest);
|
||||
} else {
|
||||
setSelectedManifest(head(imageData.manifests));
|
||||
}
|
||||
} else {
|
||||
setSelectedManifest(head(imageData.manifests));
|
||||
}
|
||||
setPullString(dockerPull(imageData.name));
|
||||
setSelectedPullTab(dockerPull(imageData.name));
|
||||
} else if (!isEmpty(response.data.errors)) {
|
||||
@ -286,6 +300,11 @@ function TagDetails() {
|
||||
return 'Pull Image';
|
||||
};
|
||||
|
||||
const handleOSArchChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setSelectedManifest(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
@ -329,6 +348,26 @@ function TagDetails() {
|
||||
<SignatureIconCheck isSigned={imageDetailData.isSigned} />
|
||||
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<FormControl sx={{ m: '1', minWidth: '4.6875rem' }} className={classes.sortForm} size="small">
|
||||
<InputLabel>OS/Arch</InputLabel>
|
||||
{!isEmpty(selectedManifest) && (
|
||||
<Select
|
||||
label="OS/Arch"
|
||||
value={selectedManifest}
|
||||
onChange={handleOSArchChange}
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
>
|
||||
{imageDetailData.manifests.map((el) => (
|
||||
<MenuItem key={el.digest} value={el}>
|
||||
{`${el.platform?.Os}/${el.platform?.Arch}`}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Typography gutterBottom className={classes.digest}>
|
||||
DIGEST: {selectedManifest?.digest}
|
||||
|
@ -20,7 +20,7 @@ const mapToRepo = (responseRepo) => {
|
||||
const mapToRepoFromRepoInfo = (responseRepoInfo) => {
|
||||
return {
|
||||
name: responseRepoInfo.Summary?.Name,
|
||||
images: responseRepoInfo.Images,
|
||||
images: responseRepoInfo.Images?.map((image) => mapToImage(image)) || [],
|
||||
lastUpdated: responseRepoInfo.Summary?.LastUpdated,
|
||||
size: responseRepoInfo.Summary?.Size,
|
||||
platforms: responseRepoInfo.Summary?.Platforms,
|
||||
|
@ -32,28 +32,28 @@ export const tagsSortByCriteria = {
|
||||
value: 'UPDATETIME_DESC',
|
||||
label: 'Newest',
|
||||
func: (a, b) => {
|
||||
return DateTime.fromISO(b.LastUpdated).diff(DateTime.fromISO(a.LastUpdated));
|
||||
return DateTime.fromISO(b.lastUpdated).diff(DateTime.fromISO(a.lastUpdated));
|
||||
}
|
||||
},
|
||||
updateTime: {
|
||||
value: 'UPDATETIME',
|
||||
label: 'Oldest',
|
||||
func: (a, b) => {
|
||||
return DateTime.fromISO(a.LastUpdated).diff(DateTime.fromISO(b.LastUpdated));
|
||||
return DateTime.fromISO(a.lastUpdated).diff(DateTime.fromISO(b.lastUpdated));
|
||||
}
|
||||
},
|
||||
alphabetic: {
|
||||
value: 'ALPHABETIC',
|
||||
label: 'A - Z',
|
||||
func: (a, b) => {
|
||||
return a.Tag?.localeCompare(b.Tag);
|
||||
return a.tag?.localeCompare(b.tag);
|
||||
}
|
||||
},
|
||||
alphabeticDesc: {
|
||||
value: 'ALPHABETIC_DESC',
|
||||
label: 'Z - A',
|
||||
func: (a, b) => {
|
||||
return b.Tag?.localeCompare(a.Tag);
|
||||
return b.tag?.localeCompare(a.tag);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user