Compare commits
25 Commits
commit-5bf
...
zui
Author | SHA1 | Date | |
---|---|---|---|
3dc49925d0 | |||
303dfb3253 | |||
203b0973a5 | |||
930ae7e0d3 | |||
5918aebbf3 | |||
508fe324b0 | |||
5a9533d95e | |||
182ef55166 | |||
f67c1c8c1e | |||
6cadd7feac | |||
7bd1d7dfc7 | |||
f0bf77a487 | |||
317820926e | |||
c78b303ee8 | |||
9de2337809 | |||
c4d595c782 | |||
09ab4474e9 | |||
177406df41 | |||
e2367c2a33 | |||
33524ce3cc | |||
e037c6c577 | |||
c268991495 | |||
0edfe0f73a | |||
f4a6030d93 | |||
9358539e0c |
@ -1,10 +0,0 @@
|
||||
**/.git
|
||||
**/.svn
|
||||
**/.hg
|
||||
**/node_modules
|
||||
**/.github
|
||||
README.md
|
||||
LICENSE
|
||||
Makefile
|
||||
**/coverage
|
||||
**/build
|
@ -1,54 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended"],
|
||||
"overrides": [],
|
||||
"rules": {
|
||||
"react/prop-types": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react"],
|
||||
"settings": {
|
||||
"react": {
|
||||
"createClass": "createReactClass", // Regex for Component Factory to use,
|
||||
// default to "createReactClass"
|
||||
"pragma": "React", // Pragma to use, default to "React"
|
||||
"fragment": "Fragment", // Fragment to use (may be a property of <pragma>), default to "Fragment"
|
||||
"version": "detect", // React version. "detect" automatically picks the version you have installed.
|
||||
// You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
|
||||
// It will default to "latest" and warn if missing, and to "detect" in the future
|
||||
"flowVersion": "0.53" // Flow version
|
||||
},
|
||||
"propWrapperFunctions": [
|
||||
// The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
|
||||
"forbidExtraProps",
|
||||
{ "property": "freeze", "object": "Object" },
|
||||
{ "property": "myFavoriteWrapper" },
|
||||
// for rules that check exact prop wrappers
|
||||
{ "property": "forbidExtraProps", "exact": true }
|
||||
],
|
||||
"componentWrapperFunctions": [
|
||||
// The name of any function used to wrap components, e.g. Mobx `observer` function. If this isn't set, components wrapped by these functions will be skipped.
|
||||
"observer", // `property`
|
||||
{ "property": "styled" }, // `object` is optional
|
||||
{ "property": "observer", "object": "Mobx" },
|
||||
{ "property": "observer", "object": "<pragma>" } // sets `object` to whatever value `settings.react.pragma` is set to
|
||||
],
|
||||
"formComponents": [
|
||||
// Components used as alternatives to <form> for forms, eg. <Form endpoint={ url } />
|
||||
"CustomForm",
|
||||
{ "name": "Form", "formAttribute": "endpoint" }
|
||||
],
|
||||
"linkComponents": [
|
||||
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
|
||||
"Hyperlink",
|
||||
{ "name": "Link", "linkAttribute": "to" }
|
||||
]
|
||||
}
|
||||
}
|
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
node-version: [20.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
|
13
.github/workflows/coverage.yml
vendored
13
.github/workflows/coverage.yml
vendored
@ -9,26 +9,27 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
run: make install
|
||||
|
||||
- name: Run the tests
|
||||
run: npm test -- --coverage
|
||||
run: npm run test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
|
15
.github/workflows/end-to-end-test.yml
vendored
15
.github/workflows/end-to-end-test.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
name: Test zui/zot integration
|
||||
env:
|
||||
CI: ""
|
||||
REGISTRY_HOST: "localhost"
|
||||
REGISTRY_HOST: "127.0.0.1"
|
||||
REGISTRY_PORT: "8080"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -32,14 +32,14 @@ jobs:
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
|
||||
- name: Checkout zui repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set up Node.js 18.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 18.x
|
||||
cache: 'npm'
|
||||
|
||||
- name: Build zui
|
||||
@ -81,7 +81,7 @@ jobs:
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
go-version: 1.22.x
|
||||
|
||||
- name: Checkout zot repo
|
||||
uses: actions/checkout@v3
|
||||
@ -135,7 +135,8 @@ jobs:
|
||||
make integration-tests REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT
|
||||
|
||||
- name: Upload playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
|
3
Makefile
3
Makefile
@ -6,7 +6,8 @@ all: install audit build
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
npm install --no-audit
|
||||
npm install --no-audit;\
|
||||
npx update-browserslist-db@latest
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
|
@ -1,4 +1,5 @@
|
||||
# zot UI [](https://github.com/project-zot/zui/actions/workflows/build-test.yml) [](http://codecov.io/github/project-zot/zui?branch=main) [](https://github.com/project-zot/zui/actions?query=workflow%3ACodeQL)
|
||||
# zot UI [](https://github.com/project-zot/zui/actions/workflows/build-test.yml) [](http://codecov.io/github/project-zot/zui?branch=main) [](https://github.com/project-zot/zui/actions?query=workflow%3ACodeQL) [](https://app.fossa.com/projects/git%2Bgithub.com%2Fproject-zot%2Fzui?ref=badge_shield)
|
||||
|
||||
A graphical user interface to interact with a [zot](https://github.com/project-zot/zot) server instance.
|
||||
|
||||
Built with [React JS](https://reactjs.org/) and [Material UI](https://mui.com/).
|
||||
@ -59,3 +60,7 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
To learn Material UI, check out the [Material UI Library](https://mui.com/).
|
||||
|
||||
|
||||
## License
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fproject-zot%2Fzui?ref=badge_large)
|
108
eslint.config.mjs
Normal file
108
eslint.config.mjs
Normal file
@ -0,0 +1,108 @@
|
||||
import react from 'eslint-plugin-react';
|
||||
import globals from 'globals';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import js from '@eslint/js';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
ignores: [
|
||||
'**/.git',
|
||||
'**/.svn',
|
||||
'**/.hg',
|
||||
'**/node_modules',
|
||||
'**/.github',
|
||||
'**/README.md',
|
||||
'**/LICENSE',
|
||||
'**/Makefile',
|
||||
'**/coverage',
|
||||
'**/build'
|
||||
]
|
||||
},
|
||||
...compat.extends('eslint:recommended', 'plugin:react/recommended', 'plugin:prettier/recommended'),
|
||||
{
|
||||
plugins: {
|
||||
react
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.jest,
|
||||
...globals.node
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
createClass: 'createReactClass',
|
||||
pragma: 'React',
|
||||
fragment: 'Fragment',
|
||||
version: 'detect',
|
||||
flowVersion: '0.53'
|
||||
},
|
||||
|
||||
propWrapperFunctions: [
|
||||
'forbidExtraProps',
|
||||
{
|
||||
property: 'freeze',
|
||||
object: 'Object'
|
||||
},
|
||||
{
|
||||
property: 'myFavoriteWrapper'
|
||||
},
|
||||
{
|
||||
property: 'forbidExtraProps',
|
||||
exact: true
|
||||
}
|
||||
],
|
||||
|
||||
componentWrapperFunctions: [
|
||||
'observer',
|
||||
{
|
||||
property: 'styled'
|
||||
},
|
||||
{
|
||||
property: 'observer',
|
||||
object: 'Mobx'
|
||||
},
|
||||
{
|
||||
property: 'observer',
|
||||
object: '<pragma>'
|
||||
}
|
||||
],
|
||||
|
||||
formComponents: [
|
||||
'CustomForm',
|
||||
{
|
||||
name: 'Form',
|
||||
formAttribute: 'endpoint'
|
||||
}
|
||||
],
|
||||
|
||||
linkComponents: [
|
||||
'Hyperlink',
|
||||
{
|
||||
name: 'Link',
|
||||
linkAttribute: 'to'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
rules: {
|
||||
'react/prop-types': 'off'
|
||||
}
|
||||
}
|
||||
];
|
15824
package-lock.json
generated
15824
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
65
package.json
65
package.json
@ -3,58 +3,59 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.9.3",
|
||||
"@emotion/styled": "^11.9.3",
|
||||
"@mui/icons-material": "^5.2.5",
|
||||
"@mui/lab": "^5.0.0-alpha.89",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@mui/styles": "^5.8.6",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.24.0",
|
||||
"@adobe/css-tools": "^4.4.1",
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@emotion/styled": "^11.13.5",
|
||||
"@mui/icons-material": "^6.1.10",
|
||||
"@mui/lab": "^6.0.0-beta.18",
|
||||
"@mui/material": "^6.1.10",
|
||||
"@mui/styles": "^6.1.10",
|
||||
"@mui/x-date-pickers": "^7.23.1",
|
||||
"axios": "^1.7.8",
|
||||
"downshift": "^6.1.12",
|
||||
"export-from-json": "^1.7.3",
|
||||
"export-from-json": "^1.7.4",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.5.2",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-sticky-el": "^2.0.9",
|
||||
"luxon": "^3.5.0",
|
||||
"markdown-to-jsx": "^7.6.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.0.2",
|
||||
"react-sticky-el": "^2.1.1",
|
||||
"web-vitals": "^2.1.3",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"prettier": "^2.7.1",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@playwright/test": "^1.46.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"globals": "^14.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --detectOpenHandles",
|
||||
"test:coverage": "react-scripts test --detectOpenHandles --coverage",
|
||||
"test": "react-scripts test --detectOpenHandles --max_old_space_size=4096 --transformIgnorePatterns 'node_modules/(?!my-library-dir)/'",
|
||||
"test:coverage": "react-scripts test --detectOpenHandles --max_old_space_size=4096 --transformIgnorePatterns 'node_modules/(?!my-library-dir)/' --coverage",
|
||||
"test:ui": "playwright test",
|
||||
"test:ui-headed": "playwright test --headed --trace on",
|
||||
"test:ui-debug": "playwright test --trace on",
|
||||
"test:release": "npm run test && npm run test:ui",
|
||||
"lint": "eslint -c .eslintrc.json --ext .js,.jsx .",
|
||||
"lint": "eslint -c eslint.config.mjs",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
|
@ -7,7 +7,6 @@ const { devices } = require('@playwright/test');
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
* @type {import('@playwright/test').PlaywrightTestConfig}
|
||||
@ -53,7 +52,7 @@ const config = {
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
@ -61,7 +60,7 @@ const config = {
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
@ -69,8 +68,8 @@ const config = {
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
@ -102,7 +101,7 @@ const config = {
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: 'test-results/',
|
||||
outputDir: 'test-results/'
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router';
|
||||
|
||||
import { isAuthenticated } from 'utilities/authUtilities';
|
||||
import { isAuthenticated, isApiKeyEnabled } from 'utilities/authUtilities';
|
||||
import { AuthWrapper } from 'utilities/AuthWrapper';
|
||||
|
||||
import HomePage from './pages/HomePage';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import { AuthWrapper } from 'utilities/AuthWrapper';
|
||||
import RepoPage from 'pages/RepoPage';
|
||||
import TagPage from 'pages/TagPage';
|
||||
import ExplorePage from 'pages/ExplorePage';
|
||||
import UserManagementPage from 'pages/UserManagementPage';
|
||||
|
||||
import './App.css';
|
||||
|
||||
@ -25,6 +26,7 @@ function App() {
|
||||
<Route path="/explore" element={<ExplorePage />} />
|
||||
<Route path="/image/:name" element={<RepoPage />} />
|
||||
<Route path="/image/:reponame/tag/:tag" element={<TagPage />} />
|
||||
{isApiKeyEnabled() && <Route path="/user/apikey" element={<UserManagementPage />} />}
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Route>
|
||||
<Route element={<AuthWrapper isLoggedIn={!isLoggedIn} redirect="/" />}>
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import Explore from 'components/Explore/Explore';
|
||||
import React from 'react';
|
||||
import { createSearchParams, MemoryRouter } from 'react-router-dom';
|
||||
import { createSearchParams, MemoryRouter } from 'react-router';
|
||||
import filterConstants from 'utilities/filterConstants.js';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria.js';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// router mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
@ -38,7 +38,7 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
IsSigned: false,
|
||||
SignatureInfo: [],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -63,7 +63,18 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -88,7 +99,18 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -113,7 +135,18 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -138,7 +171,18 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -167,7 +211,18 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -192,7 +247,18 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: 'author1'
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: 'author2'
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -225,7 +291,7 @@ const filteredMockImageListWindows = () => {
|
||||
};
|
||||
|
||||
const filteredMockImageListSigned = () => {
|
||||
const filteredRepos = mockImageList.GlobalSearch.Repos.filter((r) => r.NewestImage.IsSigned);
|
||||
const filteredRepos = mockImageList.GlobalSearch.Repos.filter((r) => r.NewestImage.SignatureInfo?.length > 0);
|
||||
return {
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 6, ItemCount: 6 },
|
||||
@ -273,7 +339,22 @@ describe('Explore component', () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<StateExploreWrapper />);
|
||||
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(1);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(6);
|
||||
expect(await screen.findAllByTestId('untrusted-icon')).toHaveLength(2);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(10);
|
||||
|
||||
const allUntrustedSignaturesIcons = await screen.findAllByTestId('untrusted-icon');
|
||||
fireEvent.mouseOver(allUntrustedSignaturesIcons[0]);
|
||||
expect(await screen.findByText('Signed-by: Unknown')).toBeInTheDocument();
|
||||
const allTrustedSignaturesIcons = await screen.findAllByTestId('verified-icon');
|
||||
fireEvent.mouseOver(allTrustedSignaturesIcons[8]);
|
||||
expect(await screen.findByText('Tool: cosign')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Signed-by: author1')).toBeInTheDocument();
|
||||
fireEvent.mouseOver(allTrustedSignaturesIcons[9]);
|
||||
expect(await screen.findByText('Tool: notation')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Signed-by: author2')).toBeInTheDocument();
|
||||
const allNoSignedIcons = await screen.findAllByTestId('unverified-icon');
|
||||
fireEvent.mouseOver(allNoSignedIcons[0]);
|
||||
expect(await screen.findByText('Not signed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders vulnerability icons', async () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ExplorePage from 'pages/ExplorePage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router';
|
||||
|
||||
jest.mock(
|
||||
'components/Explore/Explore',
|
||||
|
40
src/__tests__/Header/UserAccountMenu.test.js
Normal file
40
src/__tests__/Header/UserAccountMenu.test.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import UserAccountMenu from 'components/Header/UserAccountMenu';
|
||||
import React from 'react';
|
||||
|
||||
const mockIsApiKeyEnabled = jest.fn();
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => {}
|
||||
}));
|
||||
|
||||
jest.mock('../../utilities/authUtilities', () => ({
|
||||
isApiKeyEnabled: () => {
|
||||
return mockIsApiKeyEnabled();
|
||||
},
|
||||
getLoggedInUser: () => {
|
||||
return 'jest-user';
|
||||
},
|
||||
logoutUser: () => {}
|
||||
}));
|
||||
|
||||
describe('Account Menu', () => {
|
||||
it('displays Api Keys menu item with its divider when the API Keys config is enabled', async () => {
|
||||
mockIsApiKeyEnabled.mockReturnValue(true);
|
||||
render(<UserAccountMenu />);
|
||||
const userIconButton = await screen.getByTestId('user-icon-header-button');
|
||||
fireEvent.click(userIconButton);
|
||||
expect(await screen.queryByTestId('api-keys-menu-item')).toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('api-keys-menu-item-divider')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display Api Keys menu item and divider when the API Keys config is disabled', async () => {
|
||||
mockIsApiKeyEnabled.mockReturnValue(false);
|
||||
render(<UserAccountMenu />);
|
||||
const userIconButton = await screen.getByTestId('user-icon-header-button');
|
||||
fireEvent.click(userIconButton);
|
||||
expect(await screen.queryByTestId('api-keys-menu-item')).not.toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('api-keys-menu-item-divider')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -2,14 +2,14 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import Home from 'components/Home/Home';
|
||||
import React from 'react';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import { createSearchParams } from 'react-router';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
@ -32,7 +32,7 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
IsSigned: false,
|
||||
SignatureInfo: [],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -49,7 +49,18 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -66,7 +77,18 @@ const mockImageList = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -91,7 +113,7 @@ const mockImageListRecent = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
IsSigned: false,
|
||||
SignatureInfo: [],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -108,7 +130,18 @@ const mockImageListRecent = {
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -230,7 +263,7 @@ describe('Home component', () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<HomeWrapper />);
|
||||
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(4);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(5);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('renders vulnerability icons', async () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import HomePage from 'pages/HomePage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router';
|
||||
|
||||
jest.mock(
|
||||
'components/Home/Home',
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import LoginPage from 'pages/LoginPage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
it('renders the signin presentation component and signin components if auth enabled', () => {
|
||||
|
@ -7,13 +7,13 @@ import userEvent from '@testing-library/user-event';
|
||||
const mockMgmtResponse = {
|
||||
distSpecVersion: '1.1.0-dev',
|
||||
binaryType: '-apikey-lint-metrics-mgmt-scrub-search-sync-ui-userprefs',
|
||||
http: { auth: { htpasswd: {} } }
|
||||
http: { auth: { htpasswd: {}, openid: { providers: { github: {} } } } }
|
||||
};
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
@ -24,14 +24,14 @@ afterEach(() => {
|
||||
|
||||
describe('Signin component automatic navigation', () => {
|
||||
it('navigates to homepage when user is already logged in', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={true} setIsLoggedIn={() => {}} />);
|
||||
render(<SignIn isLoggedIn={true} setIsLoggedIn={() => {}} />);
|
||||
await expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
|
||||
it('navigates to homepage when auth is disabled', async () => {
|
||||
// mock request to check auth
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { http: {} } });
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
@ -48,49 +48,180 @@ describe('Sign in form', () => {
|
||||
});
|
||||
|
||||
it('should change username and password values on user input', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
fireEvent.change(usernameInput, { target: { value: 'test' } });
|
||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||
expect(usernameInput).toHaveValue('test');
|
||||
expect(passwordInput).toHaveValue('test');
|
||||
expect(screen.getByTestId('openid-divider')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display error if username and password values are empty after change', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
userEvent.click(usernameInput);
|
||||
userEvent.type(usernameInput, 't');
|
||||
userEvent.type(usernameInput, '{backspace}');
|
||||
userEvent.click(passwordInput);
|
||||
userEvent.type(passwordInput, 't');
|
||||
userEvent.type(passwordInput, '{backspace}');
|
||||
await fireEvent.click(usernameInput);
|
||||
await userEvent.type(usernameInput, 't');
|
||||
await userEvent.type(usernameInput, '{backspace}');
|
||||
await fireEvent.click(passwordInput);
|
||||
await userEvent.type(passwordInput, 't');
|
||||
await userEvent.type(passwordInput, '{backspace}');
|
||||
const usernameError = await screen.findByText(/enter a username/i);
|
||||
const passwordError = await screen.findByText(/enter a password/i);
|
||||
await waitFor(() => expect(usernameError).toBeInTheDocument());
|
||||
await waitFor(() => expect(passwordError).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should log in the user and navigate to homepage if login is successful', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
const submitButton = await screen.findByText('Continue');
|
||||
it('should log in the user and navigate to homepage if login is successful using button', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
await userEvent.type(usernameInput, 'test');
|
||||
await userEvent.type(passwordInput, 'test');
|
||||
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {} } });
|
||||
const submitButton = await screen.findByText('Continue');
|
||||
fireEvent.click(submitButton);
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
});
|
||||
|
||||
it('should should display login error if login not successful', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
const submitButton = await screen.findByText('Continue');
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} });
|
||||
fireEvent.click(submitButton);
|
||||
const errorDisplay = await screen.findByText(/Authentication Failed/i);
|
||||
it('should display an error if username is blank and login is attempted using button', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
await userEvent.type(passwordInput, 'test');
|
||||
const submitButton = await screen.findByTestId('basic-auth-submit-btn');
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText(/enter a username/i)).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByText(/enter a password/i)).not.toBeInTheDocument());
|
||||
await waitFor(() => {
|
||||
expect(errorDisplay).toBeInTheDocument();
|
||||
expect(mockedUsedNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display an error if password is blank and login is attempted using button', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
userEvent.type(usernameInput, 'test');
|
||||
const submitButton = await screen.findByTestId('basic-auth-submit-btn');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText(/enter a username/i)).not.toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByText(/enter a password/i)).toBeInTheDocument());
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display an error if username and password are both blank and login is attempted using button', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const submitButton = await screen.findByTestId('basic-auth-submit-btn');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText(/enter a username/i)).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByText(/enter a password/i)).toBeInTheDocument());
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in the user and navigate to homepage if login is successful using enter key on username field', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
await userEvent.type(usernameInput, 'test');
|
||||
await userEvent.type(passwordInput, 'test');
|
||||
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {} } });
|
||||
userEvent.type(usernameInput, '{enter}');
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in the user and navigate to homepage if login is successful using enter key on password field', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
await userEvent.type(usernameInput, 'test');
|
||||
await userEvent.type(passwordInput, 'test');
|
||||
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {} } });
|
||||
await userEvent.type(passwordInput, '{enter}');
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display an error if username is blank and login is attempted using enter key', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
userEvent.type(passwordInput, 'test');
|
||||
userEvent.type(passwordInput, '{enter}');
|
||||
|
||||
await waitFor(() => expect(screen.queryByText(/enter a username/i)).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByText(/enter a password/i)).not.toBeInTheDocument());
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display an error if password is blank and login is attempted using enter key', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
userEvent.type(usernameInput, 'test');
|
||||
userEvent.type(usernameInput, '{enter}');
|
||||
|
||||
await waitFor(() => expect(screen.queryByText(/enter a username/i)).not.toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByText(/enter a password/i)).toBeInTheDocument());
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display an error if username and password are both blank and login is attempted using enter key', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
userEvent.type(passwordInput, '{enter}');
|
||||
|
||||
await waitFor(() => expect(screen.queryByText(/enter a username/i)).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByText(/enter a password/i)).toBeInTheDocument());
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should should display login error if login not successful', async () => {
|
||||
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
await userEvent.type(usernameInput, 'test');
|
||||
await userEvent.type(passwordInput, 'test');
|
||||
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} });
|
||||
|
||||
const submitButton = await screen.findByText('Continue');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Authentication Failed/i)).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import RepoDetails from 'components/Repo/RepoDetails';
|
||||
import React from 'react';
|
||||
import { api } from 'api';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import { createSearchParams } from 'react-router';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
@ -22,8 +22,8 @@ const mockUseLocationValue = {
|
||||
|
||||
const mockUseNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useParams: () => {
|
||||
return { name: 'test' };
|
||||
},
|
||||
@ -51,6 +51,18 @@ const mockRepoDetailsData = {
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: 'author1'
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: 'author2'
|
||||
}
|
||||
],
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'CRITICAL',
|
||||
Count: 15
|
||||
@ -285,6 +297,20 @@ describe('Repo details component', () => {
|
||||
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders signature icons', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(2);
|
||||
|
||||
const allTrustedSignaturesIcons = await screen.findAllByTestId('verified-icon');
|
||||
fireEvent.mouseOver(allTrustedSignaturesIcons[0]);
|
||||
expect(await screen.findByText('Tool: cosign')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Signed-by: author1')).toBeInTheDocument();
|
||||
fireEvent.mouseOver(allTrustedSignaturesIcons[1]);
|
||||
expect(await screen.findByText('Tool: notation')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Signed-by: author2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should log error if data can't be fetched", async () => {
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import RepoPage from 'pages/RepoPage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useLocation: () => ({
|
||||
pathname: 'localhost:3000/image/test',
|
||||
state: { lastDate: '' }
|
||||
|
@ -13,8 +13,8 @@ const TagsThemeWrapper = () => {
|
||||
};
|
||||
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
@ -79,17 +79,17 @@ describe('Tags component', () => {
|
||||
await waitFor(() => expect(screen.queryByText(/OS\/ARCH/i)).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should see delete tag button and its dialog', async () => {
|
||||
render(<TagsThemeWrapper />);
|
||||
const deleteBtn = await screen.findAllByTestId('DeleteIcon');
|
||||
fireEvent.click(deleteBtn[0]);
|
||||
expect(await screen.findByTestId('delete-dialog')).toBeInTheDocument();
|
||||
const confirmBtn = await screen.findByTestId('confirm-delete');
|
||||
expect(confirmBtn).toBeInTheDocument();
|
||||
fireEvent.click(confirmBtn);
|
||||
expect(await screen.findByTestId('confirm-delete')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('cancel-delete')).toBeInTheDocument();
|
||||
});
|
||||
// it('should see delete tag button and its dialog', async () => {
|
||||
// render(<TagsThemeWrapper />);
|
||||
// const deleteBtn = await screen.findAllByTestId('DeleteIcon');
|
||||
// fireEvent.click(deleteBtn[0]);
|
||||
// expect(await screen.findByTestId('delete-dialog')).toBeInTheDocument();
|
||||
// const confirmBtn = await screen.findByTestId('confirm-delete');
|
||||
// expect(confirmBtn).toBeInTheDocument();
|
||||
// fireEvent.click(confirmBtn);
|
||||
// expect(await screen.findByTestId('confirm-delete')).toBeInTheDocument();
|
||||
// expect(await screen.findByTestId('cancel-delete')).toBeInTheDocument();
|
||||
// });
|
||||
|
||||
it('should navigate to tag page details when tag is clicked', async () => {
|
||||
render(<TagsThemeWrapper />);
|
||||
@ -127,9 +127,9 @@ describe('Tags component', () => {
|
||||
render(<TagsThemeWrapper />);
|
||||
const selectFilter = await screen.findByText('Newest');
|
||||
expect(selectFilter).toBeInTheDocument();
|
||||
userEvent.click(selectFilter);
|
||||
await userEvent.click(selectFilter);
|
||||
const newOption = await screen.findByText('A - Z');
|
||||
userEvent.click(newOption);
|
||||
await userEvent.click(newOption);
|
||||
expect(await screen.findByText('A - Z')).toBeInTheDocument();
|
||||
expect(await screen.queryByText('Newest')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -5,8 +5,8 @@ import PreviewCard from 'components/Shared/PreviewCard';
|
||||
|
||||
// usenavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
@ -32,7 +32,7 @@ describe('Preview card component', () => {
|
||||
render(<PreviewCard name={mockImage.name} lastUpdated={mockImage.lastUpdated} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
await userEvent.click(cardTitle);
|
||||
expect(mockedUsedNavigate).toBeCalledWith(`/image/${mockImage.name}`);
|
||||
});
|
||||
});
|
||||
|
@ -2,13 +2,13 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import RepoCard from 'components/Shared/RepoCard';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import { createSearchParams } from 'react-router';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// usenavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
@ -77,7 +77,7 @@ describe('Repo card component', () => {
|
||||
render(<RepoCardWrapper image={mockImage} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
await userEvent.click(cardTitle);
|
||||
expect(mockedUsedNavigate).toBeCalledWith(`/image/${mockImage.name}`);
|
||||
});
|
||||
|
||||
@ -85,7 +85,7 @@ describe('Repo card component', () => {
|
||||
render(<RepoCardWrapper image={{ ...mockImage, lastUpdated: '' }} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
await userEvent.click(cardTitle);
|
||||
expect(mockedUsedNavigate).toBeCalledWith(`/image/${mockImage.name}`);
|
||||
expect(await screen.findByText(/timestamp n\/a/i)).toBeInTheDocument();
|
||||
});
|
||||
|
@ -2,13 +2,13 @@ import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import SearchSuggestion from 'components/Header/SearchSuggestion';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import React from 'react';
|
||||
|
||||
// router mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import DependsOn from 'components/Tag/Tabs/DependsOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const mockDependenciesList = {
|
||||
@ -65,8 +65,8 @@ const RouterDependsWrapper = () => {
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import IsDependentOn from 'components/Tag/Tabs/IsDependentOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const mockDependentsList = {
|
||||
@ -65,8 +65,8 @@ const RouterDependsWrapper = () => {
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
|
@ -40,8 +40,8 @@ const mockReferrersList = [
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
@ -71,7 +71,7 @@ describe('Referred by tab', () => {
|
||||
).toBeInTheDocument();
|
||||
await userEvent.click(firstDigest);
|
||||
expect(
|
||||
await screen.findByText(/sha256:be7a3d01c35a2cf53c502e9dc50cdf36b15d9361c81c63bf319f1d5cbe44ab7c/i)
|
||||
await screen.queryByText(/sha256:be7a3d01c35a2cf53c502e9dc50cdf36b15d9361c81c63bf319f1d5cbe44ab7c/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import TagDetails from 'components/Tag/TagDetails';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router';
|
||||
|
||||
const TagDetailsThemeWrapper = () => {
|
||||
return (
|
||||
@ -174,7 +174,20 @@ const mockImage = {
|
||||
MaxSeverity: 'CRITICAL',
|
||||
Count: 10
|
||||
},
|
||||
Vendor: 'CentOS'
|
||||
Vendor: 'CentOS',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: 'author1'
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: 'author2'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@ -842,8 +855,8 @@ Object.assign(navigator, {
|
||||
}
|
||||
});
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useParams: () => {
|
||||
return { name: 'test', tag: '1.0.1' };
|
||||
},
|
||||
@ -963,6 +976,20 @@ describe('Tags details', () => {
|
||||
expect(await screen.findByTestId('high-vulnerability-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders signature icons', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(2);
|
||||
|
||||
const allTrustedSignaturesIcons = await screen.findAllByTestId('verified-icon');
|
||||
fireEvent.mouseOver(allTrustedSignaturesIcons[0]);
|
||||
expect(await screen.findByText('Tool: cosign')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Signed-by: author1')).toBeInTheDocument();
|
||||
fireEvent.mouseOver(allTrustedSignaturesIcons[1]);
|
||||
expect(await screen.findByText('Tool: notation')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Signed-by: author2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should copy the docker pull string to clipboard', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import TagPage from 'pages/TagPage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router';
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { render, screen, waitFor, within, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
import { api } from 'api';
|
||||
import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
|
||||
jest.mock('xlsx');
|
||||
|
||||
@ -18,17 +18,17 @@ const StateVulnerabilitiesWrapper = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const mockCVEList = {
|
||||
const simpleMockCVEList = {
|
||||
CVEListForImage: {
|
||||
Tag: '',
|
||||
Page: { ItemCount: 20, TotalCount: 20 },
|
||||
Page: { ItemCount: 2, TotalCount: 2 },
|
||||
Summary: {
|
||||
Count: 5,
|
||||
UnknownCount: 1,
|
||||
LowCount: 1,
|
||||
Count: 2,
|
||||
UnknownCount: 0,
|
||||
LowCount: 0,
|
||||
MediumCount: 1,
|
||||
HighCount: 1,
|
||||
CriticalCount: 1,
|
||||
HighCount: 0,
|
||||
CriticalCount: 1
|
||||
},
|
||||
CVEList: [
|
||||
{
|
||||
@ -39,6 +39,54 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'perl-base',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '5.30.0-9ubuntu0.2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2016-1000027',
|
||||
Title: 'spring: HttpInvokerServiceExporter readRemoteInvocation method untrusted java deserialization',
|
||||
Description:
|
||||
"Pivotal Spring Framework through 5.3.16 suffers from a potential remote code execution (RCE) issue if used for Java deserialization of untrusted data. Depending on how the library is implemented within a product, this issue may or not occur, and authentication may be required. NOTE: the vendor's position is that untrusted data is not an intended use case. The product's behavior will not be changed because some users rely on deserialization of trusted data.",
|
||||
Severity: 'CRITICAL',
|
||||
Reference: 'https://avd.aquasec.com/nvd/cve-2016-1000027',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'org.springframework:spring-web',
|
||||
PackagePath: 'usr/local/tomcat/webapps/spring4shell.war/WEB-INF/lib/spring-web-5.3.15.jar',
|
||||
InstalledVersion: '5.3.15',
|
||||
FixedVersion: '6.0.0'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const mockCVEList = {
|
||||
CVEListForImage: {
|
||||
Tag: '',
|
||||
Page: { ItemCount: 20, TotalCount: 20 },
|
||||
Summary: {
|
||||
Count: 5,
|
||||
UnknownCount: 1,
|
||||
LowCount: 1,
|
||||
MediumCount: 1,
|
||||
HighCount: 1,
|
||||
CriticalCount: 1
|
||||
},
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '5.30.0-9ubuntu0.2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -54,26 +102,31 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'krb5-locales',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libgssapi-krb5-2',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libk5crypto3',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5-3',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5support0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -88,6 +141,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libgnutls30',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.6.13-2ubuntu1.6',
|
||||
FixedVersion: '3.6.13-2ubuntu1.7'
|
||||
}
|
||||
@ -102,6 +156,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libpcre2-8-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '10.34-7',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -116,6 +171,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libsqlite3-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||
}
|
||||
@ -130,6 +186,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libpcre3',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '2:8.39-12ubuntu0.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -144,6 +201,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libsqlite3-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||
}
|
||||
@ -158,11 +216,13 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'login',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1:4.8.1-1ubuntu5.20.04.2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'passwd',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1:4.8.1-1ubuntu5.20.04.2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -177,6 +237,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libgmp10',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '2:6.2.0+dfsg-4',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -191,6 +252,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libgnutls30',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.6.13-2ubuntu1.6',
|
||||
FixedVersion: '3.6.13-2ubuntu1.7'
|
||||
}
|
||||
@ -205,26 +267,31 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libncurses6',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libncursesw6',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libtinfo6',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'ncurses-base',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'ncurses-bin',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -239,6 +306,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libpcre2-8-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '10.34-7',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -253,26 +321,31 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libncurses6',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libncursesw6',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libtinfo6',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'ncurses-base',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'ncurses-bin',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -287,6 +360,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'coreutils',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '8.30-3ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -301,46 +375,55 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libasn1-8-heimdal',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libgssapi3-heimdal',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libhcrypto4-heimdal',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libheimbase1-heimdal',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libheimntlm0-heimdal',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libhx509-5-heimdal',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5-26-heimdal',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libroken18-heimdal',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libwind0-heimdal',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -355,11 +438,13 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libc-bin',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '2.31-0ubuntu9.9',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libc6',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '2.31-0ubuntu9.9',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -373,6 +458,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libcurl4',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.68.0-1ubuntu2.12',
|
||||
FixedVersion: '7.68.0-1ubuntu2.13'
|
||||
}
|
||||
@ -388,26 +474,31 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'krb5-locales',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libgssapi-krb5-2',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libk5crypto3',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5-3',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5support0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -422,6 +513,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libsqlite3-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||
}
|
||||
@ -437,6 +529,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'zlib1g',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1:1.2.11.dfsg-2ubuntu1.3',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -450,10 +543,52 @@ const mockCVEListFiltered = {
|
||||
CVEListForImage: {
|
||||
Tag: '',
|
||||
Page: { ItemCount: 20, TotalCount: 20 },
|
||||
Summary: {
|
||||
Count: 5,
|
||||
UnknownCount: 1,
|
||||
LowCount: 1,
|
||||
MediumCount: 1,
|
||||
HighCount: 1,
|
||||
CriticalCount: 1
|
||||
},
|
||||
CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Id.includes('2022'))
|
||||
}
|
||||
};
|
||||
|
||||
const mockCVEListFilteredBySeverity = (severity) => {
|
||||
return {
|
||||
CVEListForImage: {
|
||||
Tag: '',
|
||||
Page: { ItemCount: 20, TotalCount: 20 },
|
||||
Summary: {
|
||||
Count: 5,
|
||||
UnknownCount: 1,
|
||||
LowCount: 1,
|
||||
MediumCount: 1,
|
||||
HighCount: 1,
|
||||
CriticalCount: 1
|
||||
},
|
||||
CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Severity.includes(severity))
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mockCVEListFilteredExclude = {
|
||||
CVEListForImage: {
|
||||
Tag: '',
|
||||
Page: { ItemCount: 20, TotalCount: 20 },
|
||||
Summary: {
|
||||
Count: 5,
|
||||
UnknownCount: 1,
|
||||
LowCount: 1,
|
||||
MediumCount: 1,
|
||||
HighCount: 1,
|
||||
CriticalCount: 1
|
||||
},
|
||||
CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => !e.Id.includes('2022'))
|
||||
}
|
||||
};
|
||||
|
||||
const mockCVEFixed = {
|
||||
pageOne: {
|
||||
ImageListWithCVEFixed: {
|
||||
@ -508,17 +643,76 @@ describe('Vulnerabilties page', () => {
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20));
|
||||
await waitFor(() => expect(screen.getAllByText(/CVE/)).toHaveLength(20));
|
||||
});
|
||||
|
||||
it('renders the vulnerabilities by severity', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(20));
|
||||
expect(screen.getByLabelText('Medium')).toBeInTheDocument();
|
||||
const mediumSeverity = await screen.getByLabelText('Medium');
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('MEDIUM') } });
|
||||
fireEvent.click(mediumSeverity);
|
||||
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(6));
|
||||
expect(screen.getByLabelText('High')).toBeInTheDocument();
|
||||
const highSeverity = await screen.getByLabelText('High');
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('HIGH') } });
|
||||
fireEvent.click(highSeverity);
|
||||
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1));
|
||||
expect(screen.getByLabelText('Critical')).toBeInTheDocument();
|
||||
const criticalSeverity = await screen.getByLabelText('Critical');
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
.mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('CRITICAL') } });
|
||||
fireEvent.click(criticalSeverity);
|
||||
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1));
|
||||
expect(screen.getByLabelText('Low')).toBeInTheDocument();
|
||||
const lowSeverity = await screen.getByLabelText('Low');
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('LOW') } });
|
||||
fireEvent.click(lowSeverity);
|
||||
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(10));
|
||||
expect(screen.getByLabelText('Unknown')).toBeInTheDocument();
|
||||
const unknownSeverity = await screen.getByLabelText('Unknown');
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('UNKNOWN') } });
|
||||
fireEvent.click(unknownSeverity);
|
||||
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1));
|
||||
expect(screen.getByText('Total 5')).toBeInTheDocument();
|
||||
const totalSeverity = await screen.getByText('Total 5');
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('') } });
|
||||
fireEvent.click(totalSeverity);
|
||||
await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(20));
|
||||
});
|
||||
|
||||
it('sends filtered query if user types in the search bar', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText(/CVE/)).toHaveLength(20));
|
||||
const cveSearchInput = screen.getByPlaceholderText(/search/i);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFiltered } });
|
||||
await userEvent.type(cveSearchInput, '2022');
|
||||
expect(cveSearchInput).toHaveValue('2022');
|
||||
await waitFor(() => expect(screen.queryAllByText(/2022/i)).toHaveLength(7));
|
||||
await waitFor(() => expect(screen.queryAllByText(/2021/i)).toHaveLength(1));
|
||||
});
|
||||
|
||||
it('should have a collapsable search bar', async () => {
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
|
||||
.mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredExclude } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
const cveSearchInput = screen.getByPlaceholderText(/search/i);
|
||||
jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } });
|
||||
await userEvent.type(cveSearchInput, '2022');
|
||||
expect((await screen.queryAllByText(/2023/i).length) === 0);
|
||||
expect((await screen.findAllByText(/2022/i)).length === 6);
|
||||
const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0];
|
||||
await fireEvent.click(expandSearch);
|
||||
await waitFor(() => expect(screen.getAllByPlaceholderText('Exclude')).toHaveLength(1));
|
||||
const excludeInput = screen.getByPlaceholderText('Exclude');
|
||||
await userEvent.type(excludeInput, '2022');
|
||||
expect(excludeInput).toHaveValue('2022');
|
||||
await waitFor(() => expect(screen.queryAllByText(/2022/i)).toHaveLength(0));
|
||||
await waitFor(() => expect(screen.queryAllByText(/2021/i)).toHaveLength(6));
|
||||
});
|
||||
|
||||
it('renders no vulnerabilities if there are not any', async () => {
|
||||
@ -530,19 +724,18 @@ describe('Vulnerabilties page', () => {
|
||||
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1));
|
||||
});
|
||||
|
||||
it('should open and close description dropdown for vulnerabilities', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
||||
it('should show description for vulnerabilities', async () => {
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
|
||||
.mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageOne } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText(/description/i)).toHaveLength(20));
|
||||
const openText = screen.getAllByText(/description/i);
|
||||
await fireEvent.click(openText[0]);
|
||||
const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon');
|
||||
fireEvent.click(expandListBtn[0]);
|
||||
await waitFor(() => expect(screen.getAllByText(/Description/)).toHaveLength(20));
|
||||
await waitFor(() =>
|
||||
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1)
|
||||
);
|
||||
await fireEvent.click(openText[0]);
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText(/CPAN 2.28 allows Signature Verification Bypass./i)).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it("should log an error when data can't be fetched", async () => {
|
||||
@ -560,55 +753,112 @@ describe('Vulnerabilties page', () => {
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
await fireEvent.click(screen.getAllByText(/fixed in/i)[0]);
|
||||
const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon');
|
||||
fireEvent.click(expandListBtn[1]);
|
||||
await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument());
|
||||
const loadMoreBtn = screen.getByText(/load more/i);
|
||||
expect(loadMoreBtn).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getAllByText(/Load more/).length).toBe(1));
|
||||
const loadMoreBtn = screen.getAllByText(/Load more/)[0];
|
||||
await fireEvent.click(loadMoreBtn);
|
||||
await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument());
|
||||
expect(await screen.findByText('latest')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the list of vulnerable packages for the CVEs', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: simpleMockCVEList } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
const expandListBtn = await screen.findByTestId('expand-list-view-toggle');
|
||||
fireEvent.click(expandListBtn);
|
||||
const packageLists = await screen.findAllByTestId('cve-package-list');
|
||||
expect(packageLists.length).toEqual(2); // Data set has 2 CVEs, so 2 package lists
|
||||
|
||||
const expectedData = [
|
||||
{
|
||||
Name: 'perl-base',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '5.30.0-9ubuntu0.2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'org.springframework:spring-web',
|
||||
PackagePath: 'usr/local/tomcat/webapps/spring4shell.war/WEB-INF/lib/spring-web-5.3.15.jar',
|
||||
InstalledVersion: '5.3.15',
|
||||
FixedVersion: '6.0.0'
|
||||
}
|
||||
];
|
||||
|
||||
for (let index = 0; index < 2; index++) {
|
||||
const expectedPackageData = expectedData[index];
|
||||
const container = packageLists[index];
|
||||
const pkgName = await within(container).findAllByTestId('cve-info-pkg-name');
|
||||
expect(pkgName).toHaveLength(1);
|
||||
expect(pkgName[0]).toHaveTextContent(expectedPackageData.Name);
|
||||
|
||||
const pkgPath = await within(container).findAllByTestId('cve-info-pkg-path');
|
||||
expect(pkgPath).toHaveLength(1);
|
||||
expect(pkgPath[0]).toHaveTextContent(expectedPackageData.PackagePath);
|
||||
|
||||
const pkgInstalledVer = await within(container).findAllByTestId('cve-info-pkg-install-ver');
|
||||
expect(pkgInstalledVer).toHaveLength(1);
|
||||
expect(pkgInstalledVer[0]).toHaveTextContent(expectedPackageData.InstalledVersion);
|
||||
|
||||
const pkgFixedVer = await within(container).findAllByTestId('cve-info-pkg-fixed-ver');
|
||||
expect(pkgFixedVer).toHaveLength(1);
|
||||
expect(pkgFixedVer[0]).toHaveTextContent(expectedPackageData.FixedVersion);
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow export of vulnerabilities list', async () => {
|
||||
const xlsxMock = jest.createMockFromModule('xlsx');
|
||||
xlsxMock.writeFile = jest.fn();
|
||||
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
const downloadBtn = await screen.findAllByTestId('DownloadIcon');
|
||||
fireEvent.click(downloadBtn[0]);
|
||||
await fireEvent.click(downloadBtn[0]);
|
||||
expect(await screen.findByTestId('export-csv-menuItem')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('export-excel-menuItem')).toBeInTheDocument();
|
||||
const exportAsCSVBtn = screen.getByText(/csv/i);
|
||||
expect(exportAsCSVBtn).toBeInTheDocument();
|
||||
global.URL.createObjectURL = jest.fn();
|
||||
await fireEvent.click(exportAsCSVBtn);
|
||||
expect(await screen.findByTestId('export-csv-menuItem')).not.toBeInTheDocument();
|
||||
fireEvent.click(downloadBtn[0]);
|
||||
await waitFor(() => expect(screen.queryByTestId('export-csv-menuItem')).not.toBeInTheDocument());
|
||||
await fireEvent.click(downloadBtn[0]);
|
||||
const exportAsExcelBtn = screen.getByText(/xlsx/i);
|
||||
expect(exportAsExcelBtn).toBeInTheDocument();
|
||||
await fireEvent.click(exportAsExcelBtn);
|
||||
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument();
|
||||
await userEvent.click(exportAsExcelBtn);
|
||||
expect(await screen.queryByTestId('export-excel-menuItem')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should expand/collapse the list of CVEs', async () => {
|
||||
it("should log an error when data can't be fetched for downloading", async () => {
|
||||
const xlsxMock = jest.createMockFromModule('xlsx');
|
||||
xlsxMock.writeFile = jest.fn();
|
||||
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
|
||||
.mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
const downloadBtn = await screen.findAllByTestId('DownloadIcon');
|
||||
fireEvent.click(downloadBtn[0]);
|
||||
expect(await screen.findByTestId('export-csv-menuItem')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('export-excel-menuItem')).toBeInTheDocument();
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should expand/collapse the list of CVEs', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageOne } });
|
||||
const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon');
|
||||
await fireEvent.click(expandListBtn[0]);
|
||||
await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20));
|
||||
const collapseListBtn = await screen.findAllByTestId('ViewHeadlineIcon');
|
||||
fireEvent.click(collapseListBtn[0]);
|
||||
expect(await screen.findByText('Fixed in')).not.toBeVisible();
|
||||
const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon');
|
||||
fireEvent.click(expandListBtn[0]);
|
||||
await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20));
|
||||
await fireEvent.click(collapseListBtn[0]);
|
||||
await waitFor(() => expect(screen.queryByText('Fixed in')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should handle fixed CVE query errors', async () => {
|
||||
@ -619,7 +869,8 @@ describe('Vulnerabilties page', () => {
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
await fireEvent.click(screen.getAllByText(/fixed in/i)[0]);
|
||||
const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon');
|
||||
fireEvent.click(expandListBtn[1]);
|
||||
await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument());
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
26
src/api.js
26
src/api.js
@ -67,11 +67,14 @@ const api = {
|
||||
return axios.put(urli, payload, config);
|
||||
},
|
||||
|
||||
delete(urli, abortSignal, cfg) {
|
||||
delete(urli, params, abortSignal, cfg) {
|
||||
let config = isEmpty(cfg) ? this.getRequestCfg() : cfg;
|
||||
if (!isEmpty(abortSignal) && isEmpty(config.signal)) {
|
||||
config = { ...config, signal: abortSignal };
|
||||
}
|
||||
if (!isEmpty(params)) {
|
||||
config = { ...config, params };
|
||||
}
|
||||
return axios.delete(urli, config);
|
||||
}
|
||||
};
|
||||
@ -81,6 +84,7 @@ const endpoints = {
|
||||
authConfig: `/v2/_zot/ext/mgmt`,
|
||||
openidAuth: `/zot/auth/login`,
|
||||
logout: `/zot/auth/logout`,
|
||||
apiKeys: '/zot/auth/apikey',
|
||||
deleteImage: (name, tag) => `/v2/${name}/manifests/${tag}`,
|
||||
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
||||
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
||||
@ -89,18 +93,30 @@ const endpoints = {
|
||||
detailedRepoInfo: (name) =>
|
||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor IsDeletable } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||
detailedImageInfo: (name, tag) =>
|
||||
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
||||
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '') => {
|
||||
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}}} Vendor Licenses }}`,
|
||||
vulnerabilitiesForRepo: (
|
||||
name,
|
||||
{ pageNumber = 1, pageSize = 15 },
|
||||
searchTerm = '',
|
||||
excludedTerm = '',
|
||||
severity = ''
|
||||
) => {
|
||||
let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
}}`;
|
||||
if (!isEmpty(searchTerm)) {
|
||||
query += `, searchedCVE: "${searchTerm}"`;
|
||||
}
|
||||
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}} Summary {Count UnknownCount LowCount MediumCount HighCount CriticalCount}}}`;
|
||||
if (!isEmpty(excludedTerm)) {
|
||||
query += `, excludedCVE: "${excludedTerm}"`;
|
||||
}
|
||||
if (!isEmpty(severity)) {
|
||||
query += `, severity: "${severity}"`;
|
||||
}
|
||||
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name PackagePath InstalledVersion FixedVersion}} Summary {Count UnknownCount LowCount MediumCount HighCount CriticalCount}}}`;
|
||||
},
|
||||
allVulnerabilitiesForRepo: (name) =>
|
||||
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name InstalledVersion FixedVersion}}}}`,
|
||||
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name PackagePath InstalledVersion FixedVersion}}}}`,
|
||||
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }, filter = {}) => {
|
||||
let filterParam = '';
|
||||
if (filter.Os || filter.Arch) {
|
||||
|
@ -15,7 +15,7 @@ import makeStyles from '@mui/styles/makeStyles';
|
||||
import { api, endpoints } from '../../api';
|
||||
import { host } from '../../host';
|
||||
import { mapToRepo } from 'utilities/objectModels.js';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import FilterCard from '../Shared/FilterCard.jsx';
|
||||
import { isEmpty, isNil } from 'lodash';
|
||||
import filterConstants from 'utilities/filterConstants.js';
|
||||
@ -221,7 +221,6 @@ function Explore({ searchInputValue }) {
|
||||
description={item.description}
|
||||
downloads={item.downloads}
|
||||
stars={item.stars}
|
||||
isSigned={item.isSigned}
|
||||
signatureInfo={item.signatureInfo}
|
||||
isBookmarked={item.isBookmarked}
|
||||
isStarred={item.isStarred}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// react global
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Link, useLocation, useNavigate } from 'react-router';
|
||||
|
||||
// components
|
||||
import { Typography, Breadcrumbs } from '@mui/material';
|
||||
|
@ -1,6 +1,6 @@
|
||||
// react global
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router';
|
||||
|
||||
import { isAuthenticated, isAuthenticationEnabled, logoutUser } from '../../utilities/authUtilities';
|
||||
|
||||
|
@ -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, useNavigate, useSearchParams } from 'react-router';
|
||||
import { debounce, isEmpty } from 'lodash';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { HEADER_SEARCH_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||
@ -132,8 +132,14 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
||||
|
||||
const handleSearch = (event) => {
|
||||
const { key, type } = event;
|
||||
const name = event.target.value;
|
||||
if (key === 'Enter' || type === 'click') {
|
||||
navigate({ pathname: `/explore`, search: createSearchParams({ search: inputValue || '' }).toString() });
|
||||
if (name?.includes(':')) {
|
||||
const splitName = name.split(':');
|
||||
navigate(`/image/${encodeURIComponent(splitName[0])}/tag/${splitName[1]}`);
|
||||
} else {
|
||||
navigate({ pathname: `/explore`, search: createSearchParams({ search: inputValue || '' }).toString() });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -2,11 +2,17 @@ import React, { useState } from 'react';
|
||||
|
||||
import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material';
|
||||
|
||||
import { getLoggedInUser, logoutUser } from '../../utilities/authUtilities';
|
||||
import { getLoggedInUser, logoutUser, isApiKeyEnabled } from '../../utilities/authUtilities';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
function UserAccountMenu() {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const openMenu = Boolean(anchorEl);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const apiKeyManagement = () => {
|
||||
navigate('/user/apikey');
|
||||
};
|
||||
|
||||
const handleUserClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@ -24,6 +30,7 @@ function UserAccountMenu() {
|
||||
aria-controls={open ? 'account-menu' : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
data-testid="user-icon-header-button"
|
||||
>
|
||||
<Avatar sx={{ width: 32, height: 32 }} />
|
||||
</IconButton>
|
||||
@ -37,6 +44,12 @@ function UserAccountMenu() {
|
||||
>
|
||||
<MenuItem onClick={handleUserClose}>{getLoggedInUser()}</MenuItem>
|
||||
<Divider />
|
||||
{isApiKeyEnabled() && (
|
||||
<MenuItem onClick={apiKeyManagement} data-testid="api-keys-menu-item">
|
||||
API Keys
|
||||
</MenuItem>
|
||||
)}
|
||||
{isApiKeyEnabled() && <Divider data-testid="api-keys-menu-item-divider" />}
|
||||
<MenuItem onClick={logoutUser}>Log out</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
|
@ -6,7 +6,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import RepoCard from '../Shared/RepoCard';
|
||||
import { mapToRepo } from 'utilities/objectModels';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { useNavigate, createSearchParams } from 'react-router-dom';
|
||||
import { useNavigate, createSearchParams } from 'react-router';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||
import {
|
||||
HOME_POPULAR_PAGE_SIZE,
|
||||
@ -261,7 +261,6 @@ function Home() {
|
||||
description={item.description}
|
||||
downloads={item.downloads}
|
||||
stars={item.stars}
|
||||
isSigned={item.isSigned}
|
||||
signatureInfo={item.signatureInfo}
|
||||
isBookmarked={item.isBookmarked}
|
||||
isStarred={item.isStarred}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// react global
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../api';
|
||||
@ -20,7 +20,7 @@ import Alert from '@mui/material/Alert';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Loading from '../Shared/Loading';
|
||||
|
||||
import { GoogleLoginButton, GithubLoginButton, OIDCLoginButton } from './ThirdPartyLoginComponents';
|
||||
import { GoogleLoginButton, GithubLoginButton, GitlabLoginButton, OIDCLoginButton } from './ThirdPartyLoginComponents';
|
||||
|
||||
// styling
|
||||
import { makeStyles } from '@mui/styles';
|
||||
@ -149,8 +149,8 @@ const useStyles = makeStyles(() => ({
|
||||
export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = () => {} }) {
|
||||
const [usernameError, setUsernameError] = useState(null);
|
||||
const [passwordError, setPasswordError] = useState(null);
|
||||
const [username, setUsername] = useState(null);
|
||||
const [password, setPassword] = useState(null);
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [requestProcessing, setRequestProcessing] = useState(false);
|
||||
const [requestError, setRequestError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@ -228,13 +228,20 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
if (Object.keys(authMethods).includes('htpasswd')) {
|
||||
const handleBasicAuthSubmit = () => {
|
||||
setRequestError(false);
|
||||
const isUsernameValid = handleUsernameValidation(username);
|
||||
const isPasswordValid = handlePasswordValidation(password);
|
||||
if (Object.keys(authMethods).includes('htpasswd') && isUsernameValid && isPasswordValid) {
|
||||
handleBasicAuth();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
handleBasicAuthSubmit();
|
||||
};
|
||||
|
||||
const handleGuestClick = () => {
|
||||
setRequestProcessing(false);
|
||||
setRequestError(false);
|
||||
@ -251,38 +258,58 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
);
|
||||
};
|
||||
|
||||
const handleUsernameValidation = (username) => {
|
||||
let isValid = true;
|
||||
if (username === '') {
|
||||
setUsernameError('Please enter a username');
|
||||
isValid = false;
|
||||
} else {
|
||||
setUsernameError(null);
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handlePasswordValidation = (password) => {
|
||||
let isValid = true;
|
||||
if (password === '') {
|
||||
setPasswordError('Please enter a password');
|
||||
isValid = false;
|
||||
} else {
|
||||
setPasswordError(null);
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleChange = (event, type) => {
|
||||
event.preventDefault();
|
||||
setRequestError(false);
|
||||
|
||||
const val = event.target?.value;
|
||||
const isEmpty = val === '';
|
||||
|
||||
switch (type) {
|
||||
case 'username':
|
||||
setUsername(val);
|
||||
if (isEmpty) {
|
||||
setUsernameError('Please enter a username');
|
||||
} else {
|
||||
setUsernameError(null);
|
||||
}
|
||||
handleUsernameValidation(val);
|
||||
break;
|
||||
case 'password':
|
||||
setPassword(val);
|
||||
if (isEmpty) {
|
||||
setPasswordError('Please enter a password');
|
||||
} else {
|
||||
setPasswordError(null);
|
||||
}
|
||||
handlePasswordValidation(val);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginInputFieldKeyDown = (event) => {
|
||||
const keyPressed = event.key;
|
||||
if (keyPressed === 'Enter') {
|
||||
handleBasicAuthSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const renderThirdPartyLoginMethods = () => {
|
||||
let isGoogle = isObject(authMethods.openid?.providers?.google);
|
||||
// let isGitlab = isObject(authMethods.openid?.providers?.gitlab);
|
||||
let isGitlab = isObject(authMethods.openid?.providers?.gitlab);
|
||||
let isGithub = isObject(authMethods.openid?.providers?.github);
|
||||
let isOIDC = isObject(authMethods.openid?.providers?.oidc);
|
||||
let oidcName = authMethods.openid?.providers?.oidc?.name;
|
||||
@ -291,7 +318,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
<Stack direction="column" spacing="1rem" className={classes.thirdPartyLoginContainer}>
|
||||
{isGithub && <GithubLoginButton handleClick={handleClickExternalLogin} />}
|
||||
{isGoogle && <GoogleLoginButton handleClick={handleClickExternalLogin} />}
|
||||
{/* {isGitlab && <GitlabLoginButton handleClick={handleClickExternalLogin} />} */}
|
||||
{isGitlab && <GitlabLoginButton handleClick={handleClickExternalLogin} />}
|
||||
{isOIDC && <OIDCLoginButton handleClick={handleClickExternalLogin} oidcName={oidcName} />}
|
||||
</Stack>
|
||||
);
|
||||
@ -312,7 +339,13 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
Welcome back! Please login.
|
||||
</Typography>
|
||||
{renderThirdPartyLoginMethods()}
|
||||
{Object.keys(authMethods).length > 1 && <Divider className={classes.divider}>or</Divider>}
|
||||
{Object.keys(authMethods).length > 1 &&
|
||||
Object.keys(authMethods).includes('openid') &&
|
||||
Object.keys(authMethods.openid.providers).length > 0 && (
|
||||
<Divider className={classes.divider} data-testid="openid-divider">
|
||||
or
|
||||
</Divider>
|
||||
)}
|
||||
{Object.keys(authMethods).includes('htpasswd') && (
|
||||
<Box component="form" onSubmit={null} noValidate autoComplete="off">
|
||||
<TextField
|
||||
@ -328,6 +361,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
onInput={(e) => handleChange(e, 'username')}
|
||||
error={usernameError != null}
|
||||
helperText={usernameError}
|
||||
onKeyDown={(e) => handleLoginInputFieldKeyDown(e)}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
@ -343,6 +377,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
onInput={(e) => handleChange(e, 'password')}
|
||||
error={passwordError != null}
|
||||
helperText={passwordError}
|
||||
onKeyDown={(e) => handleLoginInputFieldKeyDown(e)}
|
||||
/>
|
||||
{requestProcessing && <CircularProgress style={{ marginTop: 20 }} color="secondary" />}
|
||||
{requestError && (
|
||||
@ -351,7 +386,13 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
</Alert>
|
||||
)}
|
||||
<div>
|
||||
<Button fullWidth variant="contained" className={classes.continueButton} onClick={handleClick}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
className={classes.continueButton}
|
||||
onClick={handleClick}
|
||||
data-testid="basic-auth-submit-btn"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -8,7 +8,10 @@ import { isEmpty, uniq } from 'lodash';
|
||||
// utility
|
||||
import { api, endpoints } from '../../api';
|
||||
import { host } from '../../host';
|
||||
import { useParams, useNavigate, createSearchParams } from 'react-router-dom';
|
||||
import { useParams, useNavigate, createSearchParams } from 'react-router';
|
||||
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
|
||||
import { isAuthenticated } from 'utilities/authUtilities';
|
||||
import filterConstants from 'utilities/filterConstants';
|
||||
|
||||
// components
|
||||
import { Card, CardContent, CardMedia, Chip, Grid, Stack, Tooltip, Typography, IconButton } from '@mui/material';
|
||||
@ -16,7 +19,11 @@ import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import Tags from './Tabs/Tags.jsx';
|
||||
import RepoDetailsMetadata from './RepoDetailsMetadata';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
|
||||
// placeholder images
|
||||
import repocube1 from '../../assets/repocube-1.png';
|
||||
@ -24,13 +31,7 @@ import repocube2 from '../../assets/repocube-2.png';
|
||||
import repocube3 from '../../assets/repocube-3.png';
|
||||
import repocube4 from '../../assets/repocube-4.png';
|
||||
|
||||
import Tags from './Tabs/Tags.jsx';
|
||||
import RepoDetailsMetadata from './RepoDetailsMetadata';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
|
||||
import { isAuthenticated } from 'utilities/authUtilities';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
pageWrapper: {
|
||||
@ -260,6 +261,28 @@ function RepoDetails() {
|
||||
return lastDate;
|
||||
};
|
||||
|
||||
const getSignatureChips = () => {
|
||||
const cosign = repoDetailData.signatureInfo
|
||||
?.map((s) => s.tool)
|
||||
.includes(filterConstants.signatureToolConstants.COSIGN)
|
||||
? repoDetailData.signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.COSIGN)
|
||||
: null;
|
||||
const notation = repoDetailData.signatureInfo
|
||||
?.map((s) => s.tool)
|
||||
.includes(filterConstants.signatureToolConstants.NOTATION)
|
||||
? repoDetailData.signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.NOTATION)
|
||||
: null;
|
||||
const sigArray = [];
|
||||
if (cosign) sigArray.push(cosign);
|
||||
if (notation) sigArray.push(notation);
|
||||
if (sigArray.length === 0) return <SignatureIconCheck />;
|
||||
return sigArray.map((sig, index) => (
|
||||
<div className="hide-on-mobile" key={`${name}sig${index}`}>
|
||||
<SignatureIconCheck signatureInfo={sig} />
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
@ -288,10 +311,7 @@ function RepoDetails() {
|
||||
</Stack>
|
||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
|
||||
<VulnerabilityIconCheck vulnerabilitySeverity={repoDetailData?.vulnerabilitySeverity} />
|
||||
<SignatureIconCheck
|
||||
isSigned={repoDetailData.isSigned}
|
||||
signatureInfo={repoDetailData.signatureInfo}
|
||||
/>
|
||||
{getSignatureChips()}
|
||||
</Stack>
|
||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={1}>
|
||||
{isAuthenticated() && (
|
||||
@ -304,13 +324,20 @@ function RepoDetails() {
|
||||
</IconButton>
|
||||
)}
|
||||
{isAuthenticated() && (
|
||||
<IconButton component="span" onClick={handleBookmarkClick} data-testid="bookmark-button">
|
||||
{repoDetailData?.isBookmarked ? (
|
||||
<BookmarkIcon data-testid="bookmarked" />
|
||||
) : (
|
||||
<BookmarkBorderIcon data-testid="not-bookmarked" />
|
||||
)}
|
||||
</IconButton>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
sx={{ width: { xs: '100%', md: 'auto' } }}
|
||||
direction="row"
|
||||
spacing={2}
|
||||
>
|
||||
<IconButton component="span" onClick={handleBookmarkClick} data-testid="bookmark-button">
|
||||
{repoDetailData?.isBookmarked ? (
|
||||
<BookmarkIcon data-testid="bookmarked" />
|
||||
) : (
|
||||
<BookmarkBorderIcon data-testid="not-bookmarked" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import transform from 'utilities/transform';
|
||||
|
||||
import { Card, CardContent, Typography, Grid, Divider, Stack, Collapse } from '@mui/material';
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||
import transform from 'utilities/transform';
|
||||
import { useState } from 'react';
|
||||
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Card, CardActionArea, CardContent, CardMedia, Grid, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
// placeholder images
|
||||
import repocube1 from '../../assets/repocube-1.png';
|
||||
@ -10,7 +10,7 @@ import repocube3 from '../../assets/repocube-3.png';
|
||||
import repocube4 from '../../assets/repocube-4.png';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { VulnerabilityIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
|
||||
// temporary utility to get image
|
||||
const randomIntFromInterval = (min, max) => {
|
||||
@ -67,7 +67,7 @@ const useStyles = makeStyles(() => ({
|
||||
function PreviewCard(props) {
|
||||
const classes = useStyles();
|
||||
const navigate = useNavigate();
|
||||
const { name, isSigned, vulnerabilityData, logo } = props;
|
||||
const { name, vulnerabilityData, logo } = props;
|
||||
|
||||
const goToDetails = () => {
|
||||
navigate(`/image/${encodeURIComponent(name)}`);
|
||||
@ -108,7 +108,6 @@ function PreviewCard(props) {
|
||||
</Tooltip>
|
||||
<Stack direction="row" spacing={0.5} sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
||||
<VulnerabilityIconCheck {...vulnerabilityData} />
|
||||
<SignatureIconCheck isSigned={isSigned} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
@ -1,6 +1,6 @@
|
||||
// react global
|
||||
import React, { useRef, useMemo, useState } from 'react';
|
||||
import { useNavigate, createSearchParams } from 'react-router-dom';
|
||||
import { useNavigate, createSearchParams } from 'react-router';
|
||||
|
||||
// utility
|
||||
import { DateTime } from 'luxon';
|
||||
@ -32,15 +32,16 @@ import StarIcon from '@mui/icons-material/Star';
|
||||
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import filterConstants from 'utilities/filterConstants';
|
||||
|
||||
// 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 { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
|
||||
// temporary utility to get image
|
||||
const randomIntFromInterval = (min, max) => {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
@ -186,7 +187,6 @@ function RepoCard(props) {
|
||||
description,
|
||||
downloads,
|
||||
stars,
|
||||
isSigned,
|
||||
signatureInfo,
|
||||
lastUpdated,
|
||||
version,
|
||||
@ -296,6 +296,24 @@ function RepoCard(props) {
|
||||
);
|
||||
};
|
||||
|
||||
const getSignatureChips = () => {
|
||||
const cosign = signatureInfo?.map((s) => s.tool).includes(filterConstants.signatureToolConstants.COSIGN)
|
||||
? signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.COSIGN)
|
||||
: null;
|
||||
const notation = signatureInfo?.map((s) => s.tool).includes(filterConstants.signatureToolConstants.NOTATION)
|
||||
? signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.NOTATION)
|
||||
: null;
|
||||
const sigArray = [];
|
||||
if (cosign) sigArray.push(cosign);
|
||||
if (notation) sigArray.push(notation);
|
||||
if (sigArray.length === 0) return <SignatureIconCheck />;
|
||||
return sigArray.map((sig, index) => (
|
||||
<div className="hide-on-mobile" key={`${name}sig${index}`}>
|
||||
<SignatureIconCheck signatureInfo={sig} />
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Card variant="outlined" className={classes.card} data-testid="repo-card">
|
||||
<CardActionArea
|
||||
@ -323,12 +341,10 @@ function RepoCard(props) {
|
||||
{name}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<div className="hide-on-mobile">
|
||||
<div className="hide-on-mobile" style={{ display: 'inline-flex' }}>
|
||||
<VulnerabilityIconCheck {...vulnerabilityData} className="hide-on-mobile" />
|
||||
</div>
|
||||
<div className="hide-on-mobile">
|
||||
<SignatureIconCheck isSigned={isSigned} signatureInfo={signatureInfo} className="hide-on-mobile" />
|
||||
</div>
|
||||
{getSignatureChips()}
|
||||
</Stack>
|
||||
<Tooltip title={description || 'Description not available'} placement="top">
|
||||
<Typography className={classes.description} pt={1} sx={{ fontSize: 12 }} gutterBottom noWrap>
|
||||
|
@ -1,19 +1,17 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Typography, Stack } from '@mui/material';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { getStrongestSignature, getAllAuthorsOfSignatures } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
|
||||
function SignatureTooltip({ isSigned, signatureInfo }) {
|
||||
const { tool, isTrusted, author } = !isEmpty(signatureInfo)
|
||||
? signatureInfo[0]
|
||||
: { tool: 'Unknown', isTrusted: 'Unknown', author: 'Unknown' };
|
||||
function SignatureTooltip({ signatureInfo }) {
|
||||
const strongestSignature = useMemo(() => getStrongestSignature(signatureInfo));
|
||||
|
||||
return (
|
||||
return isEmpty(strongestSignature) ? (
|
||||
<Typography>Not signed</Typography>
|
||||
) : (
|
||||
<Stack direction="column">
|
||||
<Typography>{isSigned ? 'Verified Signature' : 'Unverified Signature'}</Typography>
|
||||
<Typography>Tool: {tool}</Typography>
|
||||
<Typography>Trusted: {!isEmpty(isTrusted) ? isTrusted : 'Unknown'}</Typography>
|
||||
<Typography>Author: {!isEmpty(author) ? author : 'Unknown'}</Typography>
|
||||
<Typography>Tool: {strongestSignature?.tool || 'Unknown'}</Typography>
|
||||
<Typography>Signed-by: {getAllAuthorsOfSignatures(signatureInfo) || 'Unknown'}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Box, Card, CardContent, Collapse, Grid, Stack, Tooltip, Typography, Divider } from '@mui/material';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import transform from 'utilities/transform';
|
||||
@ -167,7 +167,7 @@ export default function TagCard(props) {
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
<Typography variant="body1" color="primary">
|
||||
{el.platform?.Os}/{el.platform?.Arch}
|
||||
{el.platform?.Os || '----'}/{el.platform?.Arch || '----'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid
|
||||
|
@ -9,10 +9,11 @@ import { Box, Card, CardContent, Stack, Typography, Divider } from '@mui/materia
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../../host';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router';
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||
import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { CVE_FIXEDIN_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||
import VulnerabilityPackageSection from './VulnerabilityPackageSection';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
@ -29,18 +30,46 @@ const useStyles = makeStyles((theme) => ({
|
||||
marginTop: '2rem',
|
||||
marginBottom: '2rem'
|
||||
},
|
||||
cardCollapsed: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
border: '1px solid #E0E5EB',
|
||||
borderRadius: '0.75rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
width: '100%'
|
||||
},
|
||||
content: {
|
||||
textAlign: 'left',
|
||||
color: '#606060',
|
||||
padding: '2% 3% 2% 3%',
|
||||
width: '100%'
|
||||
},
|
||||
contentCollapsed: {
|
||||
textAlign: 'left',
|
||||
color: '#606060',
|
||||
padding: '1% 3% 1% 3%',
|
||||
width: '100%',
|
||||
'&:last-child': {
|
||||
paddingBottom: '1%'
|
||||
}
|
||||
},
|
||||
cveId: {
|
||||
color: theme.palette.primary.main,
|
||||
fontSize: '1rem',
|
||||
fontWeight: 400,
|
||||
textDecoration: 'underline'
|
||||
},
|
||||
cveIdCollapsed: {
|
||||
color: theme.palette.primary.main,
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'underline',
|
||||
flexBasis: '19%'
|
||||
},
|
||||
cveSummary: {
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '0.75rem',
|
||||
@ -48,6 +77,13 @@ const useStyles = makeStyles((theme) => ({
|
||||
textOverflow: 'ellipsis',
|
||||
marginTop: '0.5rem'
|
||||
},
|
||||
cveSummaryCollapsed: {
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: '600',
|
||||
textOverflow: 'ellipsis',
|
||||
flexBasis: '82%'
|
||||
},
|
||||
link: {
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
@ -72,14 +108,15 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
vulnerabilityCardDivider: {
|
||||
margin: '1rem 0'
|
||||
},
|
||||
cveInfo: {
|
||||
marginTop: '2%'
|
||||
}
|
||||
}));
|
||||
function VulnerabilitiyCard(props) {
|
||||
const classes = useStyles();
|
||||
const { cve, name, platform, expand } = props;
|
||||
const [openCVE, setOpenCVE] = useState(expand);
|
||||
const [openDesc, setOpenDesc] = useState(false);
|
||||
const [openFixed, setOpenFixed] = useState(false);
|
||||
const [loadingFixed, setLoadingFixed] = useState(true);
|
||||
const [fixedInfo, setFixedInfo] = useState([]);
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
@ -87,9 +124,10 @@ function VulnerabilitiyCard(props) {
|
||||
// pagination props
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [isEndOfList, setIsEndOfList] = useState(false);
|
||||
const [loadMoreInfo, setLoadMoreInfo] = useState(false);
|
||||
|
||||
const getPaginatedResults = () => {
|
||||
if (!openFixed || isEndOfList) {
|
||||
if (!openCVE || (!loadMoreInfo && !isEmpty(fixedInfo)) || isEndOfList) {
|
||||
return;
|
||||
}
|
||||
setLoadingFixed(true);
|
||||
@ -112,11 +150,13 @@ function VulnerabilitiyCard(props) {
|
||||
);
|
||||
}
|
||||
setLoadingFixed(false);
|
||||
setLoadMoreInfo(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setIsEndOfList(true);
|
||||
setLoadingFixed(false);
|
||||
setLoadMoreInfo(false);
|
||||
});
|
||||
};
|
||||
|
||||
@ -125,7 +165,7 @@ function VulnerabilitiyCard(props) {
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [openFixed, pageNumber]);
|
||||
}, [openCVE, pageNumber, loadMoreInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpenCVE(expand);
|
||||
@ -133,6 +173,7 @@ function VulnerabilitiyCard(props) {
|
||||
|
||||
const loadMore = () => {
|
||||
if (loadingFixed || isEndOfList) return;
|
||||
setLoadMoreInfo(true);
|
||||
setPageNumber((pageNumber) => pageNumber + 1);
|
||||
};
|
||||
|
||||
@ -172,59 +213,83 @@ function VulnerabilitiyCard(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent className={classes.content}>
|
||||
<Stack direction="row" spacing="1.25rem">
|
||||
<Card className={openCVE ? classes.card : classes.cardCollapsed} raised>
|
||||
<CardContent className={openCVE ? classes.content : classes.contentCollapsed}>
|
||||
<Stack direction="row" spacing={openCVE ? '1.25rem' : '0.5rem'}>
|
||||
{!openCVE ? (
|
||||
<KeyboardArrowRight className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} />
|
||||
) : (
|
||||
<KeyboardArrowDown className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} />
|
||||
)}
|
||||
<Typography variant="body1" align="left" className={classes.cveId}>
|
||||
<Typography variant="body1" align="left" className={openCVE ? classes.cveId : classes.cveIdCollapsed}>
|
||||
{cve.id}
|
||||
</Typography>
|
||||
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
||||
{openCVE ? (
|
||||
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
||||
) : (
|
||||
<Stack direction="row" spacing="0.5rem" flexBasis="90%">
|
||||
<div style={{ transform: 'scale(0.8)', flexBasis: '18%', flexShrink: '0' }}>
|
||||
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
||||
</div>
|
||||
<Typography variant="body1" align="left" className={classes.cveSummaryCollapsed}>
|
||||
{cve.title}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
<Collapse in={openCVE} timeout="auto" unmountOnExit>
|
||||
<Typography variant="body1" align="left" className={classes.cveSummary}>
|
||||
{cve.title}
|
||||
</Typography>
|
||||
<Divider className={classes.vulnerabilityCardDivider} />
|
||||
<Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}>
|
||||
{!openFixed ? (
|
||||
<KeyboardArrowRight className={classes.dropdownText} />
|
||||
) : (
|
||||
<KeyboardArrowDown className={classes.dropdownText} />
|
||||
)}
|
||||
<Typography className={classes.dropdownText}>Fixed in</Typography>
|
||||
<Typography variant="body2" align="left" className={classes.cveInfo}>
|
||||
External reference
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
align="left"
|
||||
sx={{ color: '#0F2139', fontSize: '1rem', textDecoration: 'underline' }}
|
||||
component={Link}
|
||||
to={cve.reference}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{cve.reference}
|
||||
</Typography>
|
||||
<Typography variant="body2" align="left" className={classes.cveInfo}>
|
||||
Packages
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing="0.3rem"
|
||||
sx={{ width: '100%', padding: '0.5rem 0' }}
|
||||
data-testid="cve-package-list"
|
||||
>
|
||||
{cve.packageList.map((pkg) => (
|
||||
<VulnerabilityPackageSection key={`${cve.id}-${pkg.packageName}`} cve={pkg} />
|
||||
))}
|
||||
</Stack>
|
||||
<Collapse in={openFixed} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ width: '100%', padding: '0.5rem 0' }}>
|
||||
{loadingFixed ? (
|
||||
'Loading...'
|
||||
) : (
|
||||
<Stack direction="row" sx={{ flexWrap: 'wrap' }}>
|
||||
{renderFixedVer()}
|
||||
{renderLoadMore()}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
<Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}>
|
||||
{!openDesc ? (
|
||||
<KeyboardArrowRight className={classes.dropdownText} />
|
||||
<Typography variant="body2" align="left" className={classes.cveInfo}>
|
||||
Fixed in
|
||||
</Typography>
|
||||
<Box sx={{ width: '100%', padding: '0.5rem 0' }}>
|
||||
{loadingFixed ? (
|
||||
'Loading...'
|
||||
) : (
|
||||
<KeyboardArrowDown className={classes.dropdownText} />
|
||||
<Stack direction="row" sx={{ flexWrap: 'wrap' }}>
|
||||
{renderFixedVer()}
|
||||
{renderLoadMore()}
|
||||
</Stack>
|
||||
)}
|
||||
<Typography className={classes.dropdownText}>Description</Typography>
|
||||
</Stack>
|
||||
<Collapse in={openDesc} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ padding: '0.5rem 0' }}>
|
||||
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}>
|
||||
{cve.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
<Typography variant="body2" align="left" className={classes.cveInfo}>
|
||||
Description
|
||||
</Typography>
|
||||
<Box sx={{ padding: '0.5rem 0' }}>
|
||||
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}>
|
||||
{cve.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
@ -18,6 +18,8 @@ const lowBorderColor = '#f0ed94';
|
||||
const unknownColor = '#f2ffdd';
|
||||
const unknownBorderColor = '#e9f4d7';
|
||||
|
||||
const totalBorderColor = '#e0e5eb';
|
||||
|
||||
const fontSize = '0.75rem';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
@ -30,7 +32,11 @@ const useStyles = makeStyles((theme) => ({
|
||||
fontSize: fontSize,
|
||||
fontWeight: '600',
|
||||
borderRadius: '3px',
|
||||
marginBottom: '0'
|
||||
marginBottom: '0',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
totalSeverity: {
|
||||
border: '1px solid ' + totalBorderColor
|
||||
},
|
||||
severityList: {
|
||||
fontSize: fontSize,
|
||||
@ -63,25 +69,27 @@ const useStyles = makeStyles((theme) => ({
|
||||
|
||||
function VulnerabilitiyCountCard(props) {
|
||||
const classes = useStyles();
|
||||
const { total, critical, high, medium, low, unknown } = props;
|
||||
const { total, critical, high, medium, low, unknown, filterBySeverity } = props;
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing="0.5em">
|
||||
<div className={[classes.cveCountCard].join(' ')}>Total {total}</div>
|
||||
<Tooltip title="Total" onClick={() => filterBySeverity('')}>
|
||||
<div className={[classes.cveCountCard, classes.totalSeverity].join(' ')}>Total {total}</div>
|
||||
</Tooltip>
|
||||
<div className={classes.severityList}>
|
||||
<Tooltip title="Critical">
|
||||
<Tooltip title="Critical" onClick={() => filterBySeverity('CRITICAL')}>
|
||||
<div className={[classes.cveCountCard, classes.criticalSeverity].join(' ')}>C {critical}</div>
|
||||
</Tooltip>
|
||||
<Tooltip title="High">
|
||||
<Tooltip title="High" onClick={() => filterBySeverity('HIGH')}>
|
||||
<div className={[classes.cveCountCard, classes.highSeverity].join(' ')}>H {high}</div>
|
||||
</Tooltip>
|
||||
<Tooltip title="Medium">
|
||||
<Tooltip title="Medium" onClick={() => filterBySeverity('MEDIUM')}>
|
||||
<div className={[classes.cveCountCard, classes.mediumSeverity].join(' ')}>M {medium}</div>
|
||||
</Tooltip>
|
||||
<Tooltip title="Low">
|
||||
<Tooltip title="Low" onClick={() => filterBySeverity('LOW')}>
|
||||
<div className={[classes.cveCountCard, classes.lowSeverity].join(' ')}>L {low}</div>
|
||||
</Tooltip>
|
||||
<Tooltip title="Unknown">
|
||||
<Tooltip title="Unknown" onClick={() => filterBySeverity('UNKNOWN')}>
|
||||
<div className={[classes.cveCountCard, classes.unknownSeverity].join(' ')}>U {unknown}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
69
src/components/Shared/VulnerabilityPackageSection.jsx
Normal file
69
src/components/Shared/VulnerabilityPackageSection.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Divider, Grid, Stack, Typography } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
cvePackageCard: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
border: '1px solid #E0E5EB',
|
||||
borderRadius: '0.75rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
width: '100%'
|
||||
},
|
||||
cveInfo: {
|
||||
marginTop: '2%'
|
||||
},
|
||||
vulnerabilityCardDivider: {
|
||||
margin: '1rem 1rem'
|
||||
}
|
||||
}));
|
||||
|
||||
function VulnerabilityPackageSection(props) {
|
||||
const { cve } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing="0.2rem"
|
||||
sx={{ width: '100%', padding: '0.2rem 0.5rem' }}
|
||||
data-testid="cve-package-section"
|
||||
>
|
||||
<Typography variant="overline" color="primary" data-testid="cve-info-pkg-name" sx={{ fontWeight: 'bold' }}>
|
||||
{cve.packageName}
|
||||
</Typography>
|
||||
<Typography variant="body2" className={classes.cveInfo}>
|
||||
Package Path
|
||||
</Typography>
|
||||
<Typography variant="body1" color="primary" data-testid="cve-info-pkg-path">
|
||||
{cve.packagePath}
|
||||
</Typography>
|
||||
<Grid container>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" className={classes.cveInfo}>
|
||||
Installed Version
|
||||
</Typography>
|
||||
<Typography variant="body1" color="primary" data-testid="cve-info-pkg-install-ver">
|
||||
{cve.packageInstalledVersion}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" className={classes.cveInfo}>
|
||||
Fixed Version
|
||||
</Typography>
|
||||
<Typography variant="body1" color="primary" data-testid="cve-info-pkg-fixed-ver">
|
||||
{cve.packageFixedVersion}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider className={classes.vulnerabilityCardDivider} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default VulnerabilityPackageSection;
|
@ -107,7 +107,7 @@ function DependsOn(props) {
|
||||
repoName={dependence.repoName}
|
||||
tag={dependence.tag}
|
||||
vendor={dependence.vendor}
|
||||
isSigned={dependence.isSigned}
|
||||
signatureInfo={dependence.signatureInfo}
|
||||
manifests={dependence.manifests}
|
||||
key={index}
|
||||
lastUpdated={dependence.lastUpdated}
|
||||
|
@ -107,7 +107,7 @@ function IsDependentOn(props) {
|
||||
repoName={dependence.repoName}
|
||||
tag={dependence.tag}
|
||||
vendor={dependence.vendor}
|
||||
isSigned={dependence.isSigned}
|
||||
signatureInfo={dependence.signatureInfo}
|
||||
manifests={dependence.manifests}
|
||||
key={index}
|
||||
lastUpdated={dependence.lastUpdated}
|
||||
|
@ -30,6 +30,9 @@ import exportFromJSON from 'export-from-json';
|
||||
import ViewHeadlineIcon from '@mui/icons-material/ViewHeadline';
|
||||
import ViewAgendaIcon from '@mui/icons-material/ViewAgenda';
|
||||
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
|
||||
import VulnerabilitiyCard from '../../Shared/VulnerabilityCard';
|
||||
import VulnerabilityCountCard from '../../Shared/VulnerabilityCountCard';
|
||||
|
||||
@ -122,6 +125,21 @@ const useStyles = makeStyles((theme) => ({
|
||||
padding: '0.3rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'left'
|
||||
},
|
||||
dropdownArrowBox: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
dropdownText: {
|
||||
color: '#1479FF',
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
},
|
||||
test: {
|
||||
width: '95%'
|
||||
}
|
||||
}));
|
||||
|
||||
@ -135,8 +153,12 @@ function VulnerabilitiesDetails(props) {
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
const { name, tag, digest, platform } = props;
|
||||
|
||||
const [openExcludeSearch, setOpenExcludeSearch] = useState(false);
|
||||
|
||||
// pagination props
|
||||
const [cveFilter, setCveFilter] = useState('');
|
||||
const [cveExcludeFilter, setCveExcludeFilter] = useState('');
|
||||
const [cveSeverityFilter, setCveSeverityFilter] = useState('');
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [isEndOfList, setIsEndOfList] = useState(false);
|
||||
const listBottom = useRef(null);
|
||||
@ -144,7 +166,7 @@ function VulnerabilitiesDetails(props) {
|
||||
const [anchorExport, setAnchorExport] = useState(null);
|
||||
const openExport = Boolean(anchorExport);
|
||||
|
||||
const [selectedViewMore, setSelectedViewMore] = useState(true);
|
||||
const [selectedViewMore, setSelectedViewMore] = useState(false);
|
||||
|
||||
const getCVERequestName = () => {
|
||||
return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;
|
||||
@ -156,7 +178,9 @@ function VulnerabilitiesDetails(props) {
|
||||
`${host()}${endpoints.vulnerabilitiesForRepo(
|
||||
getCVERequestName(),
|
||||
{ pageNumber, pageSize: EXPLORE_PAGE_SIZE },
|
||||
cveFilter
|
||||
cveFilter,
|
||||
cveExcludeFilter,
|
||||
cveSeverityFilter
|
||||
)}`,
|
||||
abortController.signal
|
||||
)
|
||||
@ -226,7 +250,7 @@ function VulnerabilitiesDetails(props) {
|
||||
const wb = XLSX.utils.book_new(),
|
||||
ws = XLSX.utils.json_to_sheet(allCveData);
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, ws, name.replaceAll('/', '_') + '_' + tag);
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'vulnerabilities');
|
||||
|
||||
XLSX.writeFile(wb, `${name}:${tag}-vulnerabilities.xlsx`);
|
||||
|
||||
@ -255,7 +279,17 @@ function VulnerabilitiesDetails(props) {
|
||||
setAnchorExport(null);
|
||||
};
|
||||
|
||||
const handleExpandCVESearch = () => {
|
||||
setOpenExcludeSearch((openExcludeSearch) => !openExcludeSearch);
|
||||
};
|
||||
|
||||
const handleCveExcludeFilterChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setCveExcludeFilter(value);
|
||||
};
|
||||
|
||||
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
|
||||
const debouncedExcludeFilterChangeHandler = useMemo(() => debounce(handleCveExcludeFilterChange, 300));
|
||||
|
||||
useEffect(() => {
|
||||
getPaginatedCVEs();
|
||||
@ -289,12 +323,13 @@ function VulnerabilitiesDetails(props) {
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
resetPagination();
|
||||
}, [cveFilter]);
|
||||
}, [cveFilter, cveExcludeFilter, cveSeverityFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortController.abort();
|
||||
debouncedChangeHandler.cancel();
|
||||
debouncedExcludeFilterChangeHandler.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -319,8 +354,6 @@ function VulnerabilitiesDetails(props) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Test');
|
||||
|
||||
return !isEmpty(cveSummary) ? (
|
||||
<VulnerabilityCountCard
|
||||
total={cveSummary.Count}
|
||||
@ -329,6 +362,7 @@ function VulnerabilitiesDetails(props) {
|
||||
medium={cveSummary.MediumCount}
|
||||
low={cveSummary.LowCount}
|
||||
unknown={cveSummary.UnknownCount}
|
||||
filterBySeverity={setCveSeverityFilter}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
@ -377,6 +411,7 @@ function VulnerabilitiesDetails(props) {
|
||||
className={classes.view}
|
||||
selected={selectedViewMore}
|
||||
onChange={() => setSelectedViewMore(true)}
|
||||
data-testid="expand-list-view-toggle"
|
||||
>
|
||||
<ViewAgendaIcon />
|
||||
</ToggleButton>
|
||||
@ -417,18 +452,41 @@ function VulnerabilitiesDetails(props) {
|
||||
</Menu>
|
||||
</Stack>
|
||||
{renderCVESummary()}
|
||||
<Stack className={classes.search}>
|
||||
<InputBase
|
||||
placeholder={'Search'}
|
||||
classes={{ root: classes.searchInputBase, input: classes.input }}
|
||||
onChange={debouncedChangeHandler}
|
||||
/>
|
||||
<div className={classes.searchIcon}>
|
||||
<SearchIcon />
|
||||
<Stack direction="row">
|
||||
<div className={classes.dropdownArrowBox} onClick={handleExpandCVESearch}>
|
||||
{!openExcludeSearch ? (
|
||||
<KeyboardArrowRight className={classes.dropdownText} />
|
||||
) : (
|
||||
<KeyboardArrowDown className={classes.dropdownText} />
|
||||
)}
|
||||
</div>
|
||||
<Stack className={classes.test} direction="column" spacing="0.25em">
|
||||
<Stack className={classes.search}>
|
||||
<InputBase
|
||||
placeholder={'Search'}
|
||||
classes={{ root: classes.searchInputBase, input: classes.input }}
|
||||
onChange={debouncedChangeHandler}
|
||||
/>
|
||||
<div className={classes.searchIcon}>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Collapse in={openExcludeSearch} timeout="auto" unmountOnExit>
|
||||
<Stack className={classes.search}>
|
||||
<InputBase
|
||||
placeholder={'Exclude'}
|
||||
classes={{ root: classes.searchInputBase, input: classes.input }}
|
||||
onChange={debouncedExcludeFilterChangeHandler}
|
||||
/>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="column" spacing={selectedViewMore ? '1rem' : '0.5rem'}>
|
||||
{renderCVEs()}
|
||||
{renderListBottom()}
|
||||
</Stack>
|
||||
{renderCVEs()}
|
||||
{renderListBottom()}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router';
|
||||
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../api';
|
||||
import { host } from '../../host';
|
||||
import { mapToImage } from '../../utilities/objectModels';
|
||||
import filterConstants from 'utilities/filterConstants';
|
||||
import { isEmpty, head, uniqBy } from 'lodash';
|
||||
|
||||
// components
|
||||
import {
|
||||
Card,
|
||||
@ -19,23 +23,21 @@ import {
|
||||
Typography,
|
||||
InputLabel
|
||||
} from '@mui/material';
|
||||
import TagDetailsMetadata from './TagDetailsMetadata';
|
||||
import VulnerabilitiesDetails from './Tabs/VulnerabilitiesDetails';
|
||||
import HistoryLayers from './Tabs/HistoryLayers';
|
||||
import DependsOn from './Tabs/DependsOn';
|
||||
import IsDependentOn from './Tabs/IsDependentOn';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import ReferredBy from './Tabs/ReferredBy';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../../host';
|
||||
|
||||
// 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 TagDetailsMetadata from './TagDetailsMetadata';
|
||||
import VulnerabilitiesDetails from './Tabs/VulnerabilitiesDetails';
|
||||
import HistoryLayers from './Tabs/HistoryLayers';
|
||||
import DependsOn from './Tabs/DependsOn';
|
||||
import IsDependentOn from './Tabs/IsDependentOn';
|
||||
import { isEmpty, head } from 'lodash';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import ReferredBy from './Tabs/ReferredBy';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
pageWrapper: {
|
||||
@ -189,7 +191,7 @@ function TagDetails() {
|
||||
}, [reponame, tag]);
|
||||
|
||||
const getPlatform = () => {
|
||||
return selectedManifest.platform ? selectedManifest.platform : '--/--';
|
||||
return selectedManifest?.platform ? selectedManifest.platform : '--/--';
|
||||
};
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
@ -204,25 +206,52 @@ function TagDetails() {
|
||||
const renderTabContent = () => {
|
||||
switch (selectedTab) {
|
||||
case 'DependsOn':
|
||||
return <DependsOn name={imageDetailData.name} digest={selectedManifest.digest} />;
|
||||
return <DependsOn name={imageDetailData?.name} digest={selectedManifest?.digest} />;
|
||||
case 'IsDependentOn':
|
||||
return <IsDependentOn name={imageDetailData.name} digest={selectedManifest.digest} />;
|
||||
return <IsDependentOn name={imageDetailData?.name} digest={selectedManifest?.digest} />;
|
||||
case 'Vulnerabilities':
|
||||
return (
|
||||
<VulnerabilitiesDetails
|
||||
name={reponame}
|
||||
tag={tag}
|
||||
digest={selectedManifest?.digest}
|
||||
platform={selectedManifest.platform}
|
||||
platform={selectedManifest?.platform}
|
||||
/>
|
||||
);
|
||||
case 'ReferredBy':
|
||||
return <ReferredBy referrers={imageDetailData.referrers} />;
|
||||
const allReferrers = uniqBy(
|
||||
[...(selectedManifest?.referrers || []), ...(imageDetailData?.referrers || [])],
|
||||
'digest'
|
||||
);
|
||||
|
||||
return <ReferredBy referrers={allReferrers} />;
|
||||
default:
|
||||
return <HistoryLayers name={imageDetailData.name} history={selectedManifest.history} />;
|
||||
return <HistoryLayers name={imageDetailData?.name} history={selectedManifest?.history || []} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getSignatureChips = () => {
|
||||
const cosign = imageDetailData?.signatureInfo
|
||||
?.map((s) => s.tool)
|
||||
?.includes(filterConstants.signatureToolConstants.COSIGN)
|
||||
? imageDetailData?.signatureInfo?.filter((si) => si.tool == filterConstants.signatureToolConstants.COSIGN)
|
||||
: null;
|
||||
const notation = imageDetailData?.signatureInfo
|
||||
?.map((s) => s.tool)
|
||||
?.includes(filterConstants.signatureToolConstants.NOTATION)
|
||||
? imageDetailData?.signatureInfo?.filter((si) => si.tool == filterConstants.signatureToolConstants.NOTATION)
|
||||
: null;
|
||||
const sigArray = [];
|
||||
if (cosign) sigArray.push(cosign);
|
||||
if (notation) sigArray.push(notation);
|
||||
if (sigArray.length === 0) return <SignatureIconCheck />;
|
||||
return sigArray.map((sig, index) => (
|
||||
<div className="hide-on-mobile" key={`${imageDetailData?.name || ''}sig${index}`}>
|
||||
<SignatureIconCheck signatureInfo={sig} />
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
@ -255,39 +284,38 @@ function TagDetails() {
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={1}>
|
||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
|
||||
<VulnerabilityIconCheck
|
||||
vulnerabilitySeverity={imageDetailData.vulnerabiltySeverity}
|
||||
count={imageDetailData.vulnerabilityCount}
|
||||
/>
|
||||
<SignatureIconCheck
|
||||
isSigned={imageDetailData.isSigned}
|
||||
signatureInfo={imageDetailData.signatureInfo}
|
||||
vulnerabilitySeverity={imageDetailData?.vulnerabiltySeverity}
|
||||
count={imageDetailData?.vulnerabilityCount}
|
||||
/>
|
||||
{getSignatureChips()}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center" spacing="1rem">
|
||||
<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>
|
||||
<Typography gutterBottom className={classes.digest}>
|
||||
Digest: {selectedManifest?.digest}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{imageDetailData?.manifests && imageDetailData.manifests.length > 0 && (
|
||||
<Stack direction="row" alignItems="center" spacing="1rem">
|
||||
<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>
|
||||
<Typography gutterBottom className={classes.digest}>
|
||||
Digest: {selectedManifest?.digest}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
@ -325,7 +353,12 @@ function TagDetails() {
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent className={classes.tabCardContent}>{renderTabContent()}</CardContent>
|
||||
<CardContent
|
||||
key={`card_content_manifest_key_${selectedManifest?.digest}`}
|
||||
className={classes.tabCardContent}
|
||||
>
|
||||
{renderTabContent()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} className={classes.metadata}>
|
||||
|
@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import transform from '../../utilities/transform';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import transform from '../../utilities/transform';
|
||||
|
||||
import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
|
||||
import PullCommandButton from 'components/Shared/PullCommandButton';
|
||||
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
display: 'flex',
|
||||
|
163
src/components/User/ApiKeys/ApiKeyCard.jsx
Normal file
163
src/components/User/ApiKeys/ApiKeyCard.jsx
Normal file
@ -0,0 +1,163 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
import { Card, CardContent, Typography, Grid, Divider, Stack, Collapse, Button } from '@mui/material';
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||
import ApiKeyRevokeDialog from './ApiKeyRevokeDialog';
|
||||
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
border: '1px solid #E0E5EB',
|
||||
borderRadius: '0.75rem',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%'
|
||||
},
|
||||
content: {
|
||||
textAlign: 'left',
|
||||
color: '#52637A',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
padding: '1rem',
|
||||
backgroundColor: '#FFFFFF',
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFFFFF'
|
||||
},
|
||||
'&:last-child': {
|
||||
paddingBottom: '1rem'
|
||||
}
|
||||
},
|
||||
label: {
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
paddingRight: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem',
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
expirationDate: {
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem',
|
||||
textAlign: 'right'
|
||||
},
|
||||
revokeButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'right'
|
||||
},
|
||||
dropdownText: {
|
||||
color: '#1479FF',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
},
|
||||
dropdownButton: {
|
||||
color: '#1479FF',
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
dropdownContentBox: {
|
||||
boxSizing: 'border-box',
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: '#F7F7F7',
|
||||
borderRadius: '0.9rem',
|
||||
overflowWrap: 'break-word'
|
||||
},
|
||||
keyCardDivider: {
|
||||
margin: '1rem 0'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeyCard(props) {
|
||||
const classes = useStyles();
|
||||
const { apiKey, onRevoke } = props;
|
||||
const [openDropdown, setOpenDropdown] = useState(false);
|
||||
const [apiKeyRevokeOpen, setApiKeyRevokeOpen] = useState(false);
|
||||
|
||||
const getExpirationDisplay = () => {
|
||||
const expDateTime = DateTime.fromISO(apiKey.expirationDate);
|
||||
return `Expires on ${expDateTime.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)}`;
|
||||
};
|
||||
|
||||
const handleApiKeyRevokeDialogOpen = () => {
|
||||
setApiKeyRevokeOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent className={classes.content}>
|
||||
<Grid container alignItems="center" justifyContent="space-between">
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body1" className={classes.label}>
|
||||
{apiKey.label}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="body1" className={classes.expirationDate}>
|
||||
{getExpirationDisplay()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2} className={classes.revokeButton}>
|
||||
<Button color="error" variant="contained" onClick={handleApiKeyRevokeDialogOpen}>
|
||||
Revoke
|
||||
</Button>
|
||||
</Grid>
|
||||
{!isNil(apiKey.apiKey) && (
|
||||
<>
|
||||
<Grid item xs={12}>
|
||||
<Divider className={classes.keyCardDivider} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" onClick={() => setOpenDropdown((prevOpenState) => !prevOpenState)}>
|
||||
{!openDropdown ? (
|
||||
<KeyboardArrowRight className={classes.dropdownText} />
|
||||
) : (
|
||||
<KeyboardArrowDown className={classes.dropdownText} />
|
||||
)}
|
||||
<Typography className={classes.dropdownButton}>KEY</Typography>
|
||||
</Stack>
|
||||
<Collapse in={openDropdown} timeout="auto" unmountOnExit sx={{ marginTop: '1rem' }}>
|
||||
<Stack direction="column" spacing="1.2rem">
|
||||
<Typography variant="body1" align="left" className={classes.dropdownContentBox}>
|
||||
{apiKey.apiKey}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
<ApiKeyRevokeDialog
|
||||
open={apiKeyRevokeOpen}
|
||||
setOpen={setApiKeyRevokeOpen}
|
||||
apiKey={apiKey}
|
||||
onConfirm={onRevoke}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyCard;
|
57
src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
Normal file
57
src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography, Grid } from '@mui/material';
|
||||
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
gridWrapper: {
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem'
|
||||
},
|
||||
apiKeyDisplay: {
|
||||
boxSizing: 'border-box',
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: '#F7F7F7',
|
||||
borderRadius: '0.9rem',
|
||||
overflowWrap: 'break-word'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeyConfirmDialog(props) {
|
||||
const { open, setOpen, apiKey } = props;
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>Api Key "{apiKey?.label}" Created</DialogTitle>
|
||||
<DialogContent className={classes.apiKeyForm}>
|
||||
<Grid container className={classes.gridWrapper}>
|
||||
<Grid item xs={12}>
|
||||
<Typography>Please copy the api key, you will not be able to see it once the page is refreshed</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1" align="center" className={classes.apiKeyDisplay}>
|
||||
{apiKey?.apiKey}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyConfirmDialog;
|
172
src/components/User/ApiKeys/ApiKeyDialog.jsx
Normal file
172
src/components/User/ApiKeys/ApiKeyDialog.jsx
Normal file
@ -0,0 +1,172 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { isNil, isNumber } from 'lodash';
|
||||
import { DateTime } from 'luxon';
|
||||
import { api, endpoints } from 'api';
|
||||
import { host } from 'host';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
TextField,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
Button,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Typography,
|
||||
Grid
|
||||
} from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
gridWrapper: {
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem'
|
||||
},
|
||||
apiKeyLabel: {
|
||||
paddingBottom: '1rem'
|
||||
},
|
||||
expirationDateContainer: {
|
||||
width: '100%'
|
||||
},
|
||||
expirationDateInput: {
|
||||
width: '100%'
|
||||
},
|
||||
expirationDateDisplay: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeyDialog(props) {
|
||||
const { open, setOpen, onConfirm } = props;
|
||||
|
||||
const [apiKeyLabel, setApiKeyLabel] = useState();
|
||||
const [expirationDateOffset, setExpirationDateOffset] = useState(30);
|
||||
const [selectedExpirationDate, setSelectedExpirationDate] = useState();
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
api
|
||||
.post(`${host()}${endpoints.apiKeys}`, {
|
||||
label: apiKeyLabel,
|
||||
expirationDate: getExpirationDatetime().toISO()
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
onConfirm(response.data);
|
||||
setOpen(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleLabelChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setApiKeyLabel(value);
|
||||
};
|
||||
|
||||
const handleExpirationDateChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setExpirationDateOffset(value);
|
||||
};
|
||||
|
||||
const handleDatePickerChange = (newValue) => {
|
||||
setSelectedExpirationDate(newValue);
|
||||
};
|
||||
|
||||
const getExpirationDatetime = () => {
|
||||
if (isNumber(expirationDateOffset)) {
|
||||
return DateTime.now().plus({ days: expirationDateOffset }).endOf('day');
|
||||
} else if (expirationDateOffset === 'custom') {
|
||||
return DateTime.fromISO(selectedExpirationDate);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getExpirationDisplay = () => {
|
||||
const expDateTime = getExpirationDatetime();
|
||||
return `Expires on ${expDateTime.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>Create Api Key</DialogTitle>
|
||||
<DialogContent className={classes.apiKeyForm}>
|
||||
<Grid container className={classes.gridWrapper}>
|
||||
<Grid item container className={classes.apiKeyLabel} xs={12}>
|
||||
<TextField
|
||||
autoFocus
|
||||
required
|
||||
id="apikeylabel"
|
||||
label="Label"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleLabelChange}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container item xs={12}>
|
||||
<Grid item xs={5}>
|
||||
<FormControl className={classes.expirationDateContainer} size="small" required>
|
||||
<InputLabel disableAnimation>Expiration date</InputLabel>
|
||||
<Select
|
||||
labelId="expirationDate"
|
||||
id="expirationDate"
|
||||
label="Expiration time"
|
||||
onChange={handleExpirationDateChange}
|
||||
value={expirationDateOffset}
|
||||
className={classes.expirationDateInput}
|
||||
>
|
||||
<MenuItem value={7}>7 days</MenuItem>
|
||||
<MenuItem value={30}>30 days</MenuItem>
|
||||
<MenuItem value={60}>60 days</MenuItem>
|
||||
<MenuItem value={90}>90 days</MenuItem>
|
||||
<MenuItem value="custom">custom</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item className={classes.expirationDateDisplay} xs={7}>
|
||||
{expirationDateOffset === 'custom' ? (
|
||||
<DatePicker
|
||||
valueType="date"
|
||||
slotProps={{ textField: { size: 'small' } }}
|
||||
onChange={handleDatePickerChange}
|
||||
/>
|
||||
) : (
|
||||
<Typography>{getExpirationDisplay()}</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={handleSubmit}
|
||||
disabled={expirationDateOffset === 'custom' && isNil(selectedExpirationDate)}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyDialog;
|
71
src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx
Normal file
71
src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
import { api, endpoints } from 'api';
|
||||
import { host } from 'host';
|
||||
|
||||
import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography, Grid } from '@mui/material';
|
||||
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
gridWrapper: {
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem'
|
||||
},
|
||||
apiKeyDisplay: {
|
||||
boxSizing: 'border-box',
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: '#F7F7F7',
|
||||
borderRadius: '0.9rem',
|
||||
overflowWrap: 'break-word'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeyRevokeDialog(props) {
|
||||
const { open, setOpen, apiKey, onConfirm } = props;
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
api
|
||||
.delete(`${host()}${endpoints.apiKeys}`, { id: apiKey.uuid })
|
||||
.then((response) => {
|
||||
onConfirm(response?.status, apiKey);
|
||||
setOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>Revoke "{apiKey?.label}" key</DialogTitle>
|
||||
<DialogContent className={classes.apiKeyForm}>
|
||||
<Grid container className={classes.gridWrapper}>
|
||||
<Grid item xs={12}>
|
||||
<Typography>Are you sure you want to revoke this api key?</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="contained" color="error" onClick={handleSubmit}>
|
||||
Revoke
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyRevokeDialog;
|
146
src/components/User/ApiKeys/ApiKeys.jsx
Normal file
146
src/components/User/ApiKeys/ApiKeys.jsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { isEmpty, isNil } from 'lodash';
|
||||
import { api, endpoints } from 'api';
|
||||
import { host } from '../../../host';
|
||||
|
||||
import { Grid, Stack, Card, CardContent, Typography, Button } from '@mui/material';
|
||||
import Loading from '../../Shared/Loading';
|
||||
import ApiKeyDialog from './ApiKeyDialog';
|
||||
import ApiKeyConfirmDialog from './ApiKeyConfirmDialog';
|
||||
import ApiKeyCard from './ApiKeyCard';
|
||||
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
pageWrapper: {
|
||||
backgroundColor: 'transparent',
|
||||
height: '100%'
|
||||
},
|
||||
header: {
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0'
|
||||
}
|
||||
},
|
||||
cardRoot: {
|
||||
boxShadow: 'none!important'
|
||||
},
|
||||
pageTitle: {
|
||||
fontWeight: '600',
|
||||
fontSize: '1.5rem',
|
||||
color: theme.palette.secondary.main,
|
||||
textAlign: 'left'
|
||||
},
|
||||
apikeysContainer: {
|
||||
marginTop: '1.5rem',
|
||||
height: '100%',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0'
|
||||
}
|
||||
},
|
||||
apikeysContent: {
|
||||
padding: '1.5rem'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeys() {
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
const [newApiKey, setNewApiKey] = useState();
|
||||
const classes = useStyles();
|
||||
|
||||
// ApiKey dialog props
|
||||
const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false);
|
||||
const [apiKeyConfirmationOpen, setApiKeyConfirmationOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
api
|
||||
.get(`${host()}${endpoints.apiKeys}`)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.apiKeys) {
|
||||
setApiKeys(response.data.apiKeys);
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setIsLoading(false);
|
||||
});
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNil(newApiKey) && !apiKeyConfirmationOpen) {
|
||||
setApiKeyConfirmationOpen(true);
|
||||
}
|
||||
}, [newApiKey]);
|
||||
|
||||
const handleApiKeyDialogOpen = () => {
|
||||
setApiKeyDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleApiKeyCreateConfirm = (apiKey) => {
|
||||
setNewApiKey(apiKey);
|
||||
setApiKeys((prevState) => [...prevState, apiKey]);
|
||||
};
|
||||
|
||||
const handleApiKeyRevokeConfirm = (status, apiKey) => {
|
||||
if (status === 200) setApiKeys((prevState) => prevState.filter((ak) => ak.uuid != apiKey.uuid));
|
||||
};
|
||||
|
||||
const renderApiKeys = () => {
|
||||
return apiKeys.map((apiKey) => (
|
||||
<ApiKeyCard key={apiKey.uuid} apiKey={apiKey} onRevoke={handleApiKeyRevokeConfirm} />
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Grid container className={classes.pageWrapper}>
|
||||
<Grid item xs={12} md={12}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent>
|
||||
<Grid container className={classes.header}>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="h4" className={classes.pageTitle}>
|
||||
Manage your API Keys
|
||||
</Typography>
|
||||
<Button variant="contained" color="success" onClick={handleApiKeyDialogOpen}>
|
||||
Create new API key
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
{!isLoading && !isEmpty(apiKeys) && (
|
||||
<Grid item xs={12} className={classes.apikeysContainer}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent className={classes.apikeysContent}>
|
||||
<Stack direction="column" spacing={1}>
|
||||
{renderApiKeys()}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
<ApiKeyDialog open={apiKeyDialogOpen} setOpen={setApiKeyDialogOpen} onConfirm={handleApiKeyCreateConfirm} />
|
||||
{!isNil(newApiKey) && (
|
||||
<ApiKeyConfirmDialog open={apiKeyConfirmationOpen} setOpen={setApiKeyConfirmationOpen} apiKey={newApiKey} />
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeys;
|
16
src/index.js
16
src/index.js
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
import { createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from '@mui/material/styles';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
const theme = createTheme(
|
||||
adaptV4Theme({
|
||||
@ -32,15 +34,19 @@ theme.typography.h4 = {
|
||||
}
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<App />
|
||||
<LocalizationProvider dateAdapter={AdapterLuxon}>
|
||||
<App />
|
||||
</LocalizationProvider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
@ -14,7 +14,6 @@ const useStyles = makeStyles(() => ({
|
||||
minWidth: '60%'
|
||||
},
|
||||
gridWrapper: {
|
||||
// backgroundColor: "#fff",
|
||||
border: '0.0625em #f2f2f2 dashed'
|
||||
},
|
||||
pageWrapper: {
|
||||
|
57
src/pages/UserManagementPage.jsx
Normal file
57
src/pages/UserManagementPage.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { getLoggedInUser } from 'utilities/authUtilities.js';
|
||||
|
||||
import { Container, Grid, Stack } from '@mui/material';
|
||||
|
||||
import Header from '../components/Header/Header.jsx';
|
||||
import ApiKeys from '../components/User/ApiKeys/ApiKeys.jsx';
|
||||
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
container: {
|
||||
paddingTop: 30,
|
||||
paddingBottom: 5,
|
||||
height: '100%',
|
||||
minWidth: '60%'
|
||||
},
|
||||
gridWrapper: {
|
||||
border: '0.0625rem #f2f2f2 dashed'
|
||||
},
|
||||
pageWrapper: {
|
||||
height: '100%'
|
||||
},
|
||||
tile: {
|
||||
width: '100%',
|
||||
padding: 5
|
||||
}
|
||||
}));
|
||||
|
||||
function UserManagementPage() {
|
||||
const classes = useStyles();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(getLoggedInUser())) {
|
||||
navigate('/home');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack className={classes.pageWrapper} direction="column" data-testid="explore-container">
|
||||
<Header />
|
||||
<Container className={classes.container}>
|
||||
<Grid container className={classes.gridWrapper}>
|
||||
<Grid item className={classes.tile}>
|
||||
<ApiKeys />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserManagementPage;
|
@ -3,3 +3,7 @@
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { TextEncoder } from 'node:util';
|
||||
|
||||
global.TextEncoder = TextEncoder;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import { Navigate, Outlet } from 'react-router';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
|
@ -41,10 +41,15 @@ const isAuthenticationEnabled = () => {
|
||||
return Object.keys(authMethods).length > 0;
|
||||
};
|
||||
|
||||
const isApiKeyEnabled = () => {
|
||||
const authConfig = JSON.parse(localStorage.getItem('authConfig')) || {};
|
||||
return authConfig?.apikey;
|
||||
};
|
||||
|
||||
const getLoggedInUser = () => {
|
||||
const userCookie = getCookie('user');
|
||||
if (!userCookie) return null;
|
||||
return userCookie;
|
||||
};
|
||||
|
||||
export { isAuthenticated, isAuthenticationEnabled, getLoggedInUser, logoutUser };
|
||||
export { isAuthenticated, isAuthenticationEnabled, isApiKeyEnabled, getLoggedInUser, logoutUser };
|
||||
|
@ -66,9 +66,24 @@ const archFilters = [
|
||||
label: 'amd64',
|
||||
value: 'amd64',
|
||||
tooltip: '64-bit x86'
|
||||
},
|
||||
{
|
||||
label: 'loong64',
|
||||
value: 'loong64',
|
||||
tooltip: '64-bit LoongArch'
|
||||
},
|
||||
{
|
||||
label: 'riscv64',
|
||||
value: 'riscv64',
|
||||
tooltip: '64-bit RISC-V'
|
||||
}
|
||||
];
|
||||
|
||||
const filterConstants = { osFilters, imageFilters, archFilters };
|
||||
const signatureToolConstants = {
|
||||
COSIGN: 'cosign',
|
||||
NOTATION: 'notation'
|
||||
};
|
||||
|
||||
const filterConstants = { osFilters, imageFilters, archFilters, signatureToolConstants };
|
||||
|
||||
export default filterConstants;
|
||||
|
@ -86,7 +86,8 @@ const mapToManifest = (responseManifest) => {
|
||||
starCount: responseManifest.StarCount,
|
||||
layers: responseManifest.Layers,
|
||||
history: responseManifest.History,
|
||||
vulnerabilities: responseManifest.Vulnerabilities
|
||||
vulnerabilities: responseManifest.Vulnerabilities,
|
||||
referrers: responseManifest.Referrers
|
||||
};
|
||||
};
|
||||
|
||||
@ -96,7 +97,14 @@ const mapCVEInfo = (cveInfo) => {
|
||||
id: cve.Id,
|
||||
severity: cve.Severity,
|
||||
title: cve.Title,
|
||||
description: cve.Description
|
||||
description: cve.Description,
|
||||
reference: cve.Reference,
|
||||
packageList: cve.PackageList?.map((pkg) => ({
|
||||
packageName: pkg.Name,
|
||||
packagePath: pkg.PackagePath,
|
||||
packageInstalledVersion: pkg.InstalledVersion,
|
||||
packageFixedVersion: pkg.FixedVersion
|
||||
}))
|
||||
};
|
||||
});
|
||||
return cveList;
|
||||
@ -112,6 +120,7 @@ const mapAllCVEInfo = (cveInfo) => {
|
||||
description: cve.Description,
|
||||
reference: cve.Reference,
|
||||
packageName: packageInfo.Name,
|
||||
packagePath: packageInfo.PackagePath,
|
||||
packageInstalledVersion: packageInfo.InstalledVersion,
|
||||
packageFixedVersion: packageInfo.FixedVersion
|
||||
};
|
||||
@ -124,12 +133,12 @@ const mapSignatureInfo = (signatureInfo) => {
|
||||
return signatureInfo
|
||||
? {
|
||||
tool: signatureInfo.Tool,
|
||||
isTrusted: signatureInfo.IsTrusted?.toString(),
|
||||
isTrusted: signatureInfo.IsTrusted,
|
||||
author: signatureInfo.Author
|
||||
}
|
||||
: {
|
||||
tool: 'Unknown',
|
||||
isTrusted: 'Unknown',
|
||||
isTrusted: false,
|
||||
author: 'Unknown'
|
||||
};
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
NoneVulnerabilityIcon,
|
||||
@ -12,14 +13,26 @@ import {
|
||||
CriticalVulnerabilityChip,
|
||||
UnverifiedSignatureIcon,
|
||||
VerifiedSignatureIcon,
|
||||
UnverifiedSignatureChip,
|
||||
VerifiedSignatureChip,
|
||||
UnknownVulnerabilityIcon,
|
||||
UnknownVulnerabilityChip,
|
||||
FailedScanIcon,
|
||||
FailedScanChip
|
||||
FailedScanChip,
|
||||
NotTrustedSignatureIcon
|
||||
} from './vulnerabilityAndSignatureComponents';
|
||||
|
||||
const getStrongestSignature = (signatureInfo) => {
|
||||
if (isEmpty(signatureInfo)) return null;
|
||||
const trusted = signatureInfo.find((si) => si.isTrusted);
|
||||
if (!isEmpty(trusted)) return trusted;
|
||||
return signatureInfo[0];
|
||||
};
|
||||
|
||||
const getAllAuthorsOfSignatures = (signatureInfo) => {
|
||||
if (isEmpty(signatureInfo)) return '';
|
||||
const signatureAuthors = signatureInfo.filter((si) => si.isTrusted).map((si) => si.author);
|
||||
return signatureAuthors.join(',');
|
||||
};
|
||||
|
||||
const VulnerabilityIconCheck = ({ vulnerabilitySeverity }) => {
|
||||
let result;
|
||||
let vulnerabilityStringTitle = '';
|
||||
@ -84,20 +97,17 @@ const VulnerabilityChipCheck = ({ vulnerabilitySeverity }) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const SignatureIconCheck = ({ isSigned, signatureInfo }) => {
|
||||
if (isSigned) {
|
||||
return <VerifiedSignatureIcon signatureInfo={signatureInfo} />;
|
||||
} else {
|
||||
return <UnverifiedSignatureIcon signatureInfo={signatureInfo} />;
|
||||
}
|
||||
const SignatureIconCheck = ({ signatureInfo }) => {
|
||||
const strongestSignature = getStrongestSignature(signatureInfo);
|
||||
if (strongestSignature === null) return <UnverifiedSignatureIcon signatureInfo={signatureInfo} />;
|
||||
if (strongestSignature.isTrusted) return <VerifiedSignatureIcon signatureInfo={signatureInfo} />;
|
||||
return <NotTrustedSignatureIcon signatureInfo={signatureInfo} />;
|
||||
};
|
||||
|
||||
const SignatureChipCheck = ({ isSigned }) => {
|
||||
if (isSigned) {
|
||||
return <VerifiedSignatureChip />;
|
||||
} else {
|
||||
return <UnverifiedSignatureChip />;
|
||||
}
|
||||
export {
|
||||
VulnerabilityIconCheck,
|
||||
VulnerabilityChipCheck,
|
||||
SignatureIconCheck,
|
||||
getStrongestSignature,
|
||||
getAllAuthorsOfSignatures
|
||||
};
|
||||
|
||||
export { VulnerabilityIconCheck, VulnerabilityChipCheck, SignatureIconCheck, SignatureChipCheck };
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Chip, Tooltip } from '@mui/material';
|
||||
import { Chip, Tooltip, Badge } from '@mui/material';
|
||||
import SvgIcon from '@mui/material/SvgIcon';
|
||||
import { ReactComponent as failedScanBug } from '../assets/failedScan.svg';
|
||||
import { createSvgIcon } from '@mui/material/utils';
|
||||
import SignatureTooltip from 'components/Shared/SignatureTooltip';
|
||||
import filterConstants from 'utilities/filterConstants';
|
||||
|
||||
const FilledBugIcon = createSvgIcon(
|
||||
<path d="M17.0293 5.13093V6.1543H18.3828L21.2414 3.24068L22.2621 4.27812L19.5552 7.03876L19.5879 7.12668C20.1841 8.73695 20.4862 10.4449 20.4793 12.1662C20.4793 12.5064 20.4678 12.8466 20.4448 13.186L20.4397 13.2634H24V14.7334H20.2569L20.2466 14.7932C19.9431 16.4882 19.3517 18.0338 18.5466 19.335L18.4862 19.4335L21.9276 22.9608L20.9052 24L17.6121 20.6239L17.5138 20.7365C16.0259 22.4333 14.0983 23.4514 11.9983 23.4514C9.86724 23.4514 7.91207 22.4016 6.41552 20.6573L6.31552 20.5413L3.08966 23.833L2.06897 22.792L5.45345 19.3403L5.39483 19.2436C4.61897 17.9618 4.04655 16.4478 3.75 14.7932L3.73966 14.7334H0V13.2634H3.55862L3.55345 13.1843C3.53103 12.8502 3.51897 12.509 3.51897 12.1644C3.51202 10.4654 3.80581 8.77905 4.38621 7.18646L4.41897 7.1003L1.64138 4.2535L2.66379 3.21606L5.53103 6.1543H6.96724V5.13093C6.96724 3.77012 7.49729 2.46505 8.4408 1.50281C9.3843 0.540578 10.664 0 11.9983 0C13.3326 0 14.6123 0.540578 15.5558 1.50281C16.4993 2.46505 17.0293 3.77012 17.0293 5.13093Z" />,
|
||||
@ -14,11 +15,15 @@ const OutlinedBugIcon = createSvgIcon(
|
||||
'OutlinedBug'
|
||||
);
|
||||
const UnverifiedShieldIcon = createSvgIcon(
|
||||
<path d="M12.4837 2C13.6167 2 19.5627 4.041 20.3487 4.828C21.0047 5.484 20.9947 6.014 20.9487 8.557C20.9307 9.575 20.9057 10.962 20.9057 12.879C20.9057 19.761 13.0357 22.223 12.7007 22.324C12.6297 22.346 12.5567 22.356 12.4837 22.356C12.4107 22.356 12.3377 22.346 12.2667 22.324C11.9317 22.223 4.06165 19.761 4.06165 12.879C4.06165 10.959 4.03665 9.572 4.01865 8.554C4.01044 8.10043 4.00337 7.71095 4.00104 7.37341L4.00073 6.9925C4.00922 5.74112 4.1264 5.32 4.61765 4.828C5.40465 4.041 11.3507 2 12.4837 2ZM12.4837 3.5C11.6357 3.5 6.28465 5.384 5.66765 5.899C5.54931 6.018 5.50535 6.19514 5.49972 6.89808L5.49926 7.16877C5.50045 7.51182 5.50742 7.95335 5.51765 8.526C5.53665 9.552 5.56165 10.947 5.56165 12.879C5.56165 18.08 11.2837 20.389 12.4827 20.814C13.6807 20.387 19.4057 18.065 19.4057 12.879C19.4057 10.949 19.4307 9.555 19.4487 8.529C19.4592 7.95581 19.4663 7.51389 19.4674 7.17033L19.4668 6.89918C19.4605 6.19482 19.4138 6.01519 19.2877 5.889C18.6817 5.384 13.3317 3.5 12.4837 3.5ZM11.1346 9.5372L12.4837 10.887L13.8328 9.5372C14.1258 9.2442 14.5998 9.2442 14.8928 9.5372C15.1858 9.8302 15.1858 10.3042 14.8928 10.5972L13.5437 11.947L14.8926 13.2952C15.1856 13.5882 15.1856 14.0622 14.8926 14.3552C14.7466 14.5022 14.5546 14.5752 14.3626 14.5752C14.1706 14.5752 13.9786 14.5022 13.8326 14.3552L12.4837 13.007L11.1348 14.3552C10.9888 14.5022 10.7968 14.5752 10.6048 14.5752C10.4128 14.5752 10.2208 14.5022 10.0748 14.3552C9.78175 14.0622 9.78175 13.5882 10.0748 13.2952L11.4237 11.947L10.0746 10.5972C9.78155 10.3042 9.78155 9.8302 10.0746 9.5372C10.3676 9.2442 10.8416 9.2442 11.1346 9.5372Z" />,
|
||||
<path d="M9,0,0,4v6c0,5.55,3.84,10.74,9,12,5.16-1.26,9-6.45,9-12V4Zm7,10a10.47,10.47,0,0,1-7,9.93A10.47,10.47,0,0,1,2,10V5.3L9,2.19,16,5.3ZM7,7.74l1.22,1.4c.32.36.58.7.86,1.06H9.1c.28-.39.56-.72.84-1.07l1.2-1.39h1.68L9.91,10.89l3,3.37H11.14L9.89,12.79c-.33-.38-.62-.74-.92-1.13h0c-.28.39-.58.74-.9,1.13L6.8,14.26H5.09l3-3.33L5.23,7.74Z" />,
|
||||
'UnverifiedShield'
|
||||
);
|
||||
const VerifiedShieldIcon = createSvgIcon(
|
||||
<path d="M12.4836 2C13.6166 2 19.5616 4.041 20.3486 4.828C21.0046 5.484 20.9946 6.014 20.9486 8.554C20.9306 9.572 20.9056 10.959 20.9056 12.879C20.9056 19.761 13.0356 22.223 12.7006 22.324C12.6296 22.346 12.5566 22.356 12.4836 22.356C12.4106 22.356 12.3376 22.346 12.2666 22.324C11.9316 22.223 4.06162 19.761 4.06162 12.879C4.06162 10.962 4.03662 9.575 4.01862 8.557C4.01041 8.10289 4.00334 7.71298 4.00102 7.37507L4.00073 6.99377C4.00931 5.74113 4.12687 5.32 4.61962 4.828C5.40462 4.041 11.3496 2 12.4836 2ZM12.4836 3.5C11.6356 3.5 6.28562 5.384 5.66862 5.899C5.48662 6.082 5.47962 6.4 5.51862 8.529C5.53662 9.555 5.56162 10.949 5.56162 12.879C5.56162 18.08 11.2836 20.389 12.4826 20.814C13.6806 20.387 19.4056 18.065 19.4056 12.879C19.4056 10.947 19.4306 9.552 19.4496 8.526C19.4876 6.399 19.4806 6.081 19.2876 5.889C18.6826 5.384 13.3316 3.5 12.4836 3.5ZM16.2051 9.3395C16.4981 9.6325 16.4981 10.1075 16.2051 10.4005L12.3071 14.2995C12.1951 14.4123 12.0505 14.4854 11.8952 14.5102L11.7771 14.5195C11.5781 14.5195 11.3871 14.4405 11.2461 14.2995L9.35412 12.4055C9.06212 12.1125 9.06212 11.6365 9.35512 11.3445C9.64712 11.0515 10.1231 11.0515 10.4161 11.3445L11.7771 12.7075L15.1451 9.3395C15.4381 9.0465 15.9121 9.0465 16.2051 9.3395Z" />,
|
||||
const CVerifiedShieldIcon = createSvgIcon(
|
||||
<path d="M11.8,13.64a1.85,1.85,0,0,1,0,.25.9.9,0,0,1,0,.18.33.33,0,0,1,0,.12.47.47,0,0,1-.09.12,1.25,1.25,0,0,1-.25.18c-.13.07-.28.13-.45.2a4.13,4.13,0,0,1-.61.16,4.35,4.35,0,0,1-.74.06,3.93,3.93,0,0,1-1.41-.24A2.91,2.91,0,0,1,7.1,14a3.32,3.32,0,0,1-.67-1.2A5.2,5.2,0,0,1,6.2,11.1a5.37,5.37,0,0,1,.25-1.72,3.85,3.85,0,0,1,.72-1.26,3,3,0,0,1,1.12-.77,3.63,3.63,0,0,1,1.42-.26,3,3,0,0,1,.61,0,2.66,2.66,0,0,1,.54.14,2.34,2.34,0,0,1,.45.19,1.84,1.84,0,0,1,.28.19l.11.13s0,.09.05.14,0,.12,0,.19a2.35,2.35,0,0,1,0,.28,2.63,2.63,0,0,1,0,.3.88.88,0,0,1,0,.2.52.52,0,0,1-.07.11.17.17,0,0,1-.1,0,.38.38,0,0,1-.22-.1c-.09-.07-.21-.14-.35-.23a3.08,3.08,0,0,0-.51-.23,2.41,2.41,0,0,0-.7-.1A1.67,1.67,0,0,0,9,8.57a1.58,1.58,0,0,0-.6.52A2.54,2.54,0,0,0,8,9.92,4.15,4.15,0,0,0,7.86,11,4.31,4.31,0,0,0,8,12.17a2.19,2.19,0,0,0,.39.81,1.55,1.55,0,0,0,.61.47,2,2,0,0,0,.82.16,2.17,2.17,0,0,0,.71-.1A2.45,2.45,0,0,0,11,13.3l.35-.21a.38.38,0,0,1,.21-.1.17.17,0,0,1,.1,0,.17.17,0,0,1,.06.09c0,.05,0,.11,0,.2A3,3,0,0,1,11.8,13.64ZM9,0,0,4v6c0,5.55,3.84,10.74,9,12,5.16-1.26,9-6.45,9-12V4Zm7,10a10.47,10.47,0,0,1-7,9.93A10.47,10.47,0,0,1,2,10V5.3L9,2.19,16,5.3Z" />,
|
||||
'VerifiedShield'
|
||||
);
|
||||
const NVerifiedShieldIcon = createSvgIcon(
|
||||
<path d="M12.13,14.25a.6.6,0,0,1-.05.24.45.45,0,0,1-.13.17.39.39,0,0,1-.19.1.52.52,0,0,1-.21,0h-.66a1.79,1.79,0,0,1-.36,0,.72.72,0,0,1-.27-.15,1.06,1.06,0,0,1-.24-.3c-.08-.12-.17-.28-.27-.47L7.87,10.29c-.11-.21-.22-.44-.34-.68s-.21-.48-.3-.71h0l0,.84c0,.28,0,.56,0,.86v4a.17.17,0,0,1,0,.1.19.19,0,0,1-.11.08.81.81,0,0,1-.21.05l-.35,0-.34,0A.81.81,0,0,1,6,14.75a.19.19,0,0,1-.11-.08.17.17,0,0,1,0-.1V7.75A.51.51,0,0,1,6,7.34a.56.56,0,0,1,.39-.14h.83a1.82,1.82,0,0,1,.37,0,.84.84,0,0,1,.27.13,1.06,1.06,0,0,1,.23.24A3.9,3.9,0,0,1,8.35,8l1.47,2.78.26.49.24.49.23.47c.07.16.15.32.22.47h0c0-.27,0-.56,0-.85s0-.58,0-.85V7.43a.17.17,0,0,1,0-.1.29.29,0,0,1,.12-.09.9.9,0,0,1,.22,0h.68a.72.72,0,0,1,.2,0s.09,0,.11.09a.17.17,0,0,1,0,.1ZM9,0,0,4v6c0,5.55,3.84,10.74,9,12,5.16-1.26,9-6.45,9-12V4Zm7,10a10.47,10.47,0,0,1-7,9.93A10.47,10.47,0,0,1,2,10V5.3L9,2.19,16,5.3Z" />,
|
||||
'VerifiedShield'
|
||||
);
|
||||
|
||||
@ -222,8 +227,9 @@ const CriticalVulnerabilityChip = () => {
|
||||
|
||||
const UnverifiedSignatureIcon = ({ signatureInfo }) => {
|
||||
return (
|
||||
<Tooltip title={<SignatureTooltip isSigned={false} signatureInfo={signatureInfo} />} placement="top">
|
||||
<Tooltip title={<SignatureTooltip signatureInfo={signatureInfo} />} placement="top">
|
||||
<UnverifiedShieldIcon
|
||||
viewBox="0 0 18 22"
|
||||
sx={{
|
||||
color: '#E53935',
|
||||
padding: '0.2rem',
|
||||
@ -237,22 +243,119 @@ const UnverifiedSignatureIcon = ({ signatureInfo }) => {
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const NotTrustedSignatureIcon = ({ signatureInfo }) => {
|
||||
return (
|
||||
<Tooltip title={<SignatureTooltip signatureInfo={signatureInfo} />} placement="top">
|
||||
{(signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.NOTATION && (
|
||||
<Badge
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
overlap="circular"
|
||||
color="warning"
|
||||
badgeContent={signatureInfo.length}
|
||||
>
|
||||
<NVerifiedShieldIcon
|
||||
viewBox="0 0 18 22"
|
||||
sx={{
|
||||
backgroundColor: '#FCE2B8!important',
|
||||
color: '#FB8C00',
|
||||
alignSelf: 'center',
|
||||
padding: '0.2rem',
|
||||
background: '#E8F5E9',
|
||||
borderRadius: '1rem',
|
||||
height: '1.5rem',
|
||||
width: '1.6rem'
|
||||
}}
|
||||
data-testid="untrusted-icon"
|
||||
/>
|
||||
</Badge>
|
||||
)) ||
|
||||
(signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.COSIGN && (
|
||||
<Badge
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
overlap="circular"
|
||||
color="warning"
|
||||
badgeContent={signatureInfo.length}
|
||||
>
|
||||
<CVerifiedShieldIcon
|
||||
viewBox="0 0 18 22"
|
||||
sx={{
|
||||
backgroundColor: '#FCE2B8!important',
|
||||
color: '#FB8C00',
|
||||
alignSelf: 'center',
|
||||
padding: '0.2rem',
|
||||
background: '#E8F5E9',
|
||||
borderRadius: '1rem',
|
||||
height: '1.5rem',
|
||||
width: '1.6rem'
|
||||
}}
|
||||
data-testid="untrusted-icon"
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const VerifiedSignatureIcon = ({ signatureInfo }) => {
|
||||
return (
|
||||
<Tooltip title={<SignatureTooltip isSigned={true} signatureInfo={signatureInfo} />} placement="top">
|
||||
<VerifiedShieldIcon
|
||||
viewBox="0 0 24 24"
|
||||
sx={{
|
||||
color: '#43A047',
|
||||
alignSelf: 'center',
|
||||
padding: '0.2rem',
|
||||
background: '#E8F5E9',
|
||||
borderRadius: '1rem',
|
||||
height: '1.5rem',
|
||||
width: '1.6rem'
|
||||
}}
|
||||
data-testid="verified-icon"
|
||||
/>
|
||||
<Tooltip title={<SignatureTooltip signatureInfo={signatureInfo} />} placement="top">
|
||||
{(signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.NOTATION && (
|
||||
<Badge
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
overlap="circular"
|
||||
color="success"
|
||||
badgeContent={signatureInfo.length}
|
||||
>
|
||||
<NVerifiedShieldIcon
|
||||
viewBox="0 0 18 22"
|
||||
sx={{
|
||||
color: '#43A047',
|
||||
alignSelf: 'center',
|
||||
padding: '0.2rem',
|
||||
background: '#E8F5E9',
|
||||
borderRadius: '1rem',
|
||||
height: '1.5rem',
|
||||
width: '1.6rem'
|
||||
}}
|
||||
data-testid="verified-icon"
|
||||
/>
|
||||
</Badge>
|
||||
)) ||
|
||||
(signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.COSIGN && (
|
||||
<Badge
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
overlap="circular"
|
||||
color="success"
|
||||
badgeContent={signatureInfo.length}
|
||||
>
|
||||
<CVerifiedShieldIcon
|
||||
viewBox="0 0 18 22"
|
||||
sx={{
|
||||
color: '#43A047',
|
||||
alignSelf: 'center',
|
||||
padding: '0.2rem',
|
||||
background: '#E8F5E9',
|
||||
borderRadius: '1rem',
|
||||
height: '1.5rem',
|
||||
width: '1.6rem'
|
||||
}}
|
||||
data-testid="verified-icon"
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@ -270,19 +373,6 @@ const UnverifiedSignatureChip = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
const VerifiedSignatureChip = () => {
|
||||
return (
|
||||
<Chip
|
||||
label="Verified Signature"
|
||||
sx={{ backgroundColor: '#E8F5E9', color: '#388E3C', fontSize: '0.8125rem' }}
|
||||
variant="filled"
|
||||
onDelete={() => {
|
||||
return;
|
||||
}}
|
||||
deleteIcon={<VerifiedShieldIcon sx={{ color: '#388E3C!important' }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
NoneVulnerabilityIcon,
|
||||
@ -298,9 +388,9 @@ export {
|
||||
HighVulnerabilityChip,
|
||||
CriticalVulnerabilityChip,
|
||||
UnverifiedSignatureIcon,
|
||||
NotTrustedSignatureIcon,
|
||||
VerifiedSignatureIcon,
|
||||
UnverifiedSignatureChip,
|
||||
VerifiedSignatureChip,
|
||||
FailedScanIcon,
|
||||
FailedScanChip
|
||||
};
|
||||
|
@ -222,8 +222,8 @@ fi
|
||||
|
||||
trivy_out_file=trivy-${image}-${tag}.json
|
||||
if [ ! -z "${multiarch}" ]; then
|
||||
trivy image --scanners vuln --format json --input ${local_image_ref_trivy} -o ${trivy_out_file}
|
||||
jq -n --argfile trivy_file ${trivy_out_file} '.trivy=$trivy_file.Results' > ${trivy_out_file}.tmp
|
||||
trivy image --scanners vuln --db-repository ghcr.io/project-zot/trivy-db --format json --input ${local_image_ref_trivy} -o ${trivy_out_file}
|
||||
jq -n --slurpfile trivy_file ${trivy_out_file} '.trivy=$trivy_file[0].Results' > ${trivy_out_file}.tmp
|
||||
mv ${trivy_out_file}.tmp ${trivy_out_file}
|
||||
else
|
||||
echo '{"trivy":[]}' > ${trivy_out_file}
|
||||
|
@ -29,7 +29,7 @@ test.describe('Tag page test', () => {
|
||||
await expect(page.getByTestId('layer-card-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
||||
await page.getByRole('tab', { name: 'Uses' }).click();
|
||||
await expect(page.getByTestId('depends-on-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
||||
await expect(page.getByText('Tag')).toHaveCount(1, { timeout: 100000 });
|
||||
await expect(await page.getByText('Tag').count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Tag page with vulnerabilities', async ({ page }) => {
|
||||
@ -37,8 +37,8 @@ test.describe('Tag page test', () => {
|
||||
await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`);
|
||||
await page.getByRole('tab', { name: 'Vulnerabilities' }).click();
|
||||
await expect(page.getByTestId('vulnerability-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
||||
await expect(page.getByText('CVE-').nth(0)).toBeVisible({ timeout: 100000 });
|
||||
await expect(await page.getByText('CVE-').count()).toBeGreaterThan(0);
|
||||
await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
|
||||
await expect(page.getByText(/CVE-/).nth(0)).toBeVisible({ timeout: 100000 });
|
||||
await expect(await page.getByText(/CVE-/).count()).toBeGreaterThan(0);
|
||||
await expect(await page.getByText(/CVE-/).count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
|
||||
});
|
||||
});
|
||||
|
@ -149,7 +149,7 @@ const getRepoListOrderedAlpha = () => {
|
||||
// };
|
||||
|
||||
const getRepoCardNameForLocator = (repo) => {
|
||||
return `${repo?.repo} ${repo?.tags[0]?.description?.slice(0, 10)}`;
|
||||
return new RegExp(`${repo?.repo} \\d${repo?.tags[0]?.description?.slice(0, 10)}`);
|
||||
};
|
||||
|
||||
export {
|
||||
|
@ -25,7 +25,7 @@ const endpoints = {
|
||||
10 * (pageNumber - 1)
|
||||
}%20sortBy:%20${sortCriteria}}%20)%20{Page%20{TotalCount%20ItemCount}%20Repos%20{Name%20LastUpdated%20Size%20Platforms%20{%20Os%20Arch%20}%20IsStarred%20IsBookmarked%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20IsSigned%20SignatureInfo%20{%20Tool%20IsTrusted%20Author%20}%20Licenses%20Vendor%20Labels%20}%20StarCount%20DownloadCount}}}`,
|
||||
image: (name) =>
|
||||
`/v2/_zot/ext/search?query={Image(image:%20%22${name}%22){RepoName%20IsSigned%20SignatureInfo%20{%20Tool%20IsTrusted%20Author%20}%20Vulnerabilities%20{MaxSeverity%20Count}%20%20Referrers%20{MediaType%20ArtifactType%20Size%20Digest%20Annotations{Key%20Value}}%20Tag%20Manifests%20{History%20{Layer%20{Size%20Digest}%20HistoryDescription%20{CreatedBy%20EmptyLayer}}%20Digest%20ConfigDigest%20LastUpdated%20Size%20Platform%20{Os%20Arch}}%20Vendor%20Licenses%20}}`
|
||||
`/v2/_zot/ext/search?query={Image(image:%20%22${name}%22){RepoName%20IsSigned%20SignatureInfo%20{%20Tool%20IsTrusted%20Author%20}%20Vulnerabilities%20{MaxSeverity%20Count}%20%20Referrers%20{MediaType%20ArtifactType%20Size%20Digest%20Annotations{Key%20Value}}%20Tag%20Manifests%20{History%20{Layer%20{Size%20Digest}%20HistoryDescription%20{CreatedBy%20EmptyLayer}}%20Digest%20ConfigDigest%20LastUpdated%20Size%20Platform%20{Os%20Arch}%20Referrers%20{MediaType%20ArtifactType%20Size%20Digest%20Annotations{Key%20Value}}}%20Vendor%20Licenses%20}}`
|
||||
};
|
||||
|
||||
export { hosts, endpoints, sortCriteria, pageSizes };
|
||||
|
Reference in New Issue
Block a user