Added tag page with vulnerabilities. (#82)

Added the Image Tag details page

Signed-off-by: Amelia-Maria Breda <ameliamaria.breda@dxc.com>
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Amelia-Maria Breda 2022-09-21 12:21:58 +03:00 committed by GitHub
parent 10a349fdcd
commit 1870b53656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1521 additions and 221 deletions

View File

@ -9,6 +9,7 @@ import './App.css';
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
import { AuthWrapper } from 'utilities/AuthWrapper.jsx';
import RepoPage from 'pages/RepoPage.jsx';
import TagPage from 'pages/TagPage';
import ExplorePage from 'pages/ExplorePage.jsx';
const useStyles = makeStyles((theme) => ({
@ -38,6 +39,7 @@ function App() {
<Route path="/home" element={<HomePage keywords={searchKeywords} updateKeywords={setSearchKeywords} data={data} updateData={setData} />} />
<Route path="/explore" element={<ExplorePage keywords={searchKeywords} updateKeywords={setSearchKeywords} data={data} updateData={setData} />} />
<Route path="/image/:name" element={<RepoPage />} />
<Route path="/image/:name/tag/:tag" element={<TagPage />} />
</Route>
<Route element={<AuthWrapper isLoggedIn={!isLoggedIn} redirect="/"/>}>
<Route path="/login" element={<LoginPage isAuthEnabled={isAuthEnabled} setIsAuthEnabled={setIsAuthEnabled} isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} />} />

View File

@ -0,0 +1,19 @@
import { render, screen , fireEvent } from '@testing-library/react';
import FilterCard from 'components/FilterCard';
import React, {useState} from 'react';
const StateFilterCardWrapper = () => {
return (<FilterCard title="Products" filters={["Images","Plugins"]} />)
}
describe('Filters components', () => {
it('renders the filters cards', () => {
render(<StateFilterCardWrapper/> );
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
const checkbox = screen.getAllByRole('checkbox');
expect(checkbox[0]).not.toBeChecked()
fireEvent.click(checkbox[0])
expect(checkbox[0]).toBeChecked()
});
});

View File

@ -66,9 +66,9 @@ describe('Home component', () => {
// @ts-ignore
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } })
render(<StateHomeWrapper/>);
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(3));
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(3));
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(5));
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(2));
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(2));
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
});
it('should log an error when data can\'t be fetched', async() => {

View File

@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent,waitFor, render, screen } from '@testing-library/react';
import Tags from 'components/Tags';
import React from 'react';
@ -10,27 +10,48 @@ jest.mock('react-router-dom', () => ({
}));
const mockedTagsData = {
name: 'test',
tags: [
{
"Digest": "2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718",
"Tag": "latest",
"Layers": [
{
"Size": "2798889",
"Digest": "2408cc74d12b6cd092bb8b516ba7d5e290f485d3eb9672efc00f0583730179e8"
}
]
}
]
};
"name": "alpine",
"images": [
{
"Digest": "59118d0816d2e8e05cb04c328224056b3ce07d7afc2ad59e2f1f08bb0ba2ff3c",
"Tag": "latest",
"Layers": [
{
"Size": "2806054",
"Digest": "213ec9aee27d8be045c6a92b7eac22c9a64b44558193775a1a7f626352392b49"
}
]
}
],
"lastUpdated": "2022-08-09T17:19:53.274069586Z",
"size": "2806985",
"platforms": [
{
"Os": "linux",
"Arch": "amd64"
}
],
"vendors": [
""
],
"newestTag": null
}
describe('Tags component', () => {
it('should open and close details dropdown for tags', () => {
render(<Tags data={mockedTagsData}/>);
const openBtn = screen.getByText(/see layers/i);
const openBtn = screen.getByText(/see digests/i);
fireEvent.click(openBtn);
expect(screen.queryByText(/see layers/i)).not.toBeInTheDocument();
expect(screen.getByText(/hide layers/i)).toBeInTheDocument();
expect(screen.queryByText(/see digests/i)).not.toBeInTheDocument();
expect(screen.getByText(/hide digests/i)).toBeInTheDocument();
});
it('should navigate to tag page details when tag is clicked', async() => {
render(<Tags data={mockedTagsData}/>);
const tagLink = await screen.findByText('latest');
fireEvent.click(tagLink);
await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest');
});
})
})

View File

@ -0,0 +1,449 @@
import { render, screen , waitFor, fireEvent } from '@testing-library/react';
import { api } from 'api';
import VulnerabilitiesDetails from 'components/VulnerabilitiesDetails';
import React, {useState} from 'react';
const StateVulnerabilitiesWrapper = () => {
return (<VulnerabilitiesDetails name='mongo' />)
}
const mockCVEList = {
CVEListForImage: {
"Tag": "",
"CVEList": [
{
"Id": "CVE-2020-16156",
"Title": "perl-CPAN: Bypass of verification of signatures in CHECKSUMS files",
"Description": "CPAN 2.28 allows Signature Verification Bypass.",
"Severity": "MEDIUM",
"PackageList": [
{
"Name": "perl-base",
"InstalledVersion": "5.30.0-9ubuntu0.2",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2021-36222",
"Title": "krb5: Sending a request containing PA-ENCRYPTED-CHALLENGE padata element without using FAST could result in NULL dereference in KDC which leads to DoS",
"Description": "ec_verify in kdc/kdc_preauth_ec.c in the Key Distribution Center (KDC) in MIT Kerberos 5 (aka krb5) before 1.18.4 and 1.19.x before 1.19.2 allows remote attackers to cause a NULL pointer dereference and daemon crash. This occurs because a return value is not properly managed in a certain situation.",
"Severity": "HIGH",
"PackageList": [
{
"Name": "krb5-locales",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
},
{
"Name": "libgssapi-krb5-2",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
},
{
"Name": "libk5crypto3",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
},
{
"Name": "libkrb5-3",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
},
{
"Name": "libkrb5support0",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2021-4209",
"Title": "GnuTLS: Null pointer dereference in MD_UPDATE",
"Description": "A NULL pointer dereference flaw was found in GnuTLS. As Nettle's hash update functions internally call memcpy, providing zero-length input may cause undefined behavior. This flaw leads to a denial of service after authentication in rare circumstances.",
"Severity": "LOW",
"PackageList": [
{
"Name": "libgnutls30",
"InstalledVersion": "3.6.13-2ubuntu1.6",
"FixedVersion": "3.6.13-2ubuntu1.7"
}
]
},
{
"Id": "CVE-2022-1586",
"Title": "pcre2: Out-of-bounds read in compile_xclass_matchingpath in pcre2_jit_compile.c",
"Description": "An out-of-bounds read vulnerability was discovered in the PCRE2 library in the compile_xclass_matchingpath() function of the pcre2_jit_compile.c file. This involves a unicode property matching issue in JIT-compiled regular expressions. The issue occurs because the character was not fully read in case-less matching within JIT.",
"Severity": "CRITICAL",
"PackageList": [
{
"Name": "libpcre2-8-0",
"InstalledVersion": "10.34-7",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2021-20223",
"Title": "",
"Description": "An issue was found in fts5UnicodeTokenize() in ext/fts5/fts5_tokenize.c in Sqlite. A unicode61 tokenizer configured to treat unicode \"control-characters\" (class Cc), was treating embedded nul characters as tokens. The issue was fixed in sqlite-3.34.0 and later.",
"Severity": "NONE",
"PackageList": [
{
"Name": "libsqlite3-0",
"InstalledVersion": "3.31.1-4ubuntu0.3",
"FixedVersion": "3.31.1-4ubuntu0.4"
}
]
},
{
"Id": "CVE-2017-11164",
"Title": "pcre: OP_KETRMAX feature in the match function in pcre_exec.c",
"Description": "In PCRE 8.41, the OP_KETRMAX feature in the match function in pcre_exec.c allows stack exhaustion (uncontrolled recursion) when processing a crafted regular expression.",
"Severity": "UNKNOWN",
"PackageList": [
{
"Name": "libpcre3",
"InstalledVersion": "2:8.39-12ubuntu0.1",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2020-35527",
"Title": "sqlite: Out of bounds access during table rename",
"Description": "In SQLite 3.31.1, there is an out of bounds access problem through ALTER TABLE for views that have a nested FROM clause.",
"Severity": "MEDIUM",
"PackageList": [
{
"Name": "libsqlite3-0",
"InstalledVersion": "3.31.1-4ubuntu0.3",
"FixedVersion": "3.31.1-4ubuntu0.4"
}
]
},
{
"Id": "CVE-2013-4235",
"Title": "shadow-utils: TOCTOU race conditions by copying and removing directory trees",
"Description": "shadow: TOCTOU (time-of-check time-of-use) race condition when copying and removing directory trees",
"Severity": "LOW",
"PackageList": [
{
"Name": "login",
"InstalledVersion": "1:4.8.1-1ubuntu5.20.04.2",
"FixedVersion": "Not Specified"
},
{
"Name": "passwd",
"InstalledVersion": "1:4.8.1-1ubuntu5.20.04.2",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2021-43618",
"Title": "gmp: Integer overflow and resultant buffer overflow via crafted input",
"Description": "GNU Multiple Precision Arithmetic Library (GMP) through 6.2.1 has an mpz/inp_raw.c integer overflow and resultant buffer overflow via crafted input, leading to a segmentation fault on 32-bit platforms.",
"Severity": "LOW",
"PackageList": [
{
"Name": "libgmp10",
"InstalledVersion": "2:6.2.0+dfsg-4",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2022-2509",
"Title": "gnutls: Double free during gnutls_pkcs7_verify.",
"Description": "A vulnerability found in gnutls. This security flaw happens because of a double free error occurs during verification of pkcs7 signatures in gnutls_pkcs7_verify function.",
"Severity": "MEDIUM",
"PackageList": [
{
"Name": "libgnutls30",
"InstalledVersion": "3.6.13-2ubuntu1.6",
"FixedVersion": "3.6.13-2ubuntu1.7"
}
]
},
{
"Id": "CVE-2021-39537",
"Title": "ncurses: heap-based buffer overflow in _nc_captoinfo() in captoinfo.c",
"Description": "An issue was discovered in ncurses through v6.2-1. _nc_captoinfo in captoinfo.c has a heap-based buffer overflow.",
"Severity": "LOW",
"PackageList": [
{
"Name": "libncurses6",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
},
{
"Name": "libncursesw6",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
},
{
"Name": "libtinfo6",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
},
{
"Name": "ncurses-base",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
},
{
"Name": "ncurses-bin",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2022-1587",
"Title": "pcre2: Out-of-bounds read in get_recurse_data_length in pcre2_jit_compile.c",
"Description": "An out-of-bounds read vulnerability was discovered in the PCRE2 library in the get_recurse_data_length() function of the pcre2_jit_compile.c file. This issue affects recursions in JIT-compiled regular expressions caused by duplicate data transfers.",
"Severity": "LOW",
"PackageList": [
{
"Name": "libpcre2-8-0",
"InstalledVersion": "10.34-7",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2022-29458",
"Title": "ncurses: segfaulting OOB read",
"Description": "ncurses 6.3 before patch 20220416 has an out-of-bounds read and segmentation violation in convert_strings in tinfo/read_entry.c in the terminfo library.",
"Severity": "LOW",
"PackageList": [
{
"Name": "libncurses6",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
},
{
"Name": "libncursesw6",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
},
{
"Name": "libtinfo6",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
},
{
"Name": "ncurses-base",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
},
{
"Name": "ncurses-bin",
"InstalledVersion": "6.2-0ubuntu2",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2016-2781",
"Title": "coreutils: Non-privileged session can escape to the parent session in chroot",
"Description": "chroot in GNU coreutils, when used with --userspec, allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.",
"Severity": "LOW",
"PackageList": [
{
"Name": "coreutils",
"InstalledVersion": "8.30-3ubuntu2",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2021-3671",
"Title": "samba: Null pointer dereference on missing sname in TGS-REQ",
"Description": "A null pointer de-reference was found in the way samba kerberos server handled missing sname in TGS-REQ (Ticket Granting Server - Request). An authenticated user could use this flaw to crash the samba server.",
"Severity": "LOW",
"PackageList": [
{
"Name": "libasn1-8-heimdal",
"InstalledVersion": "7.7.0+dfsg-1ubuntu1",
"FixedVersion": "Not Specified"
},
{
"Name": "libgssapi3-heimdal",
"InstalledVersion": "7.7.0+dfsg-1ubuntu1",
"FixedVersion": "Not Specified"
},
{
"Name": "libhcrypto4-heimdal",
"InstalledVersion": "7.7.0+dfsg-1ubuntu1",
"FixedVersion": "Not Specified"
},
{
"Name": "libheimbase1-heimdal",
"InstalledVersion": "7.7.0+dfsg-1ubuntu1",
"FixedVersion": "Not Specified"
},
{
"Name": "libheimntlm0-heimdal",
"InstalledVersion": "7.7.0+dfsg-1ubuntu1",
"FixedVersion": "Not Specified"
},
{
"Name": "libhx509-5-heimdal",
"InstalledVersion": "7.7.0+dfsg-1ubuntu1",
"FixedVersion": "Not Specified"
},
{
"Name": "libkrb5-26-heimdal",
"InstalledVersion": "7.7.0+dfsg-1ubuntu1",
"FixedVersion": "Not Specified"
},
{
"Name": "libroken18-heimdal",
"InstalledVersion": "7.7.0+dfsg-1ubuntu1",
"FixedVersion": "Not Specified"
},
{
"Name": "libwind0-heimdal",
"InstalledVersion": "7.7.0+dfsg-1ubuntu1",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2016-20013",
"Title": "",
"Description": "sha256crypt and sha512crypt through 0.6 allow attackers to cause a denial of service (CPU consumption) because the algorithm's runtime is proportional to the square of the length of the password.",
"Severity": "LOW",
"PackageList": [
{
"Name": "libc-bin",
"InstalledVersion": "2.31-0ubuntu9.9",
"FixedVersion": "Not Specified"
},
{
"Name": "libc6",
"InstalledVersion": "2.31-0ubuntu9.9",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2022-35252",
"Title": "curl: control code in cookie denial of service",
"Description": "No description is available for this CVE.",
"Severity": "LOW",
"PackageList": [
{
"Name": "libcurl4",
"InstalledVersion": "7.68.0-1ubuntu2.12",
"FixedVersion": "7.68.0-1ubuntu2.13"
}
]
},
{
"Id": "CVE-2021-37750",
"Title": "krb5: NULL pointer dereference in process_tgs_req() in kdc/do_tgs_req.c via a FAST inner body that lacks server field",
"Description": "The Key Distribution Center (KDC) in MIT Kerberos 5 (aka krb5) before 1.18.5 and 1.19.x before 1.19.3 has a NULL pointer dereference in kdc/do_tgs_req.c via a FAST inner body that lacks a server field.",
"Severity": "MEDIUM",
"PackageList": [
{
"Name": "krb5-locales",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
},
{
"Name": "libgssapi-krb5-2",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
},
{
"Name": "libk5crypto3",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
},
{
"Name": "libkrb5-3",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
},
{
"Name": "libkrb5support0",
"InstalledVersion": "1.17-6ubuntu4.1",
"FixedVersion": "Not Specified"
}
]
},
{
"Id": "CVE-2020-35525",
"Title": "sqlite: Null pointer derreference in src/select.c",
"Description": "In SQlite 3.31.1, a potential null pointer derreference was found in the INTERSEC query processing.",
"Severity": "MEDIUM",
"PackageList": [
{
"Name": "libsqlite3-0",
"InstalledVersion": "3.31.1-4ubuntu0.3",
"FixedVersion": "3.31.1-4ubuntu0.4"
}
]
},
{
"Id": "CVE-2022-37434",
"Title": "zlib: a heap-based buffer over-read or buffer overflow in inflate in inflate.c via a large gzip header extra field",
"Description": "zlib through 1.2.12 has a heap-based buffer over-read or buffer overflow in inflate in inflate.c via a large gzip header extra field. NOTE: only applications that call inflateGetHeader are affected. Some common applications bundle the affected zlib source code but may be unable to call inflateGetHeader (e.g., see the nodejs/node reference).",
"Severity": "MEDIUM",
"PackageList": [
{
"Name": "zlib1g",
"InstalledVersion": "1:1.2.11.dfsg-2ubuntu1.3",
"FixedVersion": "Not Specified"
}
]
}
]
}
};
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
describe('Vulnerabilties page', () => {
it('renders the vulnerabilities if there are any', async() => {
// @ts-ignore
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } })
render(<StateVulnerabilitiesWrapper/> );
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
await waitFor(() => expect(screen.getAllByText(/Title/i)).toHaveLength(20));
});
it('renders no vulnerabilities if there are not any', async() => {
// @ts-ignore
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {CVEListForImage: {"Tag": "", "CVEList": []} } } })
render(<StateVulnerabilitiesWrapper/> );
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1));
});
it('should open and close description dropdown for vulnerabilities', async() => {
// @ts-ignore
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } })
render(<StateVulnerabilitiesWrapper/> );
await waitFor(() => expect(screen.getAllByText(/see description/i)).toHaveLength(20));
const openText = screen.getAllByText(/see description/i);
fireEvent.click(openText[0]);
await waitFor(() => expect(screen.getAllByText(/hide description/i)).toHaveLength(1));
await waitFor(() => expect(screen.getAllByText(/see description/i)).toHaveLength(19));
});
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(<StateVulnerabilitiesWrapper/> );
await waitFor(() => expect(error).toBeCalledTimes(1));
})
});

View File

@ -0,0 +1,100 @@
import { render, screen , waitFor } from '@testing-library/react';
import { api } from 'api';
import TagDetails from 'components/TagDetails';
import React from 'react';
const mockImage = {"ExpandedRepoInfo": {
"Images": [
{
"Digest": "7374731e3dd3112d41ece21cf2db5a16f11a51b33bf065e98c767893f50d3dec",
"Tag": "latest",
"Layers": [
{
"Size": "28572596",
"Digest": "3b65ec22a9e96affe680712973e88355927506aa3f792ff03330f3a3eb601a98"
},
{
"Size": "1835",
"Digest": "016bc871e2b33f0e2a37272769ebd6defdb4b702f0d41ec1e685f0366b64e64a"
},
{
"Size": "3059542",
"Digest": "9ddd649edd82d79ffc6f573cd5da7909ae50596b95aca684a571aff6e36aa8cb"
},
{
"Size": "6506025",
"Digest": "39bf776c01e412c9cf35ea7a41f97370c486dee27a2aab228cf2e850a8863e8b"
},
{
"Size": "149",
"Digest": "f7f0405a2fe343547a60a9d4182261ca02d70bb9e47d6cd248f3285d6b41e64c"
},
{
"Size": "1447",
"Digest": "89785d0d9c65afe73fbd9bcb29c451090ca84df0e128cf1ecf5712c036e8c9d2"
},
{
"Size": "261",
"Digest": "fd40d84c80b0302ca13faab8210d8c7082814f6f2ab576b3a61f467d03e1cb0b"
},
{
"Size": "193228772",
"Digest": "d50d65ac4752500ab9f3c24c86b4aa218bea9a0bb0a837ae54ffe2e6d2454f5a"
},
{
"Size": "5067",
"Digest": "255e24cbd370c0055e0d31e063e63c792fa68aff9e25a7ac0a21d39cf6d47573"
}
]
}
],
"Summary": {
"Name": "mongo",
"LastUpdated": "2022-08-02T01:30:49.193203152Z",
"Size": "231383863",
"Platforms": [
{
"Os": "linux",
"Arch": "amd64"
}
],
"Vendors": [
""
],
"NewestImage": null
}
}};
jest.mock("react-router-dom", () => ({
// @ts-ignore
...jest.requireActual("react-router-dom"),
useParams: () =>{return {name:'test'} },
}));
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
describe('Tags details', () => {
it('should show vulnerability tab', async() => {
// @ts-ignore
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } })
render(<TagDetails /> );
await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(1));
});
it('should log an error when data can\'t be fetched', async() => {
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: { } })
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<TagDetails /> );
await waitFor(() => expect(error).toBeCalledTimes(2));
})
it('should show tag details metadata', async() => {
// @ts-ignore
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } })
render(<TagDetails /> );
expect(await screen.findByTestId('tagDetailsMetadata-container')).toBeInTheDocument();
})
});

View File

@ -0,0 +1,26 @@
import { render, screen } from '@testing-library/react';
import { api } from 'api';
import TagPage from 'pages/TagPage';
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
jest.mock("components/TagDetails", () => () => {
return <div/>;
});
it('renders the tags page component', async () => {
render(
<BrowserRouter>
<Routes>
<Route path="*" element={<TagPage updateKeywords={() => {}}/>} />
</Routes>
</BrowserRouter>
);
expect(screen.getByTestId('tag-container')).toBeInTheDocument();
});

View File

@ -61,8 +61,9 @@ const api = {
};
const endpoints = {
imageList: '/v2/_zot/ext/search?query={RepoListWithNewestImage(){ NewestImage {RepoName Tag LastUpdated Description Licenses Vendor Size Labels} }}',
detailedRepoInfo: (name) => `/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Digest Tag Layers {Size Digest}} Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {Tag}}}}`
imageList: '/v2/_zot/ext/search?query={RepoListWithNewestImage(){Platforms {Os Arch} NewestImage {RepoName Tag LastUpdated Description Licenses Vendor Size Labels} }}',
detailedRepoInfo: (name) => `/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Digest Tag Layers {Size Digest}} Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {Tag}}}}`,
vulnerabilitiesForRepo: (name) => `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag, CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`
}
export {api, endpoints};

View File

@ -41,7 +41,7 @@ const useStyles = makeStyles(() => ({
sortForm:{
backgroundColor: '#ffffff',
borderColor: "#E0E0E0",
borderRadius: "6px",
borderRadius: "0.375em",
},
}));
@ -63,6 +63,7 @@ function Explore ({ keywords, data, updateData }) {
latestVersion: image.NewestImage.Tag,
tags: image.NewestImage.Labels,
description: image.NewestImage.Description,
platforms: image.Platforms,
licenses: image.NewestImage.Licenses,
size: image.NewestImage.Size,
vendor: image.NewestImage.Vendor,
@ -105,6 +106,7 @@ function Explore ({ keywords, data, updateData }) {
description={item.description}
tags={item.tags}
vendor={item.vendor}
platforms={item.platforms}
size={item.size}
licenses={item.licenses}
key={index}
@ -145,25 +147,25 @@ function Explore ({ keywords, data, updateData }) {
) : (
<Grid container className={classes.gridWrapper}>
<Grid container item xs={12}>
<Grid item xs={3}>
<Grid item xs={0}>
</Grid>
<Grid item xs={9}>
<Grid item xs={12}>
<Stack direction="row" className={classes.resultsRow}>
<Typography variant="body2" className={classes.results}>Results {filteredData.length}</Typography>
<FormControl sx={{m:'1', minWidth:"4.6875rem"}} className={classes.sortForm} size="small">
{/* <FormControl sx={{m:'1', minWidth:"4.6875rem"}} className={classes.sortForm} size="small">
<InputLabel>Sort</InputLabel>
<Select label="Sort" value={sortFilter} onChange={handleSortChange} MenuProps={{disableScrollLock: true}}>
<MenuItem value='relevance'>Relevance</MenuItem>
</Select>
</FormControl>
</FormControl> */}
</Stack>
</Grid>
</Grid>
<Grid container item xs={12} spacing={5} pt={1}>
<Grid item xs={3}>
{/* <Grid item xs={3}>
{renderFilterCards()}
</Grid>
<Grid item xs={9}>
</Grid> */}
<Grid item xs={12}>
<Stack direction="column" spacing={2}>{renderRepoCards()}</Stack>
</Grid>
</Grid>

View File

@ -3,7 +3,6 @@ import {Link, useLocation, useNavigate} from "react-router-dom";
// components
import {Typography, Breadcrumbs} from '@mui/material';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
// styling
@ -26,7 +25,7 @@ const useStyles = makeStyles((theme) => {
explore: {
color: '#52637A',
fontSize: "1rem",
letterSpacing: "0.009375rem"
letterSpacing: "0.009375rem",
}
}
});
@ -36,13 +35,18 @@ function ExploreHeader() {
const navigate = useNavigate();
const location = useLocation();
const path = location.pathname;
const pathWithoutImage = path.replace('tag/', '');
const pathToBeDisplayed = pathWithoutImage.replace('/image/', '');
const pathHeader = pathToBeDisplayed.replace("/", " / ").replace(/%2F/g,'/');
const pathWithTag = path.substring(0, path.lastIndexOf('/'));
return (
<div className={classes.exploreHeader}>
<ArrowBackIcon sx={{color: "#14191F",fontSize: "2rem", cursor: "pointer"}} onClick={() => navigate(-1)}/>
<Breadcrumbs separator="/" aria-label="breadcrumb">
<Link to="/"><Typography variant="body1" className={classes.explore}>Home</Typography></Link>
{ path.includes('/image/') && <Typography className={classes.explore} variant="body1">{path.replace('/image/', '')}</Typography> }
<Link to={pathWithTag.substring(0, pathWithTag.lastIndexOf('/'))}>{ path.includes('/image/') && <Typography className={classes.explore} variant="body1">{pathHeader}</Typography> }</Link>
</Breadcrumbs>
<div></div>
</div>

View File

@ -77,6 +77,7 @@ function Home({ keywords, data, updateData }) {
latestVersion: image.NewestImage.Tag,
tags: image.NewestImage.Labels,
description: image.NewestImage.Description,
platforms: image.Platforms,
licenses: image.NewestImage.Licenses,
size: image.NewestImage.Size,
vendor: image.NewestImage.Vendor,
@ -137,6 +138,7 @@ function Home({ keywords, data, updateData }) {
description={item.description}
tags={item.tags}
vendor={item.vendor}
platforms={item.platforms}
size={item.size}
licenses={item.licenses}
key={index}
@ -160,6 +162,7 @@ function Home({ keywords, data, updateData }) {
description={item.description}
tags={item.tags}
vendor={item.vendor}
platforms={item.platforms}
size={item.size}
licenses={item.licenses}
key={index}
@ -188,10 +191,10 @@ function Home({ keywords, data, updateData }) {
{renderPreviewCards()}
</Grid> <Grid >
</Grid>
<Typography variant="h4" align="left" className={classes.sectionTitle}>
{/* <Typography variant="h4" align="left" className={classes.sectionTitle}>
Bookmarks
</Typography>
{renderBookmarks()}
{renderBookmarks()} */}
<Stack></Stack>
<Typography variant="h4" align="left" className={classes.sectionTitle}>
Recently updated repositories

View File

@ -67,9 +67,9 @@ const useStyles = makeStyles(() => ({
}));
//function that returns a random element from an array
function getRandom (list) {
return list[Math.floor((Math.random()*list.length))];
}
// function getRandom (list) {
// return list[Math.floor((Math.random()*list.length))];
// }
function PreviewCard(props) {
const classes = useStyles();
@ -77,29 +77,29 @@ function PreviewCard(props) {
const { name } = props;
const goToDetails = (repo) => {
navigate(`/image/${name}`);
navigate(`/image/${encodeURIComponent(name)}`);
};
const vulnerabilityCheck = () => {
const noneVulnerability = <PestControlOutlinedIcon sx={{ color: "#43A047!important", padding:"0.2rem", background: "#E8F5E9", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
const unknownVulnerability = <PestControlOutlinedIcon sx={{ color: "#52637A!important", padding:"0.2rem", background: "#ECEFF1", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
const lowVulnerability = <PestControlOutlinedIcon sx={{ color: "#FB8C00!important", padding:"0.2rem", background: "#FFF3E0", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
const mediumVulnerability = <PestControlIcon sx={{ color: "#FB8C00!important", padding:"0.2rem", background: "#FFF3E0", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
const highVulnerability = <PestControlOutlinedIcon sx={{ color: "#E53935!important", padding:"0.2rem", background: "#FEEBEE", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
const criticalVulnerability = <PestControlIcon sx={{ color: "#E53935!important", padding:"0.2rem", background: "#FEEBEE", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
// const vulnerabilityCheck = () => {
// const noneVulnerability = <PestControlOutlinedIcon sx={{ color: "#43A047!important", padding:"0.2rem", background: "#E8F5E9", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
// const unknownVulnerability = <PestControlOutlinedIcon sx={{ color: "#52637A!important", padding:"0.2rem", background: "#ECEFF1", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
// const lowVulnerability = <PestControlOutlinedIcon sx={{ color: "#FB8C00!important", padding:"0.2rem", background: "#FFF3E0", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
// const mediumVulnerability = <PestControlIcon sx={{ color: "#FB8C00!important", padding:"0.2rem", background: "#FFF3E0", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
// const highVulnerability = <PestControlOutlinedIcon sx={{ color: "#E53935!important", padding:"0.2rem", background: "#FEEBEE", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
// const criticalVulnerability = <PestControlIcon sx={{ color: "#E53935!important", padding:"0.2rem", background: "#FEEBEE", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability]
return(getRandom(arrVulnerability));
}
// const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability]
// return(getRandom(arrVulnerability));
// }
const signatureCheck = () => {
const unverifiedSignature = <GppBadOutlinedIcon sx={{ color: "#E53935!important", padding:"0.2rem", background: "#FEEBEE", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
const untrustedSignature = <GppMaybeOutlinedIcon sx={{ color: "#52637A!important", padding:"0.2rem", background: "#ECEFF1", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
const verifiedSignature = <GppGoodOutlinedIcon sx={{ color: "#43A047!important", padding:"0.2rem", background: "#E8F5E9", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
// const signatureCheck = () => {
// const unverifiedSignature = <GppBadOutlinedIcon sx={{ color: "#E53935!important", padding:"0.2rem", background: "#FEEBEE", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
// const untrustedSignature = <GppMaybeOutlinedIcon sx={{ color: "#52637A!important", padding:"0.2rem", background: "#ECEFF1", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
// const verifiedSignature = <GppGoodOutlinedIcon sx={{ color: "#43A047!important", padding:"0.2rem", background: "#E8F5E9", borderRadius: "1rem", height:"1.5rem", width:"1.6rem" }} />;
const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature]
return(getRandom(arrSignature));
}
// const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature]
// return(getRandom(arrSignature));
// }
return (
<Card variant="outlined" className={classes.card}>
@ -107,7 +107,7 @@ function PreviewCard(props) {
<CardContent className={classes.content}>
<Grid container spacing={1}>
<Grid container item xs={12}>
<Stack direction="row" spacing={3} sx={{display:"flex",alignItems:"center", flexWrap:"wrap"}}>
<Stack direction="row" spacing={3} sx={{display:"flex",alignItems:"center", flexWrap:"wrap", }}>
<CardMedia classes={{
root: classes.media,
img: classes.avatar,
@ -116,17 +116,17 @@ function PreviewCard(props) {
image={randomImage()}
alt="icon"
/>
<Typography variant="h5" component="div" sx={{size:"1.5rem", lineHeight:"2rem", color:"#220052"}}>
<Typography variant="h5" component="div" sx={{size:"1.5rem", lineHeight:"2rem", color:"#220052", width:"10rem",whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}}>
{name}
</Typography>
{vulnerabilityCheck()}
{signatureCheck()}
{/* {vulnerabilityCheck()}
{signatureCheck()} */}
</Stack>
</Grid>
<Grid item xs={12} mt={2}>
<Stack alignItems="flex-end" justifyContent="space-between" direction="row">
<Typography variant="body2" sx={{fontSize:"0.875rem", lineHeight:"143%", letterSpacing:"0.010625rem"}}>Official</Typography>
<BookmarkBorderOutlinedIcon/>
{/* <BookmarkBorderOutlinedIcon/> */}
</Stack>
</Grid>

View File

@ -93,62 +93,69 @@ const useStyles = makeStyles(() => ({
function RepoCard(props) {
const classes = useStyles();
const navigate = useNavigate();
const { name, vendor, description, lastUpdated, downloads, rating, version } =
const { name, vendor, platforms, description, lastUpdated, downloads, rating, version } =
props;
//function that returns a random element from an array
function getRandom(list) {
return list[Math.floor(Math.random() * list.length)];
}
// function getRandom(list) {
// return list[Math.floor(Math.random() * list.length)];
// }
const goToDetails = (repo) => {
navigate(`/image/${name}`);
const goToDetails = () => {
navigate(`/image/${encodeURIComponent(name)}`);
};
const vulnerabilityCheck = () => {
const noneVulnerability = <Chip label="None Vulnerability" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
const unknownVulnerability = <Chip label="Unknown Vulnerability" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
const lowVulnerability = <Chip label="Low Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#FB8C00!important" }} />}/>;
const mediumVulnerability = <Chip label="Medium Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#FB8C00!important" }} />}/>;
const highVulnerability = <Chip label="High Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
const criticalVulnerability = <Chip label="Critical Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#E53935!important" }} />}/>;
// const vulnerabilityCheck = () => {
// const noneVulnerability = <Chip label="None Vulnerability" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
// const unknownVulnerability = <Chip label="Unknown Vulnerability" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
// const lowVulnerability = <Chip label="Low Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const mediumVulnerability = <Chip label="Medium Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const highVulnerability = <Chip label="High Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const criticalVulnerability = <Chip label="Critical Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#E53935!important" }} />}/>;
const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability]
return(getRandom(arrVulnerability));
};
// const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability]
// return(getRandom(arrVulnerability));
// };
const signatureCheck = () => {
const unverifiedSignature = <Chip label="Unverified Signature" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppBadOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
const untrustedSignature = <Chip label="Untrusted Signature" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppMaybeOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
const verifiedSignature = <Chip label="Verified Signature" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppGoodOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
// const signatureCheck = () => {
// const unverifiedSignature = <Chip label="Unverified Signature" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppBadOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const untrustedSignature = <Chip label="Untrusted Signature" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppMaybeOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
// const verifiedSignature = <Chip label="Verified Signature" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppGoodOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature]
return(getRandom(arrSignature));
}
// const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature]
// return(getRandom(arrSignature));
// }
const platformChips = () => {
// if platforms not received, mock data
const platforms = props.platforms || [
"Windows",
"PowerPC64LE",
"IBM Z",
"Linux",
];
return platforms.map((platform, index) => (
<Chip
key={index}
label={platform}
sx={{
backgroundColor: "#E0E5EB",
color: "#52637A",
fontSize: "0.8125rem",
}}
/>
const platformsOsArch = platforms || [];
return platformsOsArch.map((platform,index) => (
<Stack key={`stack${platform?.Os}${platform?.Arch}`} alignItems="center" direction="row" spacing={2}>
<Chip
key={`${name}${platform?.Os}${index}`}
label={platform?.Os}
sx={{
backgroundColor: "#E0E5EB",
color: "#52637A",
fontSize: "0.8125rem",
}}
/>
<Chip
key={`${name}${platform?.Arch}${index}`}
label={platform?.Arch}
sx={{
backgroundColor: "#E0E5EB",
color: "#52637A",
fontSize: "0.8125rem",
}}
/>
</Stack>
));
};
const getVendor = () => {
return `${vendor || "andrewc"}`;
return `${vendor || "N/A"}`;
};
const getVersion = () => {
const lastDate = lastUpdated
@ -182,8 +189,8 @@ function RepoCard(props) {
<Typography variant="h5" component="div">
{name}
</Typography>
{vulnerabilityCheck()}
{signatureCheck()}
{/* {vulnerabilityCheck()}
{signatureCheck()} */}
{/* <Chip label="Verified licensee" sx={{ backgroundColor: "#E8F5E9", color: "#388E3C" }} variant="filled" onDelete={() => { return }} deleteIcon={vulnerabilityCheck()} /> */}
</Stack>
<Typography
@ -193,7 +200,7 @@ function RepoCard(props) {
gutterBottom
>
{description ||
"The complete solution for node.js command-line programs"}
"N/A"}
</Typography>
<Stack alignItems="center" direction="row" spacing={2} pt={1}>
{platformChips()}
@ -218,14 +225,14 @@ function RepoCard(props) {
className={classes.contentRight}
>
<Stack direction="column" alignItems="flex-end">
<Typography variant="body2">
{/* <Typography variant="body2">
Downloads {downloads || "-"}
</Typography>
<Typography variant="body2">
Rating {rating || "-"}
</Typography>
</Typography> */}
</Stack>
<BookmarkIcon sx={{color:"#52637A"}}/>
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
</Stack>
</Grid>
</Grid>

View File

@ -82,7 +82,7 @@ const useStyles = makeStyles((theme) => ({
flexDirection:"row",
alignItems:"start",
background:"#FFFFFF",
border: "1px solid #E0E5EB",
border: "0.0625rem solid #E0E5EB",
borderRadius:"2rem",
flex:"none",
alignSelf:"stretch",
@ -101,7 +101,7 @@ const useStyles = makeStyles((theme) => ({
},
inputForm:{
'& fieldset':{
border: "2px solid #52637A",
border: "0.125rem solid #52637A",
},
},
@ -142,12 +142,14 @@ function RepoDetails (props) {
let repoInfo = response.data.data.ExpandedRepoInfo;
let imageData = {
name: name,
tags: repoInfo.Manifests,
images: repoInfo.Images,
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?.NewestTag
newestTag: repoInfo.Summary?.NewestImage
}
setRepoDetailData(imageData);
setIsLoading(false);
@ -159,41 +161,57 @@ function RepoDetails (props) {
});
}, [name])
//function that returns a random element from an array
function getRandom(list) {
return list[Math.floor(Math.random() * list.length)];
}
// function getRandom(list) {
// return list[Math.floor(Math.random() * list.length)];
// }
const vulnerabilityCheck = () => {
const noneVulnerability = <Chip label="None Vulnerability" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
const unknownVulnerability = <Chip label="Unknown Vulnerability" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
const lowVulnerability = <Chip label="Low Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#FB8C00!important" }} />}/>;
const mediumVulnerability = <Chip label="Medium Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#FB8C00!important" }} />}/>;
const highVulnerability = <Chip label="High Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
const criticalVulnerability = <Chip label="Critical Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#E53935!important" }} />}/>;
// const vulnerabilityCheck = () => {
// const noneVulnerability = <Chip label="None Vulnerability" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
// const unknownVulnerability = <Chip label="Unknown Vulnerability" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
// const lowVulnerability = <Chip label="Low Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const mediumVulnerability = <Chip label="Medium Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const highVulnerability = <Chip label="High Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const criticalVulnerability = <Chip label="Critical Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#E53935!important" }} />}/>;
const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability]
return(getRandom(arrVulnerability));
};
// const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability]
// return(getRandom(arrVulnerability));
// };
const signatureCheck = () => {
const unverifiedSignature = <Chip label="Unverified Signature" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppBadOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
const untrustedSignature = <Chip label="Untrusted Signature" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppMaybeOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
const verifiedSignature = <Chip label="Verified Signature" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppGoodOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
// const signatureCheck = () => {
// const unverifiedSignature = <Chip label="Unverified Signature" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppBadOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const untrustedSignature = <Chip label="Untrusted Signature" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppMaybeOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
// const verifiedSignature = <Chip label="Verified Signature" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppGoodOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature]
return(getRandom(arrSignature));
}
// const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature]
// return(getRandom(arrSignature));
// }
const platformChips = () => {
// if platforms not received, mock data
// @ts-ignore
const platforms = repoDetailData.platforms || ["Windows","PowerPC64LE","IBM Z","Linux"];
const platforms = repoDetailData?.platforms || [];
return platforms.map((platform, index) => (
<Chip key={index} label={platform.Os} sx={{
backgroundColor: "#E0E5EB",
color: "#52637A",
fontSize: "0.8125rem",
}}/>
<Stack key={`stack${platform?.Os}${platform?.Arch}`} alignItems="center" direction="row" spacing={2}>
<Chip
key={`${name}${platform?.Os}${index}`}
label={platform?.Os}
sx={{
backgroundColor: "#E0E5EB",
color: "#52637A",
fontSize: "0.8125rem",
}}
/>
<Chip
key={`${name}${platform?.Arch}${index}`}
label={platform?.Arch}
sx={{
backgroundColor: "#E0E5EB",
color: "#52637A",
fontSize: "0.8125rem",
}}
/>
</Stack>
));
}
@ -207,7 +225,7 @@ function RepoDetails (props) {
<Card className={classes.card} data-testid='overview-container'>
<CardContent>
<Typography variant="h4" align="left">{overviewTitle || 'Quickstart'}</Typography>
<Typography variant="body1" sx={{color:"rgba(0, 0, 0, 0.6)", fontSize:"1rem",lineHeight:"150%", marginTop:"5%", alignSelf:"stretch"}}>{description || mockData.loremIpsum}</Typography>
<Typography variant="body1" sx={{color:"rgba(0, 0, 0, 0.6)", fontSize:"1rem",lineHeight:"150%", marginTop:"5%", alignSelf:"stretch"}}>{description || "N/A"}</Typography>
</CardContent>
</Card>
);
@ -256,12 +274,12 @@ function RepoDetails (props) {
<Typography variant="h3" className={classes.repoName}>
{name}
</Typography>
{vulnerabilityCheck()}
{signatureCheck()}
<BookmarkIcon sx={{color:"#52637A"}}/>
{/* {vulnerabilityCheck()}
{signatureCheck()} */}
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
</Stack>
<Typography pt={1} sx={{ fontSize: 16,lineHeight:"1.5rem", color:"rgba(0, 0, 0, 0.6)", paddingLeft:"4rem"}} gutterBottom align="left">
{description || 'The complete solution for node.js command-line programs'}
{description || 'N/A'}
</Typography>
<Stack alignItems="center" sx={{ paddingLeft:"4rem"}} direction="row" spacing={2} pt={1}>
{platformChips()}
@ -271,9 +289,10 @@ function RepoDetails (props) {
<Typography variant="body1" sx={{color:"#52637A", fontSize: "1rem"}}>Copy and pull to pull this image</Typography>
<FormControl sx={{ m: 1, paddingLeft:"1.5rem"}} variant="outlined">
<OutlinedInput
value={`Pull ${name}`}
// value={`Pull ${name}`}
value= 'N/A'
className={classes.inputForm}
sx={{ m: 1, width: '20.625rem', borderRadius: "8px", color: "#14191F"}}
sx={{ m: 1, width: '20.625rem', borderRadius: "0.5rem", color: "#14191F"}}
endAdornment={
<InputAdornment position="end" >
<IconButton aria-label='copy' edge="end" onClick={() => navigator.clipboard.writeText(`Pull ${name}`)} data-testid='pullcopy-btn'>
@ -332,11 +351,11 @@ function RepoDetails (props) {
<Grid item xs={4} className={classes.metadata}>
<RepoDetailsMetadata
// @ts-ignore
lastUpdated={repoDetailData.lastUpdated}
lastUpdated={repoDetailData?.lastUpdated}
// @ts-ignore
size={repoDetailData.size}
size={repoDetailData?.size}
// @ts-ignore
latestTag={repoDetailData.newestTag}
latestTag={repoDetailData?.newestTag}
/>
</Grid>
</Grid>

View File

@ -43,7 +43,7 @@ function RepoDetailsMetadata (props) {
<Card variant="outlined" className={classes.card}>
<CardContent>
<Typography variant="body2" align="left" className={classes.metadataHeader}>Repository</Typography>
<Typography variant="body1" className={classes.metadataBody}>{repoURL || `----`}</Typography>
<Typography variant="body1" align="left" className={classes.metadataBody}>{repoURL || `N/A`}</Typography>
</CardContent>
</Card>
</Grid>
@ -51,7 +51,7 @@ function RepoDetailsMetadata (props) {
<Card variant="outlined" className={classes.card}>
<CardContent>
<Typography variant="body2" align="left" className={classes.metadataHeader}>Weekly downloads</Typography>
<Typography variant="body1" align="left" className={classes.metadataBody}>{weeklyDownloads || `----`}</Typography>
<Typography variant="body1" align="left" className={classes.metadataBody}>{weeklyDownloads || `N/A`}</Typography>
</CardContent>
</Card>
</Grid>
@ -60,7 +60,7 @@ function RepoDetailsMetadata (props) {
<Card variant="outlined" className={classes.card}>
<CardContent>
<Typography variant="body2" align="left" className={classes.metadataHeader}>Last publish</Typography>
<Typography variant="body1" className={classes.metadataBody}>{lastDate || `35 days ago`}</Typography>
<Typography variant="body1" align="left" className={classes.metadataBody}>{lastDate || `35 days ago`}</Typography>
</CardContent>
</Card>
</Grid>
@ -68,12 +68,12 @@ function RepoDetailsMetadata (props) {
<Card variant="outlined" className={classes.card}>
<CardContent>
<Typography variant="body2" align="left" className={classes.metadataHeader}>Total size</Typography>
<Typography variant="body1" className={classes.metadataBody}>{transform.formatBytes(size) || `----`}</Typography>
<Typography variant="body1" align="left" className={classes.metadataBody}>{transform.formatBytes(size) || `----`}</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
<Grid container item xs={12} spacing={2}>
{/* <Grid container item xs={12} spacing={2}>
<Grid item xs={12}>
<Card variant="outlined" className={classes.card}>
<CardContent>
@ -82,7 +82,7 @@ function RepoDetailsMetadata (props) {
</CardContent>
</Card>
</Grid>
</Grid>
</Grid> */}
</Grid>
)
}

View File

@ -28,7 +28,7 @@ const useStyles = makeStyles((theme) => ({
alignItems: "center",
justifyContent: "center",
margin: "0 auto",
padding: "10px",
padding: "0.625rem",
position: "relative",
},
loginCard: {
@ -42,7 +42,8 @@ const useStyles = makeStyles((theme) => ({
background: "#FFFFFF",
gap: "0.625em",
boxShadow: "0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)",
borderRadius: "1.5rem",
borderRadius: "1.5rem",
minWidth: "30rem"
},
loginCardContent: {
alignItems: "center",
@ -65,33 +66,33 @@ const useStyles = makeStyles((theme) => ({
},
textField: {
borderRadius: "4px",
borderRadius: "0.25rem",
},
button: {
textTransform: "none",
color: "##FFFFFF",
fontSize: "1.4375rem",
fontWeight: "500",
height: "50px",
borderRadius: "4px",
height: "3.125rem",
borderRadius: "0.25rem",
letterSpacing:"0.01rem",
},
gitLogo: {
height: "24px",
borderRadius: "4px",
borderRadius: "0.25rem",
paddingLeft: "1rem",
},
line: {
width: "100%",
textAlign: "center",
borderBottom: "1px solid #C2CBD6",
borderBottom: "0.0625rem solid #C2CBD6",
lineHeight: "0.1rem",
margin: "10px 0 20px",
margin: "0.625rem 0 1.25rem",
},
lineSpan:{
background: "#ffffff",
color: "#C2CBD6",
padding: "0 10px",
padding: "0 0.625rem",
fontSize: "1rem",
fontWeight: "400",
paddingLeft: "1rem",

View File

@ -0,0 +1,307 @@
// react global
import { useParams } from 'react-router-dom'
import React, { useEffect, useState } from 'react';
// utility
import {api, endpoints} from '../api';
import mockData from '../utilities/mockData';
// components
import {Box, Card, CardContent, CardMedia, Chip, FormControl, Grid, IconButton, InputAdornment, OutlinedInput, Stack, Tab, Typography} from '@mui/material';
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 GppBadOutlinedIcon from "@mui/icons-material/GppBadOutlined";
import GppGoodOutlinedIcon from "@mui/icons-material/GppGoodOutlined";
import GppMaybeOutlinedIcon from "@mui/icons-material/GppMaybeOutlined";
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import BookmarkIcon from '@mui/icons-material/Bookmark';
// placeholder images
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 TagDetailsMetadata from './TagDetailsMetadata';
import VulnerabilitiesDetails from './VulnerabilitiesDetails';
import { padding } from '@mui/system';
// @ts-ignore
const useStyles = makeStyles((theme) => ({
pageWrapper: {
backgroundColor: "#FFFFFF",
height: '100vh',
},
container: {
paddingTop: 5,
paddingBottom: 5,
marginTop: 100,
backgroundColor: "#FFFFFF",
},
repoName: {
fontWeight:"700",
fontSize:"2.5rem",
color:"#0F2139"
},
avatar: {
height:"3rem",
width:"3rem"
},
cardBtn: {
height: "100%",
width: "100%"
},
media: {
borderRadius: '3.125em',
},
tabs: {
marginTop: "3rem",
padding:"0.5rem",
height: "100%"
},
tabContent:{
height:"100%"
},
selectedTab: {
background:"#D83C0E",
borderRadius:"1.5rem"
},
tabPanel: {
height:"100%",
paddingLeft: "0rem!important"
},
metadata: {
marginTop: "8rem",
paddingLeft:"1.5rem",
},
card: {
marginBottom: 2,
display:"flex",
flexDirection:"row",
alignItems:"start",
background:"#FFFFFF",
border: "0.0625rem solid #E0E5EB",
borderRadius:"2rem",
flex:"none",
alignSelf:"stretch",
flexGrow:0,
order:0,
width:"100%",
boxShadow: "none!important"
},
platformText:{
backgroundColor:"#EDE7F6",
color: "#220052",
fontWeight:'400',
fontSize:'0.8125rem',
lineHeight:'1.125rem',
letterSpacing:'0.01rem'
},
inputForm:{
'& fieldset':{
border: "0.125rem solid #52637A",
},
},
cardRoot:{
boxShadow: "none!important",
},
header:{
paddingLeft:"2rem"
}
}));
// temporary utility to get image
const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min)
};
const randomImage = () => {
const imageArray = [repocube1,repocube2,repocube3,repocube4];
return imageArray[randomIntFromInterval(0,3)];
};
function TagDetails (props) {
const [repoDetailData, setRepoDetailData] = useState({});
// @ts-ignore
const [isLoading, setIsLoading] = useState(false);
const [selectedTab, setSelectedTab] = useState("Vulnerabilities");
// get url param from <Route here (i.e. image name)
const {name} = useParams();
const classes = useStyles();
const {description, overviewTitle, dependencies, dependents} = props;
useEffect(() => {
api.get(`${host()}${endpoints.detailedRepoInfo(name)}`)
.then(response => {
if (response.data && response.data.data) {
let repoInfo = response.data.data.ExpandedRepoInfo;
let imageData = {
name: name,
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
}
setRepoDetailData(imageData);
setIsLoading(false);
}
})
.catch((e) => {
console.error(e);
setRepoDetailData({});
});
}, [name])
//function that returns a random element from an array
// function getRandom(list) {
// return list[Math.floor(Math.random() * list.length)];
// }
// const vulnerabilityCheck = () => {
// const noneVulnerability = <Chip label="No Vulnerability" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
// const unknownVulnerability = <Chip label="Unknown Vulnerability" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
// const lowVulnerability = <Chip label="Low Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const mediumVulnerability = <Chip label="Medium Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const highVulnerability = <Chip label="High Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const criticalVulnerability = <Chip label="Critical Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#E53935!important" }} />}/>;
// const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability]
// return(getRandom(arrVulnerability));
// };
// const signatureCheck = () => {
// const unverifiedSignature = <Chip label="Unverified Signature" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppBadOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const untrustedSignature = <Chip label="Untrusted Signature" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppMaybeOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
// const verifiedSignature = <Chip label="Verified Signature" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppGoodOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
// const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature]
// return(getRandom(arrSignature));
// }
const getPlatform = () => {
// @ts-ignore
return repoDetailData?.platforms? repoDetailData.platforms[0] : '--/--';
}
// @ts-ignore
const handleTabChange = (event, newValue) => {
setSelectedTab(newValue);
};
//will need this but not for now
// const renderDependencies = () => {
// return (<Card className={classes.card}>
// <CardContent>
// <Typography variant="h4" align="left">Dependecies ({dependencies || '---'})</Typography>
// </CardContent>
// </Card>);
// };
// const renderDependents = () => {
// return (<Card className={classes.card}>
// <CardContent>
// <Typography variant="h4" align="left">Dependents ({dependents || '---'})</Typography>
// </CardContent>
// </Card>);
// };
// const renderVulnerabilities = () => {
// return (<Card className={classes.card}>
// <CardContent>
// <Typography variant="h4" align="left">Vulnerabilities</Typography>
// </CardContent>
// </Card>);
// };
return (
<div className={classes.pageWrapper}>
<Card className={classes.cardRoot}>
<CardContent>
<Grid container className={classes.header}>
<Grid item xs={8}>
<Stack alignItems="center" direction="row" spacing={2}>
<CardMedia classes={{
root: classes.media,
img: classes.avatar,
}}
component="img"
image={randomImage()}
alt="icon"
/>
<Typography variant="h3" className={classes.repoName}>
{name}:{repoDetailData?.
// @ts-ignore
tags}
</Typography>
{/* {vulnerabilityCheck()}
{signatureCheck()} */}
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
</Stack>
<Typography pt={1} sx={{ fontSize: 16,lineHeight:"1.5rem", color:"rgba(0, 0, 0, 0.6)", paddingLeft:"4rem"}} gutterBottom align="left">
Digest: {repoDetailData?.
// @ts-ignore
latestDigest}
</Typography>
</Grid>
</Grid>
<Grid container>
<Grid item xs={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="Layers" label="Layers" className={classes.tabContent}/>
<Tab value="DependsOn" label="Depends on" className={classes.tabContent}/>
<Tab value="IsDependentOn" label="Is Dependent On" className={classes.tabContent}/> */}
<Tab value="Vulnerabilities" label="Vulnerabilities" className={classes.tabContent}/>
</TabList>
<Grid container>
<Grid item xs={12}>
{/* <TabPanel value="Layers" className={classes.tabPanel}>
<Typography> Layers </Typography>
</TabPanel>
<TabPanel value="DependsOn" className={classes.tabPanel}>
<Typography> Depends On </Typography>
</TabPanel>
<TabPanel value="IsDependentOn" className={classes.tabPanel}>
<Typography> Is Dependent On </Typography>
</TabPanel> */}
<TabPanel value="Vulnerabilities" className={classes.tabPanel}>
<VulnerabilitiesDetails name={name}/>
</TabPanel>
</Grid>
</Grid>
</Box>
</TabContext>
</Grid>
<Grid item xs={4} className={classes.metadata}>
<TagDetailsMetadata
// @ts-ignore
platforms={getPlatform()}
// @ts-ignore
size={repoDetailData?.size}
// @ts-ignore
lastUpdated={repoDetailData?.lastUpdated}
/>
</Grid>
</Grid>
</CardContent>
</Card>
</div>
);
}
export default TagDetails;

View File

@ -0,0 +1,72 @@
import { Card, CardContent, Grid, Typography } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { DateTime } from 'luxon';
import React from 'react';
import transform from '../utilities/transform';
const useStyles = makeStyles(() => ({
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",
flex:"none",
alignSelf:"stretch",
flexGrow:0,
order:0,
width:"100%"
},
metadataHeader: {
color: "rgba(0, 0, 0, 0.6)"
},
metadataBody: {
color: "rgba(0, 0, 0, 0.87)",
fontFamily: 'Roboto',
fontStyle: "normal",
fontWeight: 400,
fontSize: "1rem",
lineHeight: "150%",
align:"left"
}
}));
function TagDetailsMetadata (props) {
const classes = useStyles();
const {platforms, lastUpdated, size} = props;
const lastDate = (lastUpdated ? DateTime.fromISO(lastUpdated) : DateTime.now().minus({ days: 1 })).toRelative({ unit: 'days' })
return (
<Grid container spacing={1} data-testid='tagDetailsMetadata-container'>
<Grid container item xs={12}>
<Card variant="outlined" className={classes.card}>
<CardContent>
<Typography variant="body2" align="left" className={classes.metadataHeader}>OS/Arch</Typography>
<Typography variant="body1" className={classes.metadataBody}>{platforms.Os || `----`} / {platforms.Arch || `----`}</Typography>
</CardContent>
</Card>
</Grid>
<Grid container item xs={12}>
<Card variant="outlined" className={classes.card}>
<CardContent>
<Typography variant="body2" align="left" className={classes.metadataHeader}>Total Size</Typography>
<Typography variant="body1" align="left" className={classes.metadataBody}>{transform.formatBytes(size) || `----`}</Typography>
</CardContent>
</Card>
</Grid>
<Grid container item xs={12} spacing={2}>
<Grid item xs={12}>
<Card variant="outlined" className={classes.card}>
<CardContent>
<Typography variant="body2" align="left" className={classes.metadataHeader}>Last Published</Typography>
<Typography variant="body1" align="left" className={classes.metadataBody}>{lastDate || `----`}</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
</Grid>
)
}
export default TagDetailsMetadata;

View File

@ -1,78 +1,117 @@
// react global
import * as React from 'react';
import { useNavigate } from "react-router-dom";
import PropTypes from 'prop-types';
import { DateTime } from 'luxon';
// components
import Box from '@mui/material/Box';
import Collapse from '@mui/material/Collapse';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell, {tableCellClasses} from '@mui/material/TableCell';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import transform from 'utilities/transform';
import { Card, CardContent, Divider } from '@mui/material';
import { Card, CardContent, Divider, Stack } from '@mui/material';
import { makeStyles } from '@mui/styles';
import { useEffect } from 'react';
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%"
},
card: {
marginBottom: 2,
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%"
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",
padding: "2% 3% 2% 3%",
width:"100%"
width: "100%"
}
}));
function TagCard(props) {
const {data, row} = props;
const tags = data && data.tags;
const { row, lastUpdated, vendors, size, platform } = props;
//const tags = data && data.tags;
const [open, setOpen] = React.useState(false);
const [digests, setDigests] = React.useState([]);
const classes = useStyles();
const tagRow = row;
const lastDate = (lastUpdated ? DateTime.fromISO(lastUpdated) : DateTime.now().minus({ days: 1 })).toRelative({ unit: 'days' })
const navigate = useNavigate();
useEffect(() => {
const tagDigest = [{ digest: tagRow.Digest, osArch: platform[0], size: size }];
setDigests(tagDigest);
}, []);
const goToTags = (tag) => {
navigate(`tag/${tag}`);
};
return (
<Card className={classes.card} raised>
<CardContent className={classes.content}>
<Typography variant="body1" align="left" sx={{color:"#828282"}}>{row.Tag}</Typography>
<Typography variant="caption">Last pushed {row.lastUpdated || '----'} by {row.vendor || '----'}</Typography>
<Typography sx={{color:"#7C4DFF", cursor:'pointer'}} onClick={() => setOpen(!open)}>{!open? 'See layers' : 'Hide layers'}</Typography>
<Typography variant="body1" align="left" sx={{ color: "#828282", fontSize: "1rem", paddingBottom: "0.5rem" }}>Tag</Typography>
<Typography variant="body1" align="left" sx={{ color: "#1479FF", fontSize: "1rem", textDecorationLine: "underline", cursor: 'pointer' }} onClick={() => goToTags(tagRow.Tag)}>{tagRow.Tag}</Typography>
<Stack sx={{ display: "inline" }} direction="row" spacing={0.5}>
<Typography variant="caption" sx={{ fontWeight: "400", fontSize: "0.8125rem" }} >
Last pushed
</Typography>
<Typography variant="caption" sx={{ fontWeight: "600", fontSize: "0.8125rem" }} >
{lastDate || '----'} by {vendors[0] || 'N/A'}
</Typography>
</Stack>
<Typography sx={{ color: "#1479FF", paddingTop: "1rem", fontSize: "0.8125rem", fontWeight: "600", cursor: 'pointer' }} onClick={() => setOpen(!open)}>{!open ? 'See digests' : 'Hide digests'}</Typography>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box>
<Typography variant="h6" gutterBottom component="div">
{
// Layers
}
</Typography>
<Table size="small" padding="none" sx={{[`& .${tableCellClasses.root}`]: {borderBottom: "none"}}}>
<Table size="small" padding="none" sx={{ [`& .${tableCellClasses.root}`]: { borderBottom: "none" } }}>
<TableHead>
<TableRow>
<TableCell style={{color: "#696969"}}><Typography variant="body1">Digest</Typography></TableCell>
<TableCell style={{color: "#696969"}}><Typography variant="body1">OS/ARCH</Typography></TableCell>
<TableCell style={{color: "#696969"}}><Typography variant="body1">Size</Typography></TableCell>
<TableCell style={{ color: "#696969" }}><Typography variant="body1">Digest</Typography></TableCell>
<TableCell style={{ color: "#696969" }}><Typography variant="body1">OS/ARCH</Typography></TableCell>
<TableCell style={{ color: "#696969" }}><Typography variant="body1">Size</Typography></TableCell>
</TableRow>
</TableHead>
<TableBody>
{row.Layers.map((layer) => (
<TableRow key={layer.Digest} onClick={() => {navigator.clipboard.writeText(layer.Digest)}}>
<TableCell style={{color: "#696969"}}><Typography variant="body1">{layer.Digest?.substr(0,12)}</Typography></TableCell>
<TableCell style={{color: "#696969"}}><Typography variant="body1">-----------</Typography></TableCell>
<TableCell component="th" scope="row" style={{color: "#696969"}}>
<Typography variant="body1">{transform.formatBytes(layer.Size)}</Typography>
{digests.map((layer) => (
<TableRow key={layer.digest} onClick={() => { navigator.clipboard.writeText(layer.digest) }}>
<TableCell style={{ color: "#696969" }}><Typography variant="body1">{layer.digest?.substr(0, 12)}</Typography></TableCell>
<TableCell style={{ color: "#696969" }}><Typography variant="body1"> {layer.osArch?.Os}/{layer.osArch?.Arch} </Typography></TableCell>
<TableCell component="th" scope="row" style={{ color: "#696969" }}>
<Typography variant="body1">{transform.formatBytes(layer.size)}</Typography>
</TableCell>
</TableRow>
))}
@ -85,24 +124,24 @@ function TagCard(props) {
);
}
TagCard.propTypes = {
row: PropTypes.shape({
Layers: PropTypes.arrayOf(
PropTypes.shape({
Digest: PropTypes.string.isRequired,
Size: PropTypes.string.isRequired,
}),
).isRequired,
Tag: PropTypes.string.isRequired,
}).isRequired,
};
// TagCard.propTypes = {
// row: PropTypes.shape({
// Layers: PropTypes.arrayOf(
// PropTypes.shape({
// Digest: PropTypes.string.isRequired,
// Size: PropTypes.string.isRequired,
// }),
// ).isRequired,
// Tag: PropTypes.string.isRequired,
// }).isRequired,
// };
const renderTags = (tags) => {
const renderTags = (tags, lastUpdated, vendors, size, platform) => {
const cmp = tags && tags.map((tag, index) => {
return (
<TagCard key={tag.Tag} row={tag} />
);
return (
<TagCard key={tag.Tag} row={tag} lastUpdated={lastUpdated} vendors={vendors} size={size} platform={platform} />
);
});
return cmp;
}
@ -110,15 +149,16 @@ const renderTags = (tags) => {
export default function Tags(props) {
const classes = useStyles();
const {data} = props;
const {tags} = data;
const { data } = props;
const { images, lastUpdated, vendors, size, platforms } = data;
return (
<Card className={classes.card} data-testid='tags-container'>
<Card className={classes.tagCard} data-testid='tags-container'>
<CardContent className={classes.content}>
<Typography variant="h4" gutterBottom component="div" align="left" style={{color: "rgba(0, 0, 0, 0.87)"}}>Tags history</Typography>
<Divider variant="fullWidth" sx={{margin:"5% 0% 5% 0%", background:"rgba(0, 0, 0, 0.38)", height:"0.0625rem", width:"100%"}}/>
{renderTags(tags)}
<Typography variant="h4" gutterBottom component="div" align="left" style={{ color: "rgba(0, 0, 0, 0.87)", fontSize: "1.5rem", fontWeight: "600" }}>Tags History</Typography>
<Divider variant="fullWidth" sx={{ margin: "5% 0% 5% 0%", background: "rgba(0, 0, 0, 0.38)", height: "0.00625rem", width: "100%" }} />
{renderTags(images, lastUpdated, vendors, size, platforms)}
</CardContent>
</Card>
);

View File

@ -0,0 +1,175 @@
import { useParams } from 'react-router-dom'
import React, { useEffect, useState } from 'react';
// utility
import { api, endpoints } from '../api';
import mockData from '../utilities/mockData';
// components
import Collapse from '@mui/material/Collapse';
import { Box, Card, CardContent, Divider, Chip, FormControl, Grid, IconButton, InputAdornment, OutlinedInput, Stack, Tab, Typography } from '@mui/material';
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 GppBadOutlinedIcon from "@mui/icons-material/GppBadOutlined";
import GppGoodOutlinedIcon from "@mui/icons-material/GppGoodOutlined";
import GppMaybeOutlinedIcon from "@mui/icons-material/GppMaybeOutlined";
const useStyles = makeStyles((theme) => ({
card: {
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%",
marginTop: "2rem",
marginBottom: "2rem"
},
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"
}
}));
const vulnerabilityCheck = (status) => {
const noneVulnerability = <Chip label="None" sx={{ backgroundColor: "#E8F5E9", color: "#388E3C", fontSize: "0.8125rem", }} variant="filled" onDelete={() => { return; }} deleteIcon={<PestControlOutlinedIcon sx={{ color: "#388E3C!important" }} />} />;
const unknownVulnerability = <Chip label="Unknown" sx={{ backgroundColor: "#ECEFF1", color: "#52637A", fontSize: "0.8125rem", }} variant="filled" onDelete={() => { return; }} deleteIcon={<PestControlOutlinedIcon sx={{ color: "#52637A!important" }} />} />;
const lowVulnerability = <Chip label="Low" sx={{ backgroundColor: "#FFF3E0", color: "#FB8C00", fontSize: "0.8125rem", }} variant="filled" onDelete={() => { return; }} deleteIcon={<PestControlOutlinedIcon sx={{ color: "#FB8C00!important" }} />} />;
const mediumVulnerability = <Chip label="Medium" sx={{ backgroundColor: "#FFF3E0", color: "#FB8C00", fontSize: "0.8125rem", }} variant="filled" onDelete={() => { return; }} deleteIcon={<PestControlIcon sx={{ color: "#FB8C00!important" }} />} />;
const highVulnerability = <Chip label="High" sx={{ backgroundColor: "#FEEBEE", color: "#E53935", fontSize: "0.8125rem", }} variant="filled" onDelete={() => { return; }} deleteIcon={<PestControlOutlinedIcon sx={{ color: "#E53935!important" }} />} />;
const criticalVulnerability = <Chip label="Critical" sx={{ backgroundColor: "#FEEBEE", color: "#E53935", fontSize: "0.8125rem", }} variant="filled" onDelete={() => { return; }} deleteIcon={<PestControlIcon sx={{ color: "#E53935!important" }} />} />;
let result;
switch (status) {
case "NONE":
result = noneVulnerability;
break;
case "LOW":
result = lowVulnerability;
break;
case "MEDIUM":
result = mediumVulnerability;
break;
case "HIGH":
result = highVulnerability;
break;
case "CRITICAL":
result = criticalVulnerability;
break;
default:
result = unknownVulnerability;
}
return result;
};
function VulnerabilitiyCard(props) {
const classes = useStyles();
const { cve } = props;
const [open, setOpen] = React.useState(false);
return (
<Card className={classes.card} raised>
<CardContent className={classes.content}>
<Stack sx={{ flexDirection: "row" }}>
<Typography variant="body1" align="left" className={classes.title}>ID: </Typography>
<Typography variant="body1" align="left" className={classes.values}> {cve.Id}</Typography>
</Stack>
{vulnerabilityCheck(cve.Severity)}
<Stack sx={{ flexDirection: "row" }}>
<Typography variant="body1" align="left" className={classes.title}>Title: </Typography>
<Typography variant="body1" align="left" className={classes.values}> {cve.Title}</Typography>
</Stack>
<Typography sx={{ color: "#1479FF", paddingTop: "1rem", fontSize: "0.8125rem", fontWeight: "600", cursor: 'pointer' }} onClick={() => setOpen(!open)}>{!open ? 'See description' : 'Hide description'}</Typography>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box>
<Typography variant="body2" align="left" sx={{ color: "#0F2139", fontSize: "1rem" }}> {cve.Description} </Typography>
</Box>
</Collapse>
</CardContent>
</Card>
)
}
function VulnerabilitiesDetails(props) {
const [cveData, setCveData] = useState({});
const [isLoading, setIsLoading] = useState(true);
const { name } = props;
useEffect(() => {
api.get(`${host()}${endpoints.vulnerabilitiesForRepo(name)}`)
.then(response => {
if (response.data && response.data.data) {
let cveInfo = response.data.data.CVEListForImage;
let cveListData = {
cveList: cveInfo?.CVEList
}
setCveData(cveListData);
setIsLoading(false);
}
})
.catch((e) => {
console.error(e);
setCveData({});
});
}, [])
const renderCVEs = (cves) => {
if (cves?.length !== 0) {
return (cves && cves.map((cve, index) => {
return (
<VulnerabilitiyCard key={index} cve={cve} />
);
})
);
}
else {
return (<Typography> No Vulnerabilities </Typography>);
}
}
return (
<div>
<Typography variant="h4" gutterBottom component="div" align="left" style={{ color: "rgba(0, 0, 0, 0.87)", fontSize: "1.5rem", fontWeight: "600", paddingTop:"0.5rem" }}>Vulnerabilities</Typography>
<Divider variant="fullWidth" sx={{ margin: "5% 0% 5% 0%", background: "rgba(0, 0, 0, 0.38)", height: "0.00625rem", width: "100%" }} />
{renderCVEs(cveData?.
// @ts-ignore
cveList)}
</div>
)
}
export default VulnerabilitiesDetails;

View File

@ -1,11 +1,13 @@
body {
margin: 0;
margin: 0rem 0rem 5rem 0rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
overflow-x: hidden;
/* background-image: url(./assets/background.png); */
background-color: #F6F7F9 !important;
}

50
src/pages/TagPage.jsx Normal file
View File

@ -0,0 +1,50 @@
// react global
import React from 'react';
// components
import makeStyles from '@mui/styles/makeStyles';
import { Container, Grid, Stack } from '@mui/material';
import Header from 'components/Header';
import TagDetails from 'components/TagDetails';
import ExploreHeader from 'components/ExploreHeader';
const useStyles = makeStyles((theme) => ({
pageWrapper: {
height:"100%"
},
container: {
paddingTop: 5,
paddingBottom: 5,
backgroundColor: "#FFFFFF",
},
parentWrapper: {
height: '100vh',
},
gridWrapper: {
paddingTop: 10,
paddingBottom: 10,
backgroundColor: "#fff",
width:"100%",
},
}));
function TagPage(props) {
const classes = useStyles();
return (
<Stack direction="column" className={classes.pageWrapper} data-testid='tag-container'>
<Header updateKeywords={props.updateKeywords}></Header>
<Container className={classes.container} >
<ExploreHeader/>
<Grid container className={classes.gridWrapper}>
<Grid item xs={12}>
<TagDetails/>
</Grid>
</Grid>
</Container>
</Stack>
);
}
export default TagPage;