Compare commits
70 Commits
commit-4db
...
patches
Author | SHA1 | Date | |
---|---|---|---|
3dffc1e87b | |||
0fc4aef84e | |||
fd069b2a9b | |||
348af7882f | |||
7ed5c9b7cb | |||
21a1878539 | |||
20f5cfcb74 | |||
1846f463b5 | |||
b5ba403c8c | |||
b4d248f2ff | |||
9de2337809 | |||
c4d595c782 | |||
09ab4474e9 | |||
177406df41 | |||
e2367c2a33 | |||
33524ce3cc | |||
e037c6c577 | |||
c268991495 | |||
0edfe0f73a | |||
f4a6030d93 | |||
9358539e0c | |||
5bf7d5652c | |||
12f9229320 | |||
df19fa811c | |||
6cda89c710 | |||
12b474e126 | |||
a9db66bd34 | |||
f4600b8b79 | |||
c375c0697a | |||
2e1e2e92b7 | |||
d9370fb9c1 | |||
e97e04eee5 | |||
a288523a3f | |||
fad5572db4 | |||
19e366ee1f | |||
b41fb2f841 | |||
b787273b84 | |||
9ecd46e4d0 | |||
845726cd08 | |||
ac84c375c0 | |||
96008d67be | |||
087b42693f | |||
8f4c23bf40 | |||
54c764c996 | |||
44289c751f | |||
8086f6880d | |||
a55248774c | |||
936590d822 | |||
05d5f744b0 | |||
769ffdc60d | |||
70a870a616 | |||
c09a12facc | |||
415973e23c | |||
cb2d8795f5 | |||
ac9d023272 | |||
6a2fc8d867 | |||
ba73af24b3 | |||
c1a51afede | |||
ecd584c4e2 | |||
e0d4417bf7 | |||
63ff8dabc0 | |||
f9cafd0b90 | |||
089d79087f | |||
2f94cc30ae | |||
ddf1d9224b | |||
7471fb58a8 | |||
2b3058fb14 | |||
ecff33fe01 | |||
60ca6d21d5 | |||
9029b97b47 |
8
.github/workflows/dco.yml
vendored
@ -2,16 +2,18 @@
|
||||
name: DCO
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.x
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Check DCO
|
||||
|
142
.github/workflows/end-to-end-test.yml
vendored
Normal file
@ -0,0 +1,142 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
name: end-to-end-test
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Test zui/zot integration
|
||||
env:
|
||||
CI: ""
|
||||
REGISTRY_HOST: "localhost"
|
||||
REGISTRY_PORT: "8080"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Cleanup disk space
|
||||
run: |
|
||||
# To free up ~15 GB of disk space
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
|
||||
- name: Checkout zui repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: 'npm'
|
||||
|
||||
- name: Build zui
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
make install
|
||||
make build
|
||||
|
||||
- name: Install container image tooling
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgpgme-dev libassuan-dev libbtrfs-dev libdevmapper-dev pkg-config rpm snapd jq
|
||||
git clone https://github.com/containers/skopeo -b v1.9.0 $GITHUB_WORKSPACE/src/github.com/containers/skopeo
|
||||
cd $GITHUB_WORKSPACE/src/github.com/containers/skopeo && make bin/skopeo
|
||||
chmod +x bin/skopeo
|
||||
sudo mv bin/skopeo /usr/local/bin/skopeo
|
||||
which skopeo
|
||||
skopeo -v
|
||||
curl -L https://github.com/regclient/regclient/releases/download/v0.4.7/regctl-linux-amd64 -o regctl
|
||||
chmod +x regctl
|
||||
sudo mv regctl /usr/local/bin/regctl
|
||||
which regctl
|
||||
regctl version
|
||||
curl -L https://github.com/sigstore/cosign/releases/download/v1.13.0/cosign-linux-amd64 -o cosign
|
||||
chmod +x cosign
|
||||
sudo mv cosign /usr/local/bin/cosign
|
||||
which cosign
|
||||
cosign version
|
||||
pushd $(mktemp -d)
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.38.3/trivy_0.38.3_Linux-64bit.tar.gz -o trivy.tar.gz
|
||||
tar -xzvf trivy.tar.gz
|
||||
sudo mv trivy /usr/local/bin/trivy
|
||||
popd
|
||||
which trivy
|
||||
trivy version
|
||||
cd $GITHUB_WORKSPACE
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
|
||||
- name: Checkout zot repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
repository: project-zot/zot
|
||||
ref: main
|
||||
path: zot
|
||||
|
||||
- name: Build zot
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/zot
|
||||
make binary ZUI_BUILD_PATH=$GITHUB_WORKSPACE/build
|
||||
ls -l bin/
|
||||
|
||||
- name: Bringup zot server
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/zot
|
||||
mkdir /tmp/zot
|
||||
./bin/zot-linux-amd64 serve examples/config-ui.json &
|
||||
while true; do x=0; curl -f http://$REGISTRY_HOST:$REGISTRY_PORT/v2/ || x=1; if [ $x -eq 0 ]; then break; fi; sleep 1; done
|
||||
|
||||
- name: Load image test data from cache into a local folder
|
||||
id: restore-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: tests/data/images
|
||||
key: image-config-${{ hashFiles('**/tests/data/config.yaml') }}
|
||||
restore-keys: |
|
||||
image-config-
|
||||
|
||||
- name: Load image test data into zot server
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
regctl registry set --tls disabled $REGISTRY_HOST:$REGISTRY_PORT
|
||||
make test-data REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT
|
||||
|
||||
- name: Install playwright dependencies
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
make playwright-browsers
|
||||
|
||||
- name: Trigger CVE scanning
|
||||
run: |
|
||||
# trigger CVE scanning for all images before running the tests
|
||||
curl -X POST -H "Content-Type: application/json" -m 600 --data '{ "query": "{ ImageListForCVE (id:\"CVE-2021-43616\") { Results { RepoName Tag } } }" }' http://$REGISTRY_HOST:$REGISTRY_PORT/v2/_zot/ext/search
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
make integration-tests REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT
|
||||
|
||||
- name: Upload playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
6
.gitignore
vendored
@ -8,6 +8,7 @@
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/tests/data/
|
||||
|
||||
# production
|
||||
/build
|
||||
@ -128,3 +129,8 @@ dist
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
data.md
|
||||
|
20
Makefile
@ -1,3 +1,6 @@
|
||||
REGISTRY_HOST ?= localhost
|
||||
REGISTRY_PORT ?= 8080
|
||||
|
||||
.PHONY: all
|
||||
all: install audit build
|
||||
|
||||
@ -20,3 +23,20 @@ audit:
|
||||
.PHONY: run
|
||||
run:
|
||||
npm start
|
||||
|
||||
.PHONY: test-data
|
||||
test-data:
|
||||
./tests/scripts/load_test_data.py \
|
||||
--registry $(REGISTRY_HOST):$(REGISTRY_PORT) \
|
||||
--data-dir tests/data \
|
||||
--config-file tests/data/config.yaml \
|
||||
--metadata-file tests/data/image_metadata.json \
|
||||
-d
|
||||
|
||||
.PHONY: playwright-browsers
|
||||
playwright-browsers:
|
||||
npx playwright install --with-deps
|
||||
|
||||
.PHONY: integration-tests
|
||||
integration-tests: # Triggering the tests TBD
|
||||
UI_HOST=$(REGISTRY_HOST):$(REGISTRY_PORT) API_HOST=$(REGISTRY_HOST):$(REGISTRY_PORT) npm run test:ui
|
||||
|
29077
package-lock.json
generated
15
package.json
@ -5,27 +5,30 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.9.3",
|
||||
"@emotion/styled": "^11.9.3",
|
||||
"@mui-treasury/styles": "^1.13.1",
|
||||
"@mui/icons-material": "^5.2.5",
|
||||
"@mui/lab": "^5.0.0-alpha.89",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@mui/styles": "^5.8.6",
|
||||
"@mui/x-date-pickers": "^6.18.4",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.24.0",
|
||||
"downshift": "^6.1.12",
|
||||
"export-from-json": "^1.7.3",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.5.2",
|
||||
"luxon": "^3.4.4",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
"nth-check": "^2.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-sticky-el": "^2.0.9",
|
||||
"web-vitals": "^2.1.3"
|
||||
"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",
|
||||
@ -38,6 +41,10 @@
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --detectOpenHandles",
|
||||
"test:coverage": "react-scripts test --detectOpenHandles --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:fix": "npm run lint -- --fix",
|
||||
"format": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc",
|
||||
|
114
playwright.config.js
Normal file
@ -0,0 +1,114 @@
|
||||
// @ts-check
|
||||
const { devices } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
* @type {import('@playwright/test').PlaywrightTestConfig}
|
||||
*/
|
||||
const config = {
|
||||
testDir: './tests',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 50 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 15000
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: 2,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 2,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'only-on-failure'
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
module.exports = config;
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 1.4 KiB |
@ -6,20 +6,19 @@
|
||||
<meta http-equiv="Content-Security-Policy" content="
|
||||
default-src 'none';
|
||||
script-src 'self' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/;
|
||||
font-src 'self' https://fonts.googleapis.com/ https://fonts.gstatic.com/;
|
||||
style-src 'self' 'unsafe-inline';
|
||||
font-src 'self';
|
||||
connect-src *;
|
||||
img-src 'self';
|
||||
manifest-src 'self';
|
||||
base-uri 'self'"
|
||||
>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="zot OCI-native Container Image Registry"
|
||||
content="ALT Linux OCI-native Container Image Registry"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
@ -36,7 +35,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>zot OCI-native Container Image Registry</title>
|
||||
<title>ALT Linux OCI-native Container Image Registry</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
@ -1,6 +1,7 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
height: 100vh;
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
@ -26,7 +27,7 @@
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -76,4 +77,10 @@
|
||||
.hide-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
.hide-on-small {
|
||||
display:none
|
||||
}
|
||||
}
|
45
src/App.js
@ -1,39 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import HomePage from './pages/HomePage.jsx';
|
||||
import LoginPage from './pages/LoginPage.jsx';
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
|
||||
import { isApiKeyEnabled } from 'utilities/authUtilities';
|
||||
|
||||
import HomePage from './pages/HomePage';
|
||||
import RepoPage from 'pages/RepoPage';
|
||||
import TagPage from 'pages/TagPage';
|
||||
import ExplorePage from 'pages/ExplorePage';
|
||||
import UserManagementPage from 'pages/UserManagementPage';
|
||||
|
||||
import './App.css';
|
||||
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthWrapper } from 'utilities/AuthWrapper.jsx';
|
||||
import RepoPage from 'pages/RepoPage.jsx';
|
||||
import TagPage from 'pages/TagPage';
|
||||
import ExplorePage from 'pages/ExplorePage.jsx';
|
||||
|
||||
function App() {
|
||||
const isToken = () => {
|
||||
const localStorageToken = localStorage.getItem('token');
|
||||
return localStorageToken ? true : false;
|
||||
};
|
||||
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(isToken());
|
||||
|
||||
return (
|
||||
<div className="App" data-testid="app-container">
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route element={<AuthWrapper isLoggedIn={isLoggedIn} hasHeader redirect="/login" />}>
|
||||
<Route path="/" element={<Navigate to="/home" />} />
|
||||
<Route path="/home" element={<HomePage />} />
|
||||
<Route path="/explore" element={<ExplorePage />} />
|
||||
<Route path="/image/:name" element={<RepoPage />} />
|
||||
<Route path="/image/:reponame/tag/:tag" element={<TagPage />} />
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Route>
|
||||
<Route element={<AuthWrapper isLoggedIn={!isLoggedIn} redirect="/" />}>
|
||||
<Route path="/login" element={<LoginPage isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} />} />
|
||||
<Route path="*" element={<Navigate to="/login" />} />
|
||||
</Route>
|
||||
<Route path="/" element={<Navigate to="/home" />} />
|
||||
<Route path="/home" element={<HomePage />} />
|
||||
<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" />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</div>
|
||||
|
@ -1,8 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import App from './App';
|
||||
import MockThemeProvider from './__mocks__/MockThemeProvider';
|
||||
|
||||
it('renders the app component', () => {
|
||||
render(<App />);
|
||||
render(
|
||||
<MockThemeProvider>
|
||||
<App />
|
||||
</MockThemeProvider>
|
||||
);
|
||||
expect(screen.getByTestId('app-container')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -3,8 +3,8 @@ import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme();
|
||||
|
||||
function MockThemeProvier({ children }) {
|
||||
function MockThemeProvider({ children }) {
|
||||
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||
}
|
||||
|
||||
export default MockThemeProvier;
|
||||
export default MockThemeProvider;
|
||||
|
@ -1,4 +1,4 @@
|
||||
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';
|
||||
@ -6,7 +6,7 @@ import React from 'react';
|
||||
import { createSearchParams, MemoryRouter } from 'react-router-dom';
|
||||
import filterConstants from 'utilities/filterConstants.js';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria.js';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// router mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@ -18,11 +18,11 @@ jest.mock('react-router-dom', () => ({
|
||||
const StateExploreWrapper = (props) => {
|
||||
const queryString = props.search || '';
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<MemoryRouter initialEntries={[`/explore?${queryString.toString()}`]}>
|
||||
<Explore />
|
||||
</MemoryRouter>
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
const mockImageList = {
|
||||
@ -33,10 +33,12 @@ const mockImageList = {
|
||||
Name: 'alpine',
|
||||
Size: '2806985',
|
||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
IsSigned: false,
|
||||
SignatureInfo: [],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -44,16 +46,35 @@ const mockImageList = {
|
||||
MaxSeverity: 'LOW',
|
||||
Count: 7
|
||||
}
|
||||
}
|
||||
},
|
||||
Platforms: [
|
||||
{
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Name: 'mongo',
|
||||
Size: '231383863',
|
||||
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -61,16 +82,35 @@ const mockImageList = {
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
Platforms: [
|
||||
{
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Name: 'node',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -78,16 +118,35 @@ const mockImageList = {
|
||||
MaxSeverity: 'CRITICAL',
|
||||
Count: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
Platforms: [
|
||||
{
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Name: 'centos',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -95,16 +154,35 @@ const mockImageList = {
|
||||
MaxSeverity: 'NONE',
|
||||
Count: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
Platforms: [
|
||||
{
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Name: 'debian',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -112,16 +190,39 @@ const mockImageList = {
|
||||
MaxSeverity: 'MEDIUM',
|
||||
Count: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
Platforms: [
|
||||
{
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
},
|
||||
{
|
||||
Os: 'windows',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Name: 'mysql',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -129,16 +230,35 @@ const mockImageList = {
|
||||
MaxSeverity: 'UNKNOWN',
|
||||
Count: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
Platforms: [
|
||||
{
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Name: 'base',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: 'author1'
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: 'author2'
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
@ -146,12 +266,40 @@ const mockImageList = {
|
||||
MaxSeverity: '',
|
||||
Count: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
Platforms: [
|
||||
{
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const filteredMockImageListWindows = () => {
|
||||
const filteredRepos = mockImageList.GlobalSearch.Repos.filter((r) =>
|
||||
r.Platforms.map((pf) => pf.Os).includes('windows')
|
||||
);
|
||||
return {
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 1, ItemCount: 1 },
|
||||
Repos: filteredRepos
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const filteredMockImageListSigned = () => {
|
||||
const filteredRepos = mockImageList.GlobalSearch.Repos.filter((r) => r.NewestImage.SignatureInfo?.length > 0);
|
||||
return {
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 6, ItemCount: 6 },
|
||||
Repos: filteredRepos
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// IntersectionObserver isn't available in test environment
|
||||
const mockIntersectionObserver = jest.fn();
|
||||
@ -161,6 +309,10 @@ beforeEach(() => {
|
||||
disconnect: () => null
|
||||
});
|
||||
window.IntersectionObserver = mockIntersectionObserver;
|
||||
Object.defineProperty(window.document, 'cookie', {
|
||||
writable: true,
|
||||
value: 'user=test'
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -187,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 () => {
|
||||
@ -235,4 +402,37 @@ describe('Explore component', () => {
|
||||
const filterCheckboxes = await screen.findAllByRole('checkbox');
|
||||
expect(filterCheckboxes[0]).toBeChecked();
|
||||
});
|
||||
|
||||
it('should filter the images based on filter cards', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
render(<StateExploreWrapper />);
|
||||
expect(await screen.findAllByTestId('repo-card')).toHaveLength(mockImageList.GlobalSearch.Repos.length);
|
||||
const windowsCheckbox = (await screen.findAllByRole('checkbox'))[0];
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: filteredMockImageListWindows() } });
|
||||
await userEvent.click(windowsCheckbox);
|
||||
expect(windowsCheckbox).toBeChecked();
|
||||
expect(await screen.findAllByTestId('repo-card')).toHaveLength(1);
|
||||
const signedCheckboxLabel = await screen.findByText(/signed images/i);
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: filteredMockImageListSigned() } });
|
||||
await userEvent.click(signedCheckboxLabel);
|
||||
expect(await screen.findAllByTestId('repo-card')).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('should bookmark a repo if bookmark button is clicked', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
render(<StateExploreWrapper />);
|
||||
const bookmarkButton = (await screen.findAllByTestId('bookmark-button'))[0];
|
||||
jest.spyOn(api, 'put').mockResolvedValueOnce({ status: 200, data: {} });
|
||||
await userEvent.click(bookmarkButton);
|
||||
expect(await screen.findAllByTestId('bookmarked')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should star a repo if star button is clicked', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
render(<StateExploreWrapper />);
|
||||
const starButton = (await screen.findAllByTestId('star-button'))[0];
|
||||
jest.spyOn(api, 'put').mockResolvedValueOnce({ status: 200, data: {} });
|
||||
await userEvent.click(starButton);
|
||||
expect(await screen.findAllByTestId('starred')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
@ -11,6 +11,14 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the explore page component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
@ -2,18 +2,26 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import FilterCard from 'components/Shared/FilterCard';
|
||||
import React, { useState } from 'react';
|
||||
import filterConstants from 'utilities/filterConstants';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const StateFilterCardWrapper = () => {
|
||||
const [filters, setFilters] = useState([]);
|
||||
return (
|
||||
<FilterCard title="Products" filters={filterConstants.osFilters} updateFilters={setFilters} filterValue={filters} />
|
||||
<MockThemeProvider>
|
||||
<FilterCard
|
||||
title="Operating System"
|
||||
filters={filterConstants.osFilters}
|
||||
updateFilters={setFilters}
|
||||
filterValue={filters}
|
||||
/>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Filters components', () => {
|
||||
it('renders the filters cards', async () => {
|
||||
render(<StateFilterCardWrapper />);
|
||||
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
|
||||
expect(screen.getAllByRole('checkbox')).toHaveLength(3);
|
||||
|
||||
const checkbox = screen.getAllByRole('checkbox');
|
||||
expect(checkbox[0]).not.toBeChecked();
|
||||
|
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-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
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();
|
||||
});
|
||||
});
|
@ -4,6 +4,7 @@ import Home from 'components/Home/Home';
|
||||
import React from 'react';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@ -12,9 +13,152 @@ jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
const HomeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvider>
|
||||
<Home />
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const mockImageList = {
|
||||
RepoListWithNewestImage: {
|
||||
Results: [
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 6, ItemCount: 3 },
|
||||
Repos: [
|
||||
{
|
||||
Name: 'alpine',
|
||||
Size: '2806985',
|
||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
SignatureInfo: [],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'LOW',
|
||||
Count: 7
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'mongo',
|
||||
Size: '231383863',
|
||||
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'node',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'CRITICAL',
|
||||
Count: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const mockImageListRecent = {
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 6, ItemCount: 2 },
|
||||
Repos: [
|
||||
{
|
||||
Name: 'alpine',
|
||||
Size: '2806985',
|
||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
SignatureInfo: [],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'LOW',
|
||||
Count: 7
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'mongo',
|
||||
Size: '231383863',
|
||||
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const mockImageListBookmarks = {
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 3, ItemCount: 2 },
|
||||
Repos: [
|
||||
{
|
||||
Name: 'alpine',
|
||||
Size: '2806985',
|
||||
@ -48,28 +192,36 @@ const mockImageList = {
|
||||
Count: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const mockImageListStars = {
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 3, ItemCount: 2 },
|
||||
Repos: [
|
||||
{
|
||||
Name: 'node',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
Name: 'alpine',
|
||||
Size: '2806985',
|
||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
Description: 'w',
|
||||
IsSigned: false,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'CRITICAL',
|
||||
Count: 10
|
||||
MaxSeverity: 'LOW',
|
||||
Count: 7
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'centos',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
Name: 'mongo',
|
||||
Size: '231383863',
|
||||
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
@ -78,42 +230,8 @@ const mockImageList = {
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'NONE',
|
||||
Count: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'debian',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'MEDIUM',
|
||||
Count: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'mysql',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'UNKNOWN',
|
||||
Count: 10
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,40 +250,47 @@ afterEach(() => {
|
||||
|
||||
describe('Home component', () => {
|
||||
it('fetches image data and renders popular, bookmarks and recently updated', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<Home />);
|
||||
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(2));
|
||||
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(2));
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<HomeWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(4));
|
||||
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(4));
|
||||
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
|
||||
});
|
||||
|
||||
it('renders signature icons', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<Home />);
|
||||
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(2);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(3);
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
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(10);
|
||||
});
|
||||
|
||||
it('renders vulnerability icons', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<Home />);
|
||||
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(2);
|
||||
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(2);
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<HomeWrapper />);
|
||||
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(4);
|
||||
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(4);
|
||||
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should log an error when data can't be fetched", async () => {
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<Home />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
render(<HomeWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(4));
|
||||
});
|
||||
|
||||
it('should redirect to explore page when clicking view all popular', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<Home />);
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListBookmarks } });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListStars } });
|
||||
render(<HomeWrapper />);
|
||||
const viewAllButtons = await screen.findAllByText(/view all/i);
|
||||
expect(viewAllButtons).toHaveLength(2);
|
||||
expect(viewAllButtons).toHaveLength(4);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: [] } });
|
||||
fireEvent.click(viewAllButtons[0]);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
pathname: `/explore`,
|
||||
@ -176,5 +301,15 @@ describe('Home component', () => {
|
||||
pathname: `/explore`,
|
||||
search: createSearchParams({ sortby: sortByCriteria.updateTime.value }).toString()
|
||||
});
|
||||
fireEvent.click(viewAllButtons[2]);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
pathname: `/explore`,
|
||||
search: createSearchParams({ filter: 'IsBookmarked' }).toString()
|
||||
});
|
||||
fireEvent.click(viewAllButtons[3]);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
pathname: `/explore`,
|
||||
search: createSearchParams({ filter: 'IsStarred' }).toString()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11,6 +11,14 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the homepage component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
@ -2,19 +2,22 @@ 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 MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
it('renders the signin presentation component and signin components if auth enabled', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<LoginPage isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<LoginPage isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
expect(screen.getByTestId('login-container')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('presentation-container')).toBeInTheDocument();
|
||||
|
@ -4,6 +4,12 @@ import SignIn from 'components/Login/SignIn';
|
||||
import { api } from '../../api';
|
||||
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: {}, openid: { providers: { github: {} } } } }
|
||||
};
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
@ -24,7 +30,7 @@ describe('Signin component automatic navigation', () => {
|
||||
|
||||
it('navigates to homepage when auth is disabled', async () => {
|
||||
// mock request to check auth
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: {} });
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { http: {} } });
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
|
||||
@ -35,7 +41,10 @@ describe('Signin component automatic navigation', () => {
|
||||
describe('Sign in form', () => {
|
||||
beforeEach(() => {
|
||||
// mock auth check request
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} });
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 401,
|
||||
data: mockMgmtResponse
|
||||
});
|
||||
});
|
||||
|
||||
it('should change username and password values on user input', async () => {
|
||||
@ -46,6 +55,7 @@ describe('Sign in form', () => {
|
||||
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 () => {
|
||||
@ -77,7 +87,7 @@ describe('Sign in form', () => {
|
||||
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();
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} });
|
||||
fireEvent.click(submitButton);
|
||||
const errorDisplay = await screen.findByText(/Authentication Failed/i);
|
||||
await waitFor(() => {
|
||||
|
@ -3,13 +3,14 @@ import RepoDetails from 'components/Repo/RepoDetails';
|
||||
import React from 'react';
|
||||
import { api } from 'api';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const RepoDetailsThemeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<RepoDetails />
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -45,9 +46,23 @@ const mockRepoDetailsData = {
|
||||
LastUpdated: '2023-01-30T15:05:35.420124619Z',
|
||||
Size: '451554070',
|
||||
Vendors: ['[The Node.js Docker Team](https://github.com/nodejs/docker-node)\n'],
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: 'author1'
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: 'author2'
|
||||
}
|
||||
],
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'CRITICAL',
|
||||
Count: 15
|
||||
@ -232,6 +247,13 @@ const mockRepoDetailsHigh = {
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window.document, 'cookie', {
|
||||
writable: true,
|
||||
value: 'user=test'
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
@ -248,7 +270,7 @@ describe('Repo details component', () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsWithMissingData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findByText('test')).toBeInTheDocument();
|
||||
expect(await screen.findByText(/timestamp n\/a/i)).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(/timestamp n\/a/i)).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders vulnerability icons', async () => {
|
||||
@ -275,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(() => {});
|
||||
@ -288,15 +324,6 @@ describe('Repo details component', () => {
|
||||
await waitFor(() => expect(mockUseNavigate).toBeCalledWith('/home'));
|
||||
});
|
||||
|
||||
it('should switch between tabs', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findByTestId('overview-container')).toBeInTheDocument();
|
||||
fireEvent.click(await screen.findByText(/tags/i));
|
||||
expect(await screen.findByTestId('tags-container')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('overview-container')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render platform chips and they should redirect to explore page', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
@ -307,4 +334,22 @@ describe('Repo details component', () => {
|
||||
search: createSearchParams({ filter: 'linux' }).toString()
|
||||
});
|
||||
});
|
||||
|
||||
it('should bookmark a repo if bookmark button is clicked', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockRepoDetailsData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
const bookmarkButton = await screen.findByTestId('bookmark-button');
|
||||
jest.spyOn(api, 'put').mockResolvedValue({ status: 200, data: {} });
|
||||
await userEvent.click(bookmarkButton);
|
||||
expect(await screen.findByTestId('bookmarked')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should star a repo if star button is clicked', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockRepoDetailsData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
const starButton = await screen.findByTestId('star-button');
|
||||
jest.spyOn(api, 'put').mockResolvedValue({ status: 200, data: {} });
|
||||
await userEvent.click(starButton);
|
||||
expect(await screen.findByTestId('starred')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ 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 MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
@ -27,18 +27,13 @@ afterEach(() => {
|
||||
|
||||
it('renders the repository page component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<MockThemeProvier>
|
||||
<RepoPage />
|
||||
</MockThemeProvier>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<RepoPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
expect(screen.getByTestId('repo-container')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -2,6 +2,15 @@ import { fireEvent, waitFor, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Tags from 'components/Repo/Tabs/Tags';
|
||||
import React from 'react';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const TagsThemeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvider>
|
||||
<Tags tags={mockedTagsData} />
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
@ -11,62 +20,102 @@ jest.mock('react-router-dom', () => ({
|
||||
|
||||
const mockedTagsData = [
|
||||
{
|
||||
Digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
Tag: 'latest',
|
||||
LastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
Vendor: 'test1',
|
||||
Size: '569130088',
|
||||
Platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
tag: 'latest',
|
||||
vendor: 'test1',
|
||||
isDeletable: true,
|
||||
manifests: [
|
||||
{
|
||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
size: '569130088',
|
||||
platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
Tag: 'bullseye',
|
||||
LastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
Vendor: 'test1',
|
||||
Size: '569130088',
|
||||
Platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
tag: 'bullseye',
|
||||
vendor: 'test1',
|
||||
isDeletable: true,
|
||||
manifests: [
|
||||
{
|
||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
size: '569130088',
|
||||
platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
Tag: '1.5.2',
|
||||
LastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
Vendor: 'test1',
|
||||
Size: '569130088',
|
||||
Platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
tag: '1.5.2',
|
||||
vendor: 'test1',
|
||||
isDeletable: true,
|
||||
manifests: [
|
||||
{
|
||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
size: '569130088',
|
||||
platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
describe('Tags component', () => {
|
||||
it('should open and close details dropdown for tags', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
const openBtn = screen.getAllByText(/digest/i);
|
||||
render(<TagsThemeWrapper />);
|
||||
const openBtn = screen.getAllByText(/show/i);
|
||||
fireEvent.click(openBtn[0]);
|
||||
expect(screen.getByText(/OS\/ARCH/i)).toBeInTheDocument();
|
||||
fireEvent.click(openBtn[0]);
|
||||
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 navigate to tag page details when tag is clicked', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
render(<TagsThemeWrapper />);
|
||||
const tagLink = await screen.findByText('latest');
|
||||
fireEvent.click(tagLink);
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest');
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest', { state: { digest: null } });
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to specific manifest when clicking the digest', async () => {
|
||||
render(<TagsThemeWrapper />);
|
||||
const openBtn = screen.getAllByText(/show/i);
|
||||
await fireEvent.click(openBtn[0]);
|
||||
const tagLink = await screen.findByText(/sha256:adca4/i);
|
||||
fireEvent.click(tagLink);
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest', {
|
||||
state: { digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter tag list based on user input', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
const tagFilterInput = await screen.findByPlaceholderText(/Search for Tags/i);
|
||||
render(<TagsThemeWrapper />);
|
||||
const tagFilterInput = await screen.findByPlaceholderText(/Search Tags/i);
|
||||
expect(await screen.findByText(/latest/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/bullseye/i)).toBeInTheDocument();
|
||||
userEvent.type(tagFilterInput, 'bull');
|
||||
@ -75,7 +124,7 @@ describe('Tags component', () => {
|
||||
});
|
||||
|
||||
it('should sort tags based on the picked sort criteria', async () => {
|
||||
render(<Tags tags={mockedTagsData} />);
|
||||
render(<TagsThemeWrapper />);
|
||||
const selectFilter = await screen.findByText('Newest');
|
||||
expect(selectFilter).toBeInTheDocument();
|
||||
userEvent.click(selectFilter);
|
||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import RepoCard from 'components/Shared/RepoCard';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// usenavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@ -21,9 +22,51 @@ const mockImage = {
|
||||
vendor: '',
|
||||
size: '585',
|
||||
tags: '',
|
||||
isSigned: true,
|
||||
signatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
platforms: [{ Os: 'linux', Arch: 'amd64' }]
|
||||
};
|
||||
|
||||
const RepoCardWrapper = (props) => {
|
||||
const { image } = props;
|
||||
return (
|
||||
<MockThemeProvider>
|
||||
<RepoCard
|
||||
name={image.name}
|
||||
version={image.latestVersion}
|
||||
description={image.description}
|
||||
vendor={image.vendor}
|
||||
isSigned={image.isSigned}
|
||||
signatureInfo={image.signatureInfo}
|
||||
key={1}
|
||||
lastUpdated={image.lastUpdated}
|
||||
platforms={image.platforms}
|
||||
/>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
@ -31,16 +74,7 @@ afterEach(() => {
|
||||
|
||||
describe('Repo card component', () => {
|
||||
it('navigates to repo page when clicked', async () => {
|
||||
render(
|
||||
<RepoCard
|
||||
name={mockImage.name}
|
||||
version={mockImage.latestVersion}
|
||||
description={mockImage.description}
|
||||
vendor={mockImage.vendor}
|
||||
key={1}
|
||||
lastUpdated={mockImage.lastUpdated}
|
||||
/>
|
||||
);
|
||||
render(<RepoCardWrapper image={mockImage} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
@ -48,15 +82,7 @@ describe('Repo card component', () => {
|
||||
});
|
||||
|
||||
it('renders placeholders for missing data', async () => {
|
||||
render(
|
||||
<RepoCard
|
||||
name={mockImage.name}
|
||||
version={mockImage.latestVersion}
|
||||
description={mockImage.description}
|
||||
vendor={mockImage.vendor}
|
||||
key={1}
|
||||
/>
|
||||
);
|
||||
render(<RepoCardWrapper image={{ ...mockImage, lastUpdated: '' }} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
@ -65,17 +91,7 @@ describe('Repo card component', () => {
|
||||
});
|
||||
|
||||
it('navigates to explore page when platform chip is clicked', async () => {
|
||||
render(
|
||||
<RepoCard
|
||||
name={mockImage.name}
|
||||
version={mockImage.latestVersion}
|
||||
description={mockImage.description}
|
||||
vendor={mockImage.vendor}
|
||||
key={1}
|
||||
lastUpdated={mockImage.lastUpdated}
|
||||
platforms={mockImage.platforms}
|
||||
/>
|
||||
);
|
||||
render(<RepoCardWrapper image={mockImage} />);
|
||||
const osChip = await screen.findByText(/linux/i);
|
||||
fireEvent.click(osChip);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
|
@ -3,6 +3,7 @@ import { api } from 'api';
|
||||
import DependsOn from 'components/Tag/Tabs/DependsOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const mockDependenciesList = {
|
||||
data: {
|
||||
@ -52,11 +53,13 @@ const mockDependenciesList = {
|
||||
|
||||
const RouterDependsWrapper = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<DependsOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<DependsOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { api } from 'api';
|
||||
import IsDependentOn from 'components/Tag/Tabs/IsDependentOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const mockDependentsList = {
|
||||
data: {
|
||||
@ -52,11 +53,13 @@ const mockDependentsList = {
|
||||
|
||||
const RouterDependsWrapper = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<IsDependentOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<IsDependentOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,47 +1,42 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import ReferredBy from 'components/Tag/Tabs/ReferredBy';
|
||||
import React from 'react';
|
||||
|
||||
const mockReferrersList = {
|
||||
data: {
|
||||
Referrers: [
|
||||
const mockReferrersList = [
|
||||
{
|
||||
MediaType: 'application/vnd.oci.artifact.manifest.v1+json',
|
||||
ArtifactType: 'application/vnd.example.icecream.v1',
|
||||
Size: 466,
|
||||
Digest: 'sha256:be7a3d01c35a2cf53c502e9dc50cdf36b15d9361c81c63bf319f1d5cbe44ab7c',
|
||||
Annotations: [
|
||||
{
|
||||
MediaType: 'application/vnd.oci.artifact.manifest.v1+json',
|
||||
ArtifactType: 'application/vnd.example.icecream.v1',
|
||||
Size: 466,
|
||||
Digest: 'sha256:be7a3d01c35a2cf53c502e9dc50cdf36b15d9361c81c63bf319f1d5cbe44ab7c',
|
||||
Annotations: [
|
||||
{
|
||||
Key: 'demo',
|
||||
Value: 'true'
|
||||
},
|
||||
{
|
||||
Key: 'format',
|
||||
Value: 'oci'
|
||||
}
|
||||
]
|
||||
Key: 'demo',
|
||||
Value: 'true'
|
||||
},
|
||||
{
|
||||
MediaType: 'application/vnd.oci.artifact.manifest.v1+json',
|
||||
ArtifactType: 'application/vnd.example.icecream.v1',
|
||||
Size: 466,
|
||||
Digest: 'sha256:d9ad22f41d9cb9797c134401416eee2a70446cee1a8eb76fc6b191f4320dade2',
|
||||
Annotations: [
|
||||
{
|
||||
Key: 'demo',
|
||||
Value: 'true'
|
||||
},
|
||||
{
|
||||
Key: 'format',
|
||||
Value: 'oci'
|
||||
}
|
||||
]
|
||||
Key: 'format',
|
||||
Value: 'oci'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
MediaType: 'application/vnd.oci.artifact.manifest.v1+json',
|
||||
ArtifactType: 'application/vnd.example.icecream.v1',
|
||||
Size: 466,
|
||||
Digest: 'sha256:d9ad22f41d9cb9797c134401416eee2a70446cee1a8eb76fc6b191f4320dade2',
|
||||
Annotations: [
|
||||
{
|
||||
Key: 'demo',
|
||||
Value: 'true'
|
||||
},
|
||||
{
|
||||
Key: 'format',
|
||||
Value: 'oci'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
];
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@ -57,27 +52,17 @@ afterEach(() => {
|
||||
|
||||
describe('Referred by tab', () => {
|
||||
it('should render referrers if there are any', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockReferrersList });
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
render(<ReferredBy referrers={mockReferrersList} />);
|
||||
expect(await screen.findAllByText('Media type: application/vnd.oci.artifact.manifest.v1+json')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("renders no referrers if there aren't any", async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: { Referrers: [] } } });
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
render(<ReferredBy referrers={[]} />);
|
||||
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should log an error if the request fails', async () => {
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should display the digest when clicking the dropdowns', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockReferrersList });
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
render(<ReferredBy referrers={mockReferrersList} />);
|
||||
const firstDigest = (await screen.findAllByText(/digest/i))[0];
|
||||
expect(firstDigest).toBeInTheDocument();
|
||||
await userEvent.click(firstDigest);
|
||||
@ -91,13 +76,11 @@ describe('Referred by tab', () => {
|
||||
});
|
||||
|
||||
it('should display the annotations when clicking the dropdown', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockReferrersList });
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
render(<ReferredBy referrers={mockReferrersList} />);
|
||||
const firstAnnotations = (await screen.findAllByText(/ANNOTATIONS/i))[0];
|
||||
expect(firstAnnotations).toBeInTheDocument();
|
||||
await userEvent.click(firstAnnotations);
|
||||
expect(await screen.findByText(/demo: true/i)).toBeInTheDocument();
|
||||
await userEvent.click(firstAnnotations);
|
||||
expect(await screen.findByText(/demo: true/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -3,18 +3,18 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import TagDetails from 'components/Tag/TagDetails';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||
|
||||
const TagDetailsThemeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<TagDetails />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -72,13 +72,122 @@ const mockImage = {
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf45etertdfg973e29',
|
||||
LastUpdated: '2020-12-08T00:22:52.526672082Z',
|
||||
Size: '75183423',
|
||||
ConfigDigest: 'sha256:8dd57e171a61368ffcfde38045ddb6ed74a32950c271c1da93eaddfb66a77e78',
|
||||
Platform: {
|
||||
Os: 'windows',
|
||||
Arch: 'amd64'
|
||||
},
|
||||
History: [
|
||||
{
|
||||
Layer: {
|
||||
Size: '75181999',
|
||||
Digest: 'sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621',
|
||||
Score: null
|
||||
},
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:52.526672082Z',
|
||||
CreatedBy:
|
||||
'/bin/sh -c #(nop) ADD file:bd7a2aed6ede423b719ceb2f723e4ecdfa662b28639c8429731c878e86fb138b in / ',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: false
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:52.895811646Z',
|
||||
CreatedBy:
|
||||
'/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20201204',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:53.076477777Z',
|
||||
CreatedBy: '/bin/sh -c #(nop) CMD ["/bin/bash"]',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e25',
|
||||
LastUpdated: '2020-12-08T00:22:52.526672082Z',
|
||||
Size: '75183423',
|
||||
ConfigDigest: 'sha256:8dd57e171a61368ffcfde38045ddb6ed74a32950c271c1da93eaddfb66a77e78',
|
||||
Platform: {
|
||||
Os: 'linux',
|
||||
Arch: 'arm'
|
||||
},
|
||||
History: [
|
||||
{
|
||||
Layer: {
|
||||
Size: '75181999',
|
||||
Digest: 'sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621',
|
||||
Score: null
|
||||
},
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:52.526672082Z',
|
||||
CreatedBy:
|
||||
'/bin/sh -c #(nop) ADD file:bd7a2aed6ede423b719ceb2f723e4ecdfa662b28639c8429731c878e86fb138b in / ',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: false
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:52.895811646Z',
|
||||
CreatedBy:
|
||||
'/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20201204',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2020-12-08T00:22:53.076477777Z',
|
||||
CreatedBy: '/bin/sh -c #(nop) CMD ["/bin/bash"]',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'CRITICAL',
|
||||
Count: 10
|
||||
},
|
||||
Vendor: 'CentOS'
|
||||
Vendor: 'CentOS',
|
||||
IsSigned: true,
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: 'author1'
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: 'author2'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@ -272,6 +381,472 @@ const mockDependenciesList = {
|
||||
}
|
||||
};
|
||||
|
||||
const mockDependentsList = {
|
||||
data: {
|
||||
DerivedImageList: {
|
||||
Page: { ItemCount: 4, TotalCount: 4 },
|
||||
Results: [
|
||||
{
|
||||
RepoName: 'project-stacker/c3/static-ubuntu-amd64',
|
||||
Tag: 'tag1',
|
||||
Manifests: [],
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
RepoName: 'tag2',
|
||||
Tag: 'tag2',
|
||||
Manifests: [],
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'CRITICAL',
|
||||
Count: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
RepoName: 'tag3',
|
||||
Tag: 'tag3',
|
||||
Manifests: [],
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'LOW',
|
||||
Count: 7
|
||||
}
|
||||
},
|
||||
{
|
||||
RepoName: 'tag4',
|
||||
Tag: 'tag4',
|
||||
Manifests: [],
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockCVEList = {
|
||||
CVEListForImage: {
|
||||
Tag: '',
|
||||
Page: { ItemCount: 20, TotalCount: 20 },
|
||||
CVEList: [
|
||||
{
|
||||
Id: 'CVE-2020-16156',
|
||||
Title: 'perl-CPAN: Bypass of verification of signatures in CHECKSUMS files',
|
||||
Description: 'CPAN 2.28 allows Signature Verification Bypass.',
|
||||
Severity: 'MEDIUM',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'perl-base',
|
||||
InstalledVersion: '5.30.0-9ubuntu0.2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2021-36222',
|
||||
Title:
|
||||
'krb5: Sending a request containing PA-ENCRYPTED-CHALLENGE padata element without using FAST could result in NULL dereference in KDC which leads to DoS',
|
||||
Description:
|
||||
'ec_verify in kdc/kdc_preauth_ec.c in the Key Distribution Center (KDC) in MIT Kerberos 5 (aka krb5) before 1.18.4 and 1.19.x before 1.19.2 allows remote attackers to cause a NULL pointer dereference and daemon crash. This occurs because a return value is not properly managed in a certain situation.',
|
||||
Severity: 'HIGH',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'krb5-locales',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libgssapi-krb5-2',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libk5crypto3',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5-3',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5support0',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2021-4209',
|
||||
Title: 'GnuTLS: Null pointer dereference in MD_UPDATE',
|
||||
Description:
|
||||
"A NULL pointer dereference flaw was found in GnuTLS. As Nettle's hash update functions internally call memcpy, providing zero-length input may cause undefined behavior. This flaw leads to a denial of service after authentication in rare circumstances.",
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libgnutls30',
|
||||
InstalledVersion: '3.6.13-2ubuntu1.6',
|
||||
FixedVersion: '3.6.13-2ubuntu1.7'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2022-1586',
|
||||
Title: 'pcre2: Out-of-bounds read in compile_xclass_matchingpath in pcre2_jit_compile.c',
|
||||
Description:
|
||||
'An out-of-bounds read vulnerability was discovered in the PCRE2 library in the compile_xclass_matchingpath() function of the pcre2_jit_compile.c file. This involves a unicode property matching issue in JIT-compiled regular expressions. The issue occurs because the character was not fully read in case-less matching within JIT.',
|
||||
Severity: 'CRITICAL',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libpcre2-8-0',
|
||||
InstalledVersion: '10.34-7',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2021-20223',
|
||||
Title: '',
|
||||
Description:
|
||||
'An issue was found in fts5UnicodeTokenize() in ext/fts5/fts5_tokenize.c in Sqlite. A unicode61 tokenizer configured to treat unicode "control-characters" (class Cc), was treating embedded nul characters as tokens. The issue was fixed in sqlite-3.34.0 and later.',
|
||||
Severity: 'NONE',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libsqlite3-0',
|
||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2017-11164',
|
||||
Title: 'pcre: OP_KETRMAX feature in the match function in pcre_exec.c',
|
||||
Description:
|
||||
'In PCRE 8.41, the OP_KETRMAX feature in the match function in pcre_exec.c allows stack exhaustion (uncontrolled recursion) when processing a crafted regular expression.',
|
||||
Severity: 'UNKNOWN',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libpcre3',
|
||||
InstalledVersion: '2:8.39-12ubuntu0.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2020-35527',
|
||||
Title: 'sqlite: Out of bounds access during table rename',
|
||||
Description:
|
||||
'In SQLite 3.31.1, there is an out of bounds access problem through ALTER TABLE for views that have a nested FROM clause.',
|
||||
Severity: 'MEDIUM',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libsqlite3-0',
|
||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2013-4235',
|
||||
Title: 'shadow-utils: TOCTOU race conditions by copying and removing directory trees',
|
||||
Description:
|
||||
'shadow: TOCTOU (time-of-check time-of-use) race condition when copying and removing directory trees',
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'login',
|
||||
InstalledVersion: '1:4.8.1-1ubuntu5.20.04.2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'passwd',
|
||||
InstalledVersion: '1:4.8.1-1ubuntu5.20.04.2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2021-43618',
|
||||
Title: 'gmp: Integer overflow and resultant buffer overflow via crafted input',
|
||||
Description:
|
||||
'GNU Multiple Precision Arithmetic Library (GMP) through 6.2.1 has an mpz/inp_raw.c integer overflow and resultant buffer overflow via crafted input, leading to a segmentation fault on 32-bit platforms.',
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libgmp10',
|
||||
InstalledVersion: '2:6.2.0+dfsg-4',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2022-2509',
|
||||
Title: 'gnutls: Double free during gnutls_pkcs7_verify.',
|
||||
Description:
|
||||
'A vulnerability found in gnutls. This security flaw happens because of a double free error occurs during verification of pkcs7 signatures in gnutls_pkcs7_verify function.',
|
||||
Severity: 'MEDIUM',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libgnutls30',
|
||||
InstalledVersion: '3.6.13-2ubuntu1.6',
|
||||
FixedVersion: '3.6.13-2ubuntu1.7'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2021-39537',
|
||||
Title: 'ncurses: heap-based buffer overflow in _nc_captoinfo() in captoinfo.c',
|
||||
Description:
|
||||
'An issue was discovered in ncurses through v6.2-1. _nc_captoinfo in captoinfo.c has a heap-based buffer overflow.',
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libncurses6',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libncursesw6',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libtinfo6',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'ncurses-base',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'ncurses-bin',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2022-1587',
|
||||
Title: 'pcre2: Out-of-bounds read in get_recurse_data_length in pcre2_jit_compile.c',
|
||||
Description:
|
||||
'An out-of-bounds read vulnerability was discovered in the PCRE2 library in the get_recurse_data_length() function of the pcre2_jit_compile.c file. This issue affects recursions in JIT-compiled regular expressions caused by duplicate data transfers.',
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libpcre2-8-0',
|
||||
InstalledVersion: '10.34-7',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2022-29458',
|
||||
Title: 'ncurses: segfaulting OOB read',
|
||||
Description:
|
||||
'ncurses 6.3 before patch 20220416 has an out-of-bounds read and segmentation violation in convert_strings in tinfo/read_entry.c in the terminfo library.',
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libncurses6',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libncursesw6',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libtinfo6',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'ncurses-base',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'ncurses-bin',
|
||||
InstalledVersion: '6.2-0ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2016-2781',
|
||||
Title: 'coreutils: Non-privileged session can escape to the parent session in chroot',
|
||||
Description:
|
||||
"chroot in GNU coreutils, when used with --userspec, allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.",
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'coreutils',
|
||||
InstalledVersion: '8.30-3ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2021-3671',
|
||||
Title: 'samba: Null pointer dereference on missing sname in TGS-REQ',
|
||||
Description:
|
||||
'A null pointer de-reference was found in the way samba kerberos server handled missing sname in TGS-REQ (Ticket Granting Server - Request). An authenticated user could use this flaw to crash the samba server.',
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libasn1-8-heimdal',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libgssapi3-heimdal',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libhcrypto4-heimdal',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libheimbase1-heimdal',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libheimntlm0-heimdal',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libhx509-5-heimdal',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5-26-heimdal',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libroken18-heimdal',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libwind0-heimdal',
|
||||
InstalledVersion: '7.7.0+dfsg-1ubuntu1',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2016-20013',
|
||||
Title: '',
|
||||
Description:
|
||||
"sha256crypt and sha512crypt through 0.6 allow attackers to cause a denial of service (CPU consumption) because the algorithm's runtime is proportional to the square of the length of the password.",
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libc-bin',
|
||||
InstalledVersion: '2.31-0ubuntu9.9',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libc6',
|
||||
InstalledVersion: '2.31-0ubuntu9.9',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2022-35252',
|
||||
Title: 'curl: control code in cookie denial of service',
|
||||
Description: 'No description is available for this CVE.',
|
||||
Severity: 'LOW',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libcurl4',
|
||||
InstalledVersion: '7.68.0-1ubuntu2.12',
|
||||
FixedVersion: '7.68.0-1ubuntu2.13'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2021-37750',
|
||||
Title:
|
||||
'krb5: NULL pointer dereference in process_tgs_req() in kdc/do_tgs_req.c via a FAST inner body that lacks server field',
|
||||
Description:
|
||||
'The Key Distribution Center (KDC) in MIT Kerberos 5 (aka krb5) before 1.18.5 and 1.19.x before 1.19.3 has a NULL pointer dereference in kdc/do_tgs_req.c via a FAST inner body that lacks a server field.',
|
||||
Severity: 'MEDIUM',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'krb5-locales',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libgssapi-krb5-2',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libk5crypto3',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5-3',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libkrb5support0',
|
||||
InstalledVersion: '1.17-6ubuntu4.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2020-35525',
|
||||
Title: 'sqlite: Null pointer derreference in src/select.c',
|
||||
Description:
|
||||
'In SQlite 3.31.1, a potential null pointer derreference was found in the INTERSEC query processing.',
|
||||
Severity: 'MEDIUM',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libsqlite3-0',
|
||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Id: 'CVE-2022-37434',
|
||||
Title:
|
||||
'zlib: a heap-based buffer over-read or buffer overflow in inflate in inflate.c via a large gzip header extra field',
|
||||
Description:
|
||||
'zlib through 1.2.12 has a heap-based buffer over-read or buffer overflow in inflate in inflate.c via a large gzip header extra field. NOTE: only applications that call inflateGetHeader are affected. Some common applications bundle the affected zlib source code but may be unable to call inflateGetHeader (e.g., see the nodejs/node reference).',
|
||||
Severity: 'MEDIUM',
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'zlib1g',
|
||||
InstalledVersion: '1:1.2.11.dfsg-2ubuntu1.3',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// mock clipboard copy fn
|
||||
const mockCopyToClipboard = jest.fn();
|
||||
Object.assign(navigator, {
|
||||
@ -285,7 +860,8 @@ jest.mock('react-router-dom', () => ({
|
||||
useParams: () => {
|
||||
return { name: 'test', tag: '1.0.1' };
|
||||
},
|
||||
useNavigate: () => mockUseNavigate
|
||||
useNavigate: () => mockUseNavigate,
|
||||
useLocation: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock('../../host', () => ({
|
||||
@ -314,10 +890,22 @@ describe('Tags details', () => {
|
||||
it('should show tabs and allow nagivation between them', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependenciesList });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: mockDependenciesList });
|
||||
const dependenciesTab = await screen.findByTestId('dependencies-tab');
|
||||
fireEvent.click(dependenciesTab);
|
||||
expect(await screen.findByTestId('depends-on-container')).toBeInTheDocument();
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: mockDependentsList });
|
||||
const dependentsTab = await screen.findByText(/used by/i);
|
||||
fireEvent.click(dependentsTab);
|
||||
expect(await screen.findByTestId('dependents-container')).toBeInTheDocument();
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: mockCVEList });
|
||||
const vulnerabilityTab = await screen.findByText(/vulnerabilities/i);
|
||||
fireEvent.click(vulnerabilityTab);
|
||||
expect(await screen.findByTestId('vulnerability-container')).toBeInTheDocument();
|
||||
const referrersTab = await screen.findByText(/referred by/i);
|
||||
fireEvent.click(referrersTab);
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: [] });
|
||||
expect(await screen.findByTestId('referred-by-container')).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(5));
|
||||
});
|
||||
|
||||
@ -328,6 +916,24 @@ describe('Tags details', () => {
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should show the data of the different manifests when switching between them', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
const manifestSelect = await screen.findByText(/linux\/amd64/i);
|
||||
await userEvent.click(manifestSelect);
|
||||
await userEvent.click(await screen.findByText(/windows\/amd64/i));
|
||||
expect(await screen.findByText(/windows\/amd64/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should preselect a manifest if data is received', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImage } });
|
||||
useLocation.mockImplementation(() => ({
|
||||
state: { digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e25' }
|
||||
}));
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
expect(await screen.findByText(/linux\/arm/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should redirect to homepage if it receives invalid data', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: null, errors: ['testerror'] } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
@ -370,72 +976,95 @@ 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 />);
|
||||
const dropdown = await screen.findByText('Pull Image');
|
||||
const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`);
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
userEvent.click(dropdown);
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-meniuItem')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1));
|
||||
fireEvent.click(await screen.findByTestId('pullcopy-btn'));
|
||||
await waitFor(() => expect(mockCopyToClipboard).toHaveBeenCalledWith('docker pull localhost/centos:8'));
|
||||
await waitFor(() =>
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
`docker pull localhost/${mockImage.Image.RepoName}:${mockImage.Image.Tag}`
|
||||
)
|
||||
);
|
||||
userEvent.click(dropdown);
|
||||
});
|
||||
|
||||
it('should copy the podman pull string to clipboard', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
const dropdown = await screen.findByText('Pull Image');
|
||||
const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`);
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
userEvent.click(dropdown);
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-meniuItem')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1));
|
||||
const podmanTab = await screen.findByText('Podman');
|
||||
userEvent.click(podmanTab);
|
||||
fireEvent.click(await screen.findByTestId('podmanPullcopy-btn'));
|
||||
await waitFor(() => expect(mockCopyToClipboard).toHaveBeenCalledWith('podman pull localhost/centos:8'));
|
||||
await waitFor(() =>
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
`podman pull localhost/${mockImage.Image.RepoName}:${mockImage.Image.Tag}`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should copy the skopeo copy string to clipboard', async () => {
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
|
||||
.mockResolvedValue({ status: 200, data: { data: mockImage } });
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
const dropdown = await screen.findByText('Pull Image');
|
||||
const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`);
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
userEvent.click(dropdown);
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-meniuItem')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1));
|
||||
const skopeoTab = await screen.findByText('Skopeo');
|
||||
userEvent.click(skopeoTab);
|
||||
fireEvent.click(await screen.findByTestId('skopeoPullcopy-btn'));
|
||||
await waitFor(() => expect(mockCopyToClipboard).toHaveBeenCalledWith('skopeo copy docker://localhost/centos:8'));
|
||||
await waitFor(() =>
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
`skopeo copy docker://localhost/${mockImage.Image.RepoName}:${mockImage.Image.Tag}`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should show pull tabs in dropdown and allow nagivation between them', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
const dropdown = await screen.findByText('Pull Image');
|
||||
const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`);
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
userEvent.click(dropdown);
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-meniuItem')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1));
|
||||
const podmanTab = await screen.findByText('Podman');
|
||||
userEvent.click(podmanTab);
|
||||
await userEvent.click(podmanTab);
|
||||
await waitFor(() => expect(screen.queryAllByTestId('podman-input')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(3));
|
||||
await waitFor(() => expect(screen.getAllByRole('tab').length).toBeGreaterThanOrEqual(3));
|
||||
});
|
||||
|
||||
it('should show the copied successfully button for 3 seconds', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetailsThemeWrapper />);
|
||||
const dropdown = await screen.findByText('Pull Image');
|
||||
const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`);
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
userEvent.click(dropdown);
|
||||
await userEvent.click(dropdown);
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-dropdown')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.queryAllByTestId('successPulled-buton')).toHaveLength(0));
|
||||
fireEvent.click(await screen.findByTestId('pullcopy-btn'));
|
||||
await userEvent.click(await screen.findByTestId('pullcopy-btn'));
|
||||
await waitFor(() => expect(screen.queryAllByTestId('successPulled-buton')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-dropdown')).toHaveLength(0));
|
||||
|
||||
await waitFor(() => expect(screen.queryAllByTestId('pull-dropdown')).toHaveLength(1), { timeout: 3500 });
|
||||
await waitFor(() => expect(screen.queryAllByTestId('successPulled-buton')).toHaveLength(0));
|
||||
await waitFor(() => expect(screen.queryAllByTestId('successPulled-buton')).toHaveLength(0), { timeout: 4500 });
|
||||
});
|
||||
});
|
||||
|
@ -24,6 +24,14 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the tags page component', async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
@ -1,21 +1,35 @@
|
||||
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';
|
||||
|
||||
jest.mock('xlsx');
|
||||
|
||||
const StateVulnerabilitiesWrapper = () => {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<VulnerabilitiesDetails name="mongo" />
|
||||
</MemoryRouter>
|
||||
<MockThemeProvider>
|
||||
<MemoryRouter>
|
||||
<VulnerabilitiesDetails name="mongo" />
|
||||
</MemoryRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const mockCVEList = {
|
||||
CVEListForImage: {
|
||||
const simpleMockCVEList = {
|
||||
CVEListForImage: {
|
||||
Tag: '',
|
||||
Page: { ItemCount: 20, TotalCount: 20 },
|
||||
Page: { ItemCount: 2, TotalCount: 2 },
|
||||
Summary: {
|
||||
Count: 2,
|
||||
UnknownCount: 0,
|
||||
LowCount: 0,
|
||||
MediumCount: 1,
|
||||
HighCount: 0,
|
||||
CriticalCount: 1,
|
||||
},
|
||||
CVEList: [
|
||||
{
|
||||
Id: 'CVE-2020-16156',
|
||||
@ -25,6 +39,53 @@ 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'
|
||||
}
|
||||
@ -40,26 +101,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'
|
||||
}
|
||||
@ -74,6 +140,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libgnutls30',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.6.13-2ubuntu1.6',
|
||||
FixedVersion: '3.6.13-2ubuntu1.7'
|
||||
}
|
||||
@ -88,6 +155,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libpcre2-8-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '10.34-7',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -102,6 +170,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libsqlite3-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||
}
|
||||
@ -116,6 +185,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libpcre3',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '2:8.39-12ubuntu0.1',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -130,6 +200,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libsqlite3-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||
}
|
||||
@ -144,11 +215,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'
|
||||
}
|
||||
@ -163,6 +236,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libgmp10',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '2:6.2.0+dfsg-4',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -177,6 +251,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libgnutls30',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.6.13-2ubuntu1.6',
|
||||
FixedVersion: '3.6.13-2ubuntu1.7'
|
||||
}
|
||||
@ -191,26 +266,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'
|
||||
}
|
||||
@ -225,6 +305,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libpcre2-8-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '10.34-7',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -239,26 +320,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'
|
||||
}
|
||||
@ -273,6 +359,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'coreutils',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '8.30-3ubuntu2',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -287,46 +374,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'
|
||||
}
|
||||
@ -341,11 +437,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'
|
||||
}
|
||||
@ -359,6 +457,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libcurl4',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '7.68.0-1ubuntu2.12',
|
||||
FixedVersion: '7.68.0-1ubuntu2.13'
|
||||
}
|
||||
@ -374,26 +473,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'
|
||||
}
|
||||
@ -408,6 +512,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'libsqlite3-0',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '3.31.1-4ubuntu0.3',
|
||||
FixedVersion: '3.31.1-4ubuntu0.4'
|
||||
}
|
||||
@ -423,6 +528,7 @@ const mockCVEList = {
|
||||
PackageList: [
|
||||
{
|
||||
Name: 'zlib1g',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1:1.2.11.dfsg-2ubuntu1.3',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
@ -432,6 +538,56 @@ const mockCVEList = {
|
||||
}
|
||||
};
|
||||
|
||||
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: {
|
||||
@ -485,31 +641,97 @@ describe('Vulnerabilties page', () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20));
|
||||
await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1));
|
||||
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').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);
|
||||
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");
|
||||
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 () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 200,
|
||||
data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [] } } }
|
||||
data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [], Summary: {} } } }
|
||||
});
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
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)
|
||||
);
|
||||
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 () => {
|
||||
@ -527,13 +749,111 @@ 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());
|
||||
await expect(await screen.findByText('latest')).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').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]);
|
||||
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]);
|
||||
const exportAsExcelBtn = screen.getByText(/xlsx/i);
|
||||
expect(exportAsExcelBtn).toBeInTheDocument();
|
||||
await fireEvent.click(exportAsExcelBtn);
|
||||
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
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 } }).
|
||||
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');
|
||||
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();
|
||||
});
|
||||
|
||||
it('should handle fixed CVE query errors', async () => {
|
||||
@ -544,7 +864,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));
|
||||
});
|
||||
|
19
src/__tests__/api.test.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { api } from '../api';
|
||||
|
||||
describe('api module', () => {
|
||||
it('should redirect to login if a 401 error is received', () => {
|
||||
const location = new URL('https://www.test.com');
|
||||
location.replace = jest.fn();
|
||||
delete window.location;
|
||||
window.location = location;
|
||||
const axiosInstance = api.getAxiosInstance();
|
||||
expect(
|
||||
axiosInstance.interceptors.response.handlers[0].rejected({
|
||||
response: { statusText: 'Unauthorized', status: 401 }
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
response: { statusText: 'Unauthorized', status: 401 }
|
||||
});
|
||||
expect(location.replace).toHaveBeenCalledWith('/login');
|
||||
});
|
||||
});
|
104
src/api.js
@ -1,39 +1,45 @@
|
||||
import axios from 'axios';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||
import { isAuthenticationEnabled, logoutUser } from 'utilities/authUtilities';
|
||||
import { host } from 'host';
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
if (config.url.includes(endpoints.authConfig) || !isAuthenticationEnabled()) {
|
||||
config.withCredentials = false;
|
||||
} else {
|
||||
config.headers['X-ZOT-API-CLIENT'] = 'zot-ui';
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
if (error.response.status === 401) {
|
||||
localStorage.clear();
|
||||
if (error?.response?.status === 401) {
|
||||
if (window.location.pathname.includes('/login')) return Promise.reject(error);
|
||||
logoutUser();
|
||||
window.location.replace('/login');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const api = {
|
||||
getAxiosInstance: () => axios,
|
||||
|
||||
getRequestCfg: () => {
|
||||
const authConfig = JSON.parse(localStorage.getItem('authConfig'));
|
||||
const genericHeaders = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
const authHeaders = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${token}`
|
||||
};
|
||||
return {
|
||||
headers: authHeaders
|
||||
};
|
||||
}
|
||||
|
||||
// withCredentials option must be enabled on cross-origin
|
||||
return {
|
||||
headers: genericHeaders
|
||||
headers: genericHeaders,
|
||||
withCredentials: host() !== window?.location?.origin && authConfig !== null
|
||||
};
|
||||
},
|
||||
|
||||
@ -61,40 +67,76 @@ 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);
|
||||
}
|
||||
};
|
||||
|
||||
const endpoints = {
|
||||
status: `/v2/`,
|
||||
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:${
|
||||
(pageNumber - 1) * pageSize
|
||||
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned Documentation Vendor Labels} DownloadCount}}}`,
|
||||
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned SignatureInfo { Tool IsTrusted Author } Documentation Vendor Labels} IsStarred IsBookmarked StarCount DownloadCount}}}`,
|
||||
detailedRepoInfo: (name) =>
|
||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch}} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor Size } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName IsSigned Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor 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 Vulnerabilities {MaxSeverity Count} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
||||
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }) =>
|
||||
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||
`/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 = '',
|
||||
excludedTerm = '',
|
||||
severity = ''
|
||||
) => {
|
||||
let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
}}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`,
|
||||
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }) =>
|
||||
`/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${
|
||||
}}`;
|
||||
if (!isEmpty(searchTerm)) {
|
||||
query += `, searchedCVE: "${searchTerm}"`;
|
||||
}
|
||||
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 PackagePath InstalledVersion FixedVersion}}}}`,
|
||||
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }, filter = {}) => {
|
||||
let filterParam = '';
|
||||
if (filter.Os || filter.Arch) {
|
||||
filterParam = `,filter:{`;
|
||||
if (filter.Os) filterParam += ` Os:${!isEmpty(filter.Os) ? `${JSON.stringify(filter.Os)}` : '""'}`;
|
||||
if (filter.Arch) filterParam += ` Arch:${!isEmpty(filter.Arch) ? `${JSON.stringify(filter.Arch)}` : '""'}`;
|
||||
filterParam += '}';
|
||||
}
|
||||
return `/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
}}) {Page {TotalCount ItemCount} Results {Tag}}}`,
|
||||
}}${filterParam}) {Page {TotalCount ItemCount} Results {Tag}}}`;
|
||||
},
|
||||
dependsOnForImage: (name, { pageNumber = 1, pageSize = 15 } = {}) =>
|
||||
`/v2/_zot/ext/search?query={BaseImageList(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
}}){Page {TotalCount ItemCount} Results { RepoName Tag Description Manifests {Digest Platform {Os Arch} Size} Vendor DownloadCount LastUpdated IsSigned Vulnerabilities {MaxSeverity Count}}}}`,
|
||||
}}){Page {TotalCount ItemCount} Results { RepoName Tag Description Manifests {Digest Platform {Os Arch} Size} Vendor DownloadCount LastUpdated IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count}}}}`,
|
||||
isDependentOnForImage: (name, { pageNumber = 1, pageSize = 15 } = {}) =>
|
||||
`/v2/_zot/ext/search?query={DerivedImageList(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
}}){Page {TotalCount ItemCount} Results {RepoName Tag Description Manifests {Digest Platform {Os Arch} Size} Vendor DownloadCount LastUpdated IsSigned Vulnerabilities {MaxSeverity Count}}}}`,
|
||||
}}){Page {TotalCount ItemCount} Results {RepoName Tag Description Manifests {Digest Platform {Os Arch} Size} Vendor DownloadCount LastUpdated IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count}}}}`,
|
||||
globalSearch: ({
|
||||
searchQuery = '""',
|
||||
pageNumber = 1,
|
||||
@ -110,9 +152,11 @@ const endpoints = {
|
||||
if (filter.Os) filterParam += ` Os:${!isEmpty(filter.Os) ? `${JSON.stringify(filter.Os)}` : '""'}`;
|
||||
if (filter.Arch) filterParam += ` Arch:${!isEmpty(filter.Arch) ? `${JSON.stringify(filter.Arch)}` : '""'}`;
|
||||
if (filter.HasToBeSigned) filterParam += ` HasToBeSigned: ${filter.HasToBeSigned}`;
|
||||
if (filter.IsBookmarked) filterParam += ` IsBookmarked: ${filter.IsBookmarked}`;
|
||||
if (filter.IsStarred) filterParam += ` IsStarred: ${filter.IsStarred}`;
|
||||
filterParam += '}';
|
||||
if (Object.keys(filter).length === 0) filterParam = '';
|
||||
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam} ${filterParam}) {Page {TotalCount ItemCount} Repos {Name LastUpdated Size Platforms { Os Arch } NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description IsSigned Licenses Vendor Labels } DownloadCount}}}`;
|
||||
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam} ${filterParam}) {Page {TotalCount ItemCount} Repos {Name LastUpdated Size Platforms { Os Arch } IsStarred IsBookmarked NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description IsSigned SignatureInfo { Tool IsTrusted Author } Licenses Vendor Labels } StarCount DownloadCount}}}`;
|
||||
},
|
||||
imageSuggestions: ({ searchQuery = '""', pageNumber = 1, pageSize = 15 }) => {
|
||||
const searchParam = searchQuery !== '' ? `query:"${searchQuery}"` : `query:""`;
|
||||
@ -120,7 +164,9 @@ const endpoints = {
|
||||
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam}) {Images {RepoName Tag}}}`;
|
||||
},
|
||||
referrers: ({ repo, digest, type = '' }) =>
|
||||
`/v2/_zot/ext/search?query={Referrers(repo: "${repo}" digest: "${digest}" type: "${type}"){MediaType ArtifactType Size Digest Annotations{Key Value}}}`
|
||||
`/v2/_zot/ext/search?query={Referrers(repo: "${repo}" digest: "${digest}" type: "${type}"){MediaType ArtifactType Size Digest Annotations{Key Value}}}`,
|
||||
bookmarkToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleBookmark`,
|
||||
starToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleStar`
|
||||
};
|
||||
|
||||
export { api, endpoints };
|
||||
|
401
src/assets/Alt_Linux_Team.svg
Normal file
@ -0,0 +1,401 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
||||
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
||||
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
||||
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
||||
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
||||
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
||||
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
||||
]>
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="220px" height="97px"
|
||||
viewBox="0 0 220 97" style="enable-background:new 0 0 220 97;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#FDC811;}
|
||||
.st2{fill-rule:evenodd;clip-rule:evenodd;}
|
||||
.st3{fill:#FFCA07;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="&ns_sfw;">
|
||||
<slices></slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="96.7914734" width="220" x="0" y="0.2085275"></sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<g>
|
||||
<path class="st0" d="M219.9046783,66.5411987c0-0.0004807,0-0.0004807-0.0004883-0.0004807l-0.1141663-0.3805542
|
||||
c-0.1006775-0.3381577-0.3820038-1.0241241-1.1329956-1.8189468c-0.3661041-0.389225-0.8290253-0.7047501-1.5294342-1.1821251
|
||||
c-2.2240753-1.5154724-2.5318909-1.9957428-2.5627136-2.0737801c-0.0192719-0.0703278-0.0419159-0.1401787-0.0679321-0.2095451
|
||||
c-0.465332-1.2375221-0.0703278-3.2380791,0.3126373-5.1764946c0.1690826-0.8574486,0.3439484-1.7442856,0.4484711-2.5704231
|
||||
c1.3285675-8.9984093,0.8213348-13.87817-0.8174591-21.4073524l-0.3270874-1.5911007
|
||||
c-0.7245026-3.5492649-1.3507233-6.6144085-2.5458527-9.9276352c-0.3275757-0.9142933-0.8256531-1.8305111-1.2245178-2.5641613
|
||||
l-0.3015594-0.5631237c-0.2601166-0.4952011-0.3265991-0.7649612-0.4701538-1.3478346
|
||||
c-0.1478729-0.6922226-0.1950836-1.4258718-0.2456665-2.2043209c-0.0761108-1.1883879-0.1546326-2.4172392-0.5746918-3.7087135
|
||||
c-0.5130157-1.6556492-1.9152832-3.5931015-3.3401947-4.6119261c-0.7866364-0.5443363-1.6267548-0.7957907-2.3021088-0.9976287
|
||||
c-0.2606049-0.0785193-0.6546478-0.197021-0.7668915-0.2663877c-0.0414276-0.0255308-0.0838165-0.0500982-0.1247559-0.0717754
|
||||
L201.08461,3.2608931c-1.4870605-0.7914555-3.0251617-1.6098869-4.4828339-2.528033
|
||||
c-0.868042-0.595398-2.7115631-1.1594846-4.2304077-0.2716864l-0.4007874,0.2345945
|
||||
c-0.6700592,0.3925966-1.0891571,1.0905997-1.1214294,1.8661585c-0.0327606,0.7760406,0.3270874,1.5058367,0.9629517,1.9528668
|
||||
l0.3781433,0.2663879c1.8016052,1.2697968,2.8006744,2.6739922,3.235672,4.5401506l0.1151276,0.5587883
|
||||
c-1.3719177,0.9205542-2.8353729,2.2626085-4.3209686,5.3104105c-0.5501251,1.1281738-0.9576569,2.0024834-1.3661499,2.8792019
|
||||
c-0.2663879,0.5722752-0.5202484,1.1156483-0.7996368,1.69804H20.0874577c-0.2726498,0-0.4932747,0.2206249-0.4932747,0.4932747
|
||||
v16.3561058c-0.0178242-0.0038567-0.0356464-0.0077095-0.0534706-0.0110817
|
||||
c-1.5858002-0.3246727-3.5699797-0.482193-6.0652561-0.482193c-1.5853195,0-3.0940466,0.1353607-4.4847536,0.40271
|
||||
c-1.5434103,0.297699-2.766963,0.7326889-3.7405062,1.3300133c-1.30159,0.7981987-2.3281217,1.777523-3.0521371,2.9114761
|
||||
c-0.7008934,1.0978241-1.2066927,2.4841957-1.5463009,4.2390785l-0.403676,2.087265
|
||||
c-0.0260125,0.1343994,0.0048171,0.2740974,0.0862267,0.3848915c0.0809279,0.1103134,0.2042466,0.1825676,0.3405715,0.1984673
|
||||
l3.2886589,0.3853683c-0.8357732,0.4894218-1.5164344,1.0597725-2.0588441,1.7226067
|
||||
C0.6406791,51.3267593,0,53.2622833,0,55.5383797c0,2.4832344,0.8733468,4.60952,2.5959547,6.319603
|
||||
c1.7182724,1.7038193,4.1422558,2.5680122,7.2040272,2.5680122c2.1498871,0,4.097456-0.3882599,5.7921238-1.1556282
|
||||
c0.2543449-0.1175385,0.5096531-0.2509727,0.763998-0.3983765c0.1806431,0.3559837,0.3945236,0.7779655,0.3945236,0.7779655
|
||||
c0.0838184,0.1657104,0.2538624,0.2702408,0.4398041,0.2702408h2.4037514v5.0753326
|
||||
c0,0.2726517,0.2206249,0.4932785,0.4932747,0.4932785h181.1667023c0.0476837,0,0.0939331-0.007225,0.1377716-0.0211945h16.3368378
|
||||
c0.7163086,0,1.397934-0.3424988,1.823288-0.9162216C219.9788513,67.9786377,220.1103668,67.2266769,219.9046783,66.5411987z"/>
|
||||
<rect x="22.0607033" y="22.2340794" class="st1" width="177.2202148" height="44.7882233"/>
|
||||
<path class="st1" d="M10.4436264,46.3384933L3.07324,45.4756279c0.2770877-1.432476,0.6785073-2.5584679,1.2031634-3.3794403
|
||||
c0.5240412-0.8209724,1.2788663-1.5331688,2.263741-2.1373215c0.7077856-0.4343758,1.6802859-0.7717323,2.9178729-1.010601
|
||||
c1.2384424-0.2381325,2.5775766-0.357933,4.0175247-0.357933c2.3113928,0,4.1679554,0.1447868,5.5702972,0.4321671
|
||||
c1.4023438,0.2873764,2.5702286,0.8878555,3.5051231,1.7999687c0.6570721,0.6313477,1.1752357,1.5265541,1.5537491,2.6841507
|
||||
c0.3792496,1.158329,0.5688763,2.2630043,0.5688763,3.316227v9.8730011c0,1.0532265,0.0595322,1.8778725,0.180069,2.4739418
|
||||
c0.1198025,0.5968056,0.3815765,1.3575096,0.7858143,2.2843208h-7.2374744
|
||||
c-0.2910519-0.5754929-0.4806767-1.0135384-0.5688744-1.3156128c-0.0881977-0.3020821-0.1771297-0.7754059-0.2653294-1.4214554
|
||||
c-1.0105972,1.0818901-2.0145807,1.8528862-3.0126848,2.3159256c-1.3641233,0.617382-2.9494772,0.9268074-4.7553253,0.9268074
|
||||
c-2.4004464,0-4.2224631-0.6181183-5.4667859-1.8528824c-1.2443225-1.2347679-1.8661156-2.7583847-1.8661156-4.5686417
|
||||
c0-1.6978073,0.448338-3.095005,1.3450146-4.1893921c0.8966765-1.0943832,2.5518527-1.9087448,4.9640594-2.4416008
|
||||
c2.8928833-0.6460495,4.7685547-1.0987968,5.6277466-1.3582458c0.8591928-0.2594528,1.768364-0.6004829,2.7282486-1.0216255
|
||||
c0-1.0517578-0.1955051-1.7889442-0.5872498-2.2100868c-0.3917446-0.4211464-1.0796862-0.6320839-2.0652952-0.6320839
|
||||
c-1.2634325,0-2.2108221,0.2249069-2.8421707,0.6739807C11.1447973,44.7105141,10.7471733,45.3697929,10.4436264,46.3384933z
|
||||
M17.1319504,50.8431931c-1.0605774,0.4211426-2.165988,0.7937775-3.3154974,1.116436
|
||||
c-1.5669785,0.4630356-2.5577326,0.9187279-2.9752016,1.3678017c-0.4292297,0.463768-0.6437235,0.9900131-0.6437235,1.5794716
|
||||
c0,0.6732407,0.2108192,1.2244759,0.6341677,1.6522331c0.4233494,0.4284973,1.0451431,0.6423759,1.8668518,0.6423759
|
||||
c0.8584576,0,1.6581163-0.2315178,2.3967714-0.6945572c0.738656-0.4637756,1.2626982-1.027504,1.5728598-1.6948662
|
||||
c0.3094254-0.666626,0.4637718-1.5331688,0.4637718-2.6003609V50.8431931z"/>
|
||||
<g>
|
||||
<path d="M10.4436264,46.3384933L3.07324,45.4756279c0.2770877-1.432476,0.6785073-2.5584679,1.2031634-3.3794403
|
||||
c0.5240412-0.8209724,1.2788663-1.5331688,2.263741-2.1373215c0.7077856-0.4343758,1.6802859-0.7717323,2.9178729-1.010601
|
||||
c1.2384424-0.2381325,2.5775766-0.357933,4.0175247-0.357933c2.3113928,0,4.1679554,0.1447868,5.5702972,0.4321671
|
||||
c1.4023438,0.2873764,2.5702286,0.8878555,3.5051231,1.7999687c0.6570721,0.6313477,1.1752357,1.5265541,1.5537491,2.6841507
|
||||
c0.3792496,1.158329,0.5688763,2.2630043,0.5688763,3.316227v9.8730011c0,1.0532265,0.0595322,1.8778725,0.180069,2.4739418
|
||||
c0.1198025,0.5968056,0.3815765,1.3575096,0.7858143,2.2843208h-7.2374744
|
||||
c-0.2910519-0.5754929-0.4806767-1.0135384-0.5688744-1.3156128c-0.0881977-0.3020821-0.1771297-0.7754059-0.2653294-1.4214554
|
||||
c-1.0105972,1.0818901-2.0145807,1.8528862-3.0126848,2.3159256c-1.3641233,0.617382-2.9494772,0.9268074-4.7553253,0.9268074
|
||||
c-2.4004464,0-4.2224631-0.6181183-5.4667859-1.8528824c-1.2443225-1.2347679-1.8661156-2.7583847-1.8661156-4.5686417
|
||||
c0-1.6978073,0.448338-3.095005,1.3450146-4.1893921c0.8966765-1.0943832,2.5518527-1.9087448,4.9640594-2.4416008
|
||||
c2.8928833-0.6460495,4.7685547-1.0987968,5.6277466-1.3582458c0.8591928-0.2594528,1.768364-0.6004829,2.7282486-1.0216255
|
||||
c0-1.0517578-0.1955051-1.7889442-0.5872498-2.2100868c-0.3917446-0.4211464-1.0796862-0.6320839-2.0652952-0.6320839
|
||||
c-1.2634325,0-2.2108221,0.2249069-2.8421707,0.6739807C11.1447973,44.7105141,10.7471733,45.3697929,10.4436264,46.3384933z
|
||||
M17.1319504,50.8431931c-1.0605774,0.4211426-2.165988,0.7937775-3.3154974,1.116436
|
||||
c-1.5669785,0.4630356-2.5577326,0.9187279-2.9752016,1.3678017c-0.4292297,0.463768-0.6437235,0.9900131-0.6437235,1.5794716
|
||||
c0,0.6732407,0.2108192,1.2244759,0.6341677,1.6522331c0.4233494,0.4284973,1.0451431,0.6423759,1.8668518,0.6423759
|
||||
c0.8584576,0,1.6581163-0.2315178,2.3967714-0.6945572c0.738656-0.4637756,1.2626982-1.027504,1.5728598-1.6948662
|
||||
c0.3094254-0.666626,0.4637718-1.5331688,0.4637718-2.6003609V50.8431931z"/>
|
||||
<g>
|
||||
<path d="M28.7858028,30.5908012h7.7305279v30.863308h-7.7305279V30.5908012z"/>
|
||||
<path d="M49.97015,30.5908012v8.5051918h4.2445145v6.2737999H49.97015v7.920887
|
||||
c0,0.9525337,0.0815849,1.583149,0.2462196,1.8911018c0.2528343,0.4762688,0.6945572,0.7144051,1.3259048,0.7144051
|
||||
c0.5688744,0,1.3649826-0.1815414,2.3879547-0.5468292l0.5681381,5.9151268
|
||||
c-1.9071541,0.4630356-3.6881332,0.6952896-5.3433113,0.6952896c-1.9197655,0-3.3346062-0.2734146-4.2437744-0.8202362
|
||||
c-0.9099083-0.5468254-1.5824165-1.3773537-2.018261-2.4915848c-0.4358406-1.1142311-0.6533966-2.919342-0.6533966-5.413868
|
||||
v-7.8642921h-2.8421707V39.095993h2.8421707v-4.1055984L49.97015,30.5908012z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M65.8155975,30.5908012h7.7305298v30.863308h-7.7305298V30.5908012z"/>
|
||||
<path d="M77.9810028,30.5908012h7.7114105v5.831337h-7.7114105V30.5908012z M77.9810028,39.095993h7.7114105v22.3581161h-7.7114105
|
||||
V39.095993z"/>
|
||||
<path d="M89.8428497,39.095993h7.1807632v3.6418304c1.0738068-1.4875984,2.1608429-2.5503807,3.2596436-3.1890793
|
||||
c1.098793-0.6386986,2.4379272-0.9584122,4.0166626-0.9584122c2.1343842,0,3.8057327,0.7055779,5.0118408,2.1160088
|
||||
c1.2060928,1.4111595,1.8095169,3.5896416,1.8095169,6.5369186v14.2108498h-7.7496338v-12.294754
|
||||
c0-1.4030838-0.2337189-2.3960419-0.7011719-2.9788818c-0.467453-0.5820999-1.124527-0.8738899-1.9704895-0.8738899
|
||||
c-0.9348907,0-1.6926575,0.3932152-2.274025,1.1789093c-0.5806351,0.7864265-0.8716812,2.1975937-0.8716812,4.2312813v10.7373352
|
||||
h-7.7114258V39.095993z"/>
|
||||
<path d="M136.1517639,61.4541092h-7.200592V57.832859c-1.0730743,1.4883385-2.156311,2.5474434-3.2493515,3.178791
|
||||
c-1.0929184,0.6320839-2.4342575,0.9481239-4.0262222,0.9481239c-2.1218872,0-3.7866211-0.7055817-4.9927216-2.1160088
|
||||
s-1.8093948-3.5822945-1.8093948-6.5163345V39.095993h7.7495041v12.2954979c0,1.4030762,0.2337341,2.3997078,0.7011719,2.98843
|
||||
c0.467453,0.5901909,1.124527,0.884182,1.9704895,0.884182c0.9216614,0,1.6766129-0.3924789,2.264595-1.1781731
|
||||
c0.5871201-0.7856979,0.8811264-2.1968613,0.8811264-4.2320213V39.095993h7.7113953V61.4541092z"/>
|
||||
<path d="M138.3317413,39.095993h9.1512299l3.1927643,6.2311707l3.7234039-6.2311707h8.5074158l-6.8639984,10.652813
|
||||
l7.3564301,11.7053032h-8.9998474l-3.7234039-7.1998711l-4.3671417,7.1998711h-8.3561096l7.3086395-11.7053032
|
||||
L138.3317413,39.095993z"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M56.8743248,77.8356705H42.8329544c-1.4275665,0-2.589386,1.1615219-2.5898552,2.588913h-0.4812546
|
||||
c-0.0004692-1.4273911-1.1622887-2.588913-2.5898552-2.588913h-4.6130028c-1.4278011,0-2.5898552,1.162056-2.5898552,2.5898514
|
||||
v3.8786774l-2.0189228-4.8702927c-0.4023762-0.9709625-1.3409405-1.5982361-2.3917198-1.5982361h-5.0088043
|
||||
c-1.0507793,0-1.9893436,0.6272736-2.3917198,1.5982361l-5.6032133,13.5145721
|
||||
c-0.3324184,0.8005219-0.2432098,1.7090378,0.2385139,2.4297485c0.4812555,0.7211761,1.2860069,1.1517258,2.1532049,1.1517258
|
||||
h4.7458763c1.1160412,0,2.1029663-0.711319,2.4565125-1.7700806l0.1356888-0.407074h1.473814l0.1347523,0.4047241
|
||||
c0.353075,1.060173,1.3400002,1.7724304,2.4569817,1.7724304h4.8106689c0.2206726,0,0.4436932-0.0347443,0.6953545-0.1079865
|
||||
c0.248373,0.0723038,0.4789066,0.1079865,0.7028675,0.1079865h11.799427c0.5892448,0,1.152195-0.1990738,1.6104431-0.5653
|
||||
c0.4577789,0.3662262,1.0207291,0.5653,1.6099739,0.5653h4.5477371c1.427803,0,2.5898552-1.162056,2.5898552-2.589859v-7.562973
|
||||
h2.1579018c1.4277992,0,2.5898552-1.162056,2.5898552-2.589859v-3.3617401
|
||||
C59.46418,78.9977264,58.302124,77.8356705,56.8743248,77.8356705z"/>
|
||||
<path class="st0" d="M134.7516785,86.9945374l3.5993042-5.0698471c0.5624847-0.7934799,0.6347961-1.8231354,0.1887512-2.6870422
|
||||
c-0.4465027-0.8648529-1.3282623-1.4019775-2.3006287-1.4019775h-5.076889c-0.9211884,0-1.7804108,0.4958115-2.2419434,1.2935181
|
||||
l-0.2835846,0.4915848l-0.355423-0.5681152c-0.4760971-0.7620239-1.2968063-1.2169876-2.1954651-1.2169876h-5.0740662
|
||||
c-0.2244339,0-0.4502716,0.0333328-0.6878433,0.1004791c-0.2403946-0.0671463-0.4620056-0.1004791-0.6756363-0.1004791
|
||||
h-4.6144104c-1.2522049,0-2.2992249,0.893486-2.538681,2.0762024c-0.2389832-1.1827164-1.2860031-2.0762024-2.538208-2.0762024
|
||||
h-4.6125336c-0.5282059,0-1.0324631,0.1619797-1.470993,0.4704514c-0.4380569-0.3084717-0.9418488-0.4704514-1.4705276-0.4704514
|
||||
h-4.3463135c-1.3362503,0-2.4396133,1.0174408-2.5753021,2.3184738l-0.952179-1.276619
|
||||
c-0.4864197-0.6521606-1.2625351-1.0418549-2.075737-1.0418549h-4.2843399c-0.6620178,0-1.2935181,0.2587051-1.7747726,0.7178879
|
||||
c-0.4803162-0.4591827-1.1122894-0.7178879-1.7743073-0.7178879h-4.6158142c-1.427803,0-2.589859,1.162056-2.589859,2.5898514
|
||||
v7.6287155h-3.8021469v-7.6287155c0-1.4277954-1.162056-2.5898514-2.5898514-2.5898514h-4.614418
|
||||
c-1.4277954,0-2.5898514,1.162056-2.5898514,2.5898514V93.940094c0,1.427803,1.162056,2.589859,2.5898514,2.589859h11.7327576
|
||||
c0.3183365,0,0.6315002-0.0596313,0.9319916-0.1765366c0.3004913,0.1169052,0.6131897,0.1765366,0.9315262,0.1765366h4.6158142
|
||||
c0.6620178,0,1.2939911-0.2587051,1.7743073-0.7178955c0.4812546,0.4591904,1.1127548,0.7178955,1.7747726,0.7178955h4.2843399
|
||||
c1.3296738,0,2.4283447-1.0075836,2.5734253-2.2992249l0.9672012,1.2747345
|
||||
c0.4868927,0.6413651,1.2578354,1.0244904,2.0625916,1.0244904h4.3463135c1.3371811,0,2.4410172-1.0188522,2.5757675-2.3212891
|
||||
c0.6704712,0.7277451,1.4432983,1.2911682,2.2917175,1.6728821c0.8502884,0.4169312,1.8949661,0.6840897,3.0218048,0.7718887
|
||||
l0.2319412,0.0178452c0.8117981,0.0615005,1.5789871,0.1201935,2.3335037,0.1201935
|
||||
c1.6541061,0,2.9898834-0.1981354,4.0725861-0.6005096c0.5117722-0.1638641,1.016037-0.4061356,1.5334396-0.7380829
|
||||
c0.4845428,0.6704712,1.2606583,1.0770721,2.0870056,1.0770721h5.1388626c0.8601532,0,1.6616211-0.4258499,2.1442795-1.1381073
|
||||
l0.7573318-1.1183929l0.7545166,1.1165085c0.48172,0.7136688,1.2836609,1.1399918,2.1452179,1.1399918h5.1421509
|
||||
c0.9808197,0,1.86586-0.5437012,2.3100281-1.4193497c0.443222-0.8751831,0.3582306-1.9104614-0.2216187-2.7015991
|
||||
L134.7516785,86.9945374z"/>
|
||||
<path class="st0" d="M208.9085693,77.8356705h-5.9943237c-1.0061646,0-1.917038,0.5925293-2.3400726,1.4822617
|
||||
c-0.4225616-0.8902054-1.331543-1.4822617-2.3400726-1.4822617h-6.0666199c-1.4278107,0-2.589859,1.162056-2.589859,2.5898514
|
||||
v4.5585403l-2.3011017-5.5501556c-0.4023743-0.9709625-1.3409424-1.5982361-2.3917236-1.5982361h-5.0078583
|
||||
c-1.0507813,0-1.9893494,0.6272736-2.3917236,1.5982361l-2.8053589,6.7657394v-0.6991119
|
||||
c0-0.1788864-0.0187836-0.3573074-0.0554047-0.5324326c0.3784332-0.4620056,0.585495-1.0390396,0.585495-1.6395569v-2.9030228
|
||||
c0-1.4277954-1.1620636-2.5898514-2.589859-2.5898514H160.293396c-0.3629456,0-0.7211914,0.0812225-1.067688,0.24086
|
||||
c-0.3455658-0.1596375-0.7042694-0.24086-1.067215-0.24086h-14.0366669c-1.4278107,0-2.589859,1.162056-2.589859,2.5898514
|
||||
v3.3617401c0,1.427803,1.1620483,2.589859,2.589859,2.589859h2.0902863v7.562973
|
||||
c0,1.427803,1.1620483,2.589859,2.589859,2.589859h4.6129913c1.4278107,0,2.589859-1.162056,2.589859-2.589859v-7.562973
|
||||
h1.6987152v7.562973c0,1.427803,1.1620483,2.589859,2.589859,2.589859h12.5891418
|
||||
c0.2220917,0,0.4516754-0.0352173,0.6986542-0.1065826c0.2511902,0.0723038,0.4727936,0.1065826,0.6920624,0.1065826h4.7486877
|
||||
c1.1183929,0,2.1062622-0.7131958,2.4579315-1.7747726l0.1333313-0.4023819h1.4705353l0.1352234,0.4061356
|
||||
c0.3535461,1.059227,1.3409271,1.771019,2.4569702,1.771019h4.812088c0.2601013,0,0.5286713-0.0493011,0.8193054-0.1507187
|
||||
c0.2817078,0.1000061,0.5704651,0.1507187,0.8601532,0.1507187h3.756134c0.5164642,0,1.0240173-0.1568222,1.4503326-0.4446335
|
||||
c0.4263306,0.2878113,0.9338684,0.4446335,1.4498749,0.4446335h3.4307556c0.5160065,0,1.0230713-0.1568222,1.4494019-0.4441681
|
||||
c0.4258423,0.2878189,0.9329224,0.4441681,1.4494019,0.4441681h3.755188c1.4277954,0,2.589859-1.162056,2.589859-2.589859
|
||||
V80.4255219C211.4984283,78.9977264,210.3363647,77.8356705,208.9085693,77.8356705z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st2" d="M20.5494957,80.4256134L14.9462709,93.939949h4.7459002l0.7266064-2.1771622h5.2069492l0.7242508,2.1771622
|
||||
h4.8109646l-5.6026363-13.5143356H20.5494957z M21.4068203,88.7963791l1.6487007-4.8101578l1.6478157,4.8101578H21.4068203z"/>
|
||||
<polygon class="st2" points="37.1717606,80.4256134 32.5589333,80.4256134 32.5589333,93.939949 44.3586197,93.939949
|
||||
44.3586197,90.6439514 37.1717606,90.6439514 "/>
|
||||
<polygon class="st2" points="42.8327599,83.7874222 47.5786629,83.7874222 47.5786629,93.939949 52.1267204,93.939949
|
||||
52.1267204,83.7874222 56.8740921,83.7874222 56.8740921,80.4256134 42.8327599,80.4256134 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st2" points="71.0224304,80.4256134 66.4078369,80.4256134 66.4078369,93.939949 78.1403961,93.939949
|
||||
78.1403961,90.6439514 71.0224304,90.6439514 "/>
|
||||
<rect x="80.0041428" y="80.4256134" class="st2" width="4.6157718" height="13.5143366"/>
|
||||
<polygon class="st2" points="98.0564499,87.9391251 92.4532242,80.4256134 88.1692505,80.4256134 88.1692505,93.939949
|
||||
92.4532242,93.939949 92.4532242,86.555687 98.0564499,93.939949 102.4031296,93.939949 102.4031296,80.4256134
|
||||
98.0564499,80.4256134 "/>
|
||||
<path class="st2" d="M115.0338593,88.7336655c0,0.7249908-0.1987228,1.3160934-0.6577072,1.709938
|
||||
c-0.4634018,0.3968658-1.0551682,0.5941925-1.8450775,0.5941925c-0.7922592,0-1.4529114-0.1973267-1.9127808-0.5941925
|
||||
c-0.4634094-0.3938446-0.6612473-0.9849472-0.6612473-1.709938v-8.3080521h-4.6128311v8.1077042
|
||||
c0,0.6569061,0.132782,1.3833618,0.4610443,2.2391434c0.1330795,0.5292053,0.4610519,1.0575943,0.9895172,1.5837097
|
||||
c0.4619293,0.5269241,0.9880447,0.9230499,1.5821609,1.1838226c0.5270004,0.2653427,1.2518387,0.4642181,2.1115189,0.5314865
|
||||
c0.8549652,0.0649948,1.646637,0.129982,2.3691254,0.129982c1.2533035,0,2.3735352-0.129982,3.2332153-0.4611206
|
||||
c0.6577148-0.2003479,1.2488937-0.5941925,1.8406525-1.1180267c0.5941238-0.5314789,1.0575256-1.1233978,1.3183746-1.8498535
|
||||
c0.2655563-0.722702,0.3986359-1.4491653,0.3986359-2.2391434v-8.1077042h-4.6146011V88.7336655z"/>
|
||||
<polygon class="st2" points="136.2389221,80.4256134 131.1621094,80.4256134 128.7232056,84.6446686 126.0855789,80.4256134
|
||||
121.0117035,80.4256134 125.6895981,86.9510803 120.5506592,93.939949 125.6895981,93.939949 128.5924835,89.6520844
|
||||
131.4909668,93.939949 136.632843,93.939949 131.55867,87.0183563 "/>
|
||||
</g>
|
||||
<polygon class="st2" points="144.121933,83.7874222 148.8021851,83.7874222 148.8021851,93.939949 153.4150085,93.939949
|
||||
153.4150085,83.7874222 158.1585541,83.7874222 158.1585541,80.4256134 144.121933,80.4256134 "/>
|
||||
<polygon class="st2" points="164.9077606,88.2702637 172.09021,88.2702637 172.09021,85.5003738 164.9077606,85.5003738
|
||||
164.9077606,83.3285828 172.6201477,83.3285828 172.6201477,80.4256134 160.293457,80.4256134 160.293457,93.939949
|
||||
172.8827515,93.939949 172.8827515,90.9070053 164.9077606,90.9070053 "/>
|
||||
<path class="st2" d="M179.876709,80.4256134l-5.6032257,13.5143356h4.7482452l0.7219086-2.1771622h5.2069397l0.7251434,2.1771622
|
||||
h4.8124237l-5.6032257-13.5143356H179.876709z M180.733139,88.7963791l1.6495819-4.8101578l1.6457672,4.8101578H180.733139z"/>
|
||||
<polygon class="st2" points="202.9142761,80.4256134 200.5380859,88.6663971 198.234024,80.4256134 192.1674042,80.4256134
|
||||
192.1674042,93.939949 195.9237823,93.939949 195.9237823,83.6550827 198.8237305,93.939949 202.2547913,93.939949
|
||||
205.1532593,83.6550827 205.1532593,93.939949 208.9087677,93.939949 208.9087677,80.4256134 "/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st2" d="M184.3468018,66.8016739h10.7164917l-1.0384216-2.0768509c0,0,0.3323059-1.0799561,0.3323059-1.204567
|
||||
s0.0830688-1.9937706,0.0830688-1.9937706l-1.4953308-3.0737267c0,0,0.2492218-1.5368652,0.2492218-1.6614761
|
||||
s0.1302032-1.4689789,0.1302032-1.4689789l-1.2516785-1.2724533c0,0,2.076828-0.8307381,2.2014465-0.955349
|
||||
c0.1246033-0.1246109,0.8722687-0.4984436,0.8722687-0.4984436l-1.5784149-2.450676c0,0-2.2014465-9.262722-2.2014465-9.0965767
|
||||
c0,0.1661491-1.9937592,7.3935661-2.0353088,7.5597115c-0.0415344,0.1661453-2.6998901,8.7227478-2.6998901,8.8473549
|
||||
c0,0.1246109-0.3738403,4.9428864-0.3738403,4.9428864l0.1246185,1.4953346l0.166153,1.3707161L184.3468018,66.8016739z"/>
|
||||
<g>
|
||||
<path d="M217.7933044,66.8032532c-0.0118866-0.0402832-0.1311951-0.4059448-0.6125336-0.9156876
|
||||
c-0.1967621-0.2091675-0.5996094-0.4834061-1.1573944-0.8635254c-1.2596436-0.8588715-3.1638336-2.1567421-3.4907684-3.3735237
|
||||
c-0.6951447-1.8489342-0.2349854-4.1802406,0.2097015-6.435112c0.1709442-0.8650742,0.332077-1.6826324,0.4322815-2.474369
|
||||
c1.2813416-8.6760368,0.7891541-13.3959808-0.7979431-20.6904831l-0.328476-1.5989666
|
||||
c-0.7101288-3.4757862-1.3236847-6.4779778-2.4671173-9.6485348c-0.2685699-0.7478371-0.7049866-1.5529995-1.0902557-2.262619
|
||||
c-0.1105347-0.2045174-0.2169189-0.400774-0.3134918-0.5846348c-0.390976-0.7447357-0.5113068-1.2343426-0.663147-1.8546124
|
||||
l-0.0206604-0.0836678c-0.1828308-0.8573256-0.2380829-1.7131023-0.2917938-2.5404739
|
||||
c-0.070755-1.1031628-0.1384125-2.144865-0.478775-3.1922493c-0.3651123-1.1770163-1.4724121-2.7134891-2.4717712-3.4282722
|
||||
c-0.4896088-0.3387985-1.0876617-0.518527-1.6661072-0.6920581c-0.4870148-0.1461587-0.9477081-0.2845707-1.316452-0.5128465
|
||||
l-1.125885-0.6021943c-1.5344086-0.8165259-3.1204681-1.6614568-4.7235718-2.6732054
|
||||
c-0.2778473-0.1905742-1.2203979-0.5257578-1.8019409-0.1854095l-0.4023132,0.2355065l0.3811493,0.2685599
|
||||
c2.2683105,1.5979323,3.5935364,3.4990273,4.1693878,5.972362c0.0351257,0.1786957,0.0702362,0.3480949,0.1043396,0.5087147
|
||||
c0.2489166,1.1852798,0.3026276,1.614459-0.3295135,1.9971571c-1.3655243,0.8258219-2.7129669,1.8163948-4.1822968,4.8299475
|
||||
c-0.5562439,1.1413822-0.965271,2.0203991-1.3526154,2.851902c-0.636795,1.3686237-1.1904602,2.5580349-2.2249146,4.5531254
|
||||
c-3.9938049-1.7947044-7.3781738-1.9103928-10.3333588-0.3615227c-6.8007813,3.2552567-8.4921875,11.1839771-8.7132263,17.2622128
|
||||
c-0.1187897,3.1700401,0.4611969,6.665966,1.0215454,10.0462074c0.641449,3.8688164,1.3050995,7.8693275,0.8831482,11.2640305
|
||||
c-0.3883667,2.5533829-1.8117371,4.0185852-4.4141846,4.6032181c-0.3785553,0-0.6352539,0.1001968-0.7628174,0.2964478
|
||||
c-0.0542297,0.0831604-0.1286011,0.2582397-0.0237579,0.4932251l0.0795288,0.1776581h15.6095428
|
||||
c0.6729431,0,0.9766235-0.3191681,1.3278198-0.6884384c0.1518555-0.1590652,0.3269196-0.3439636,0.5861969-0.5582962
|
||||
c0.6734619-0.6533203,0.5236816-1.4171677,0.3346558-2.3845024c-0.114151-0.5835991-0.2432556-1.2451897-0.2432556-2.0606804
|
||||
c0-2.7945747,0.9647522-5.5540314,1.8974762-8.2220688l0.3548126-1.0194931
|
||||
c1.1501617-3.3203316,1.9527435-6.0384712,2.5379028-8.6145821c0.0469971,0.2158813,0.0944977,0.4369278,0.1430511,0.6621017
|
||||
c0.231369,1.078373,0.4942474,2.3003235,0.8046417,3.4732056c0.1570129,0.5913467,0.350174,1.2803078,0.6125336,1.9971581
|
||||
c-0.6249237,0.2561646-1.2865143,0.519043-1.8411865,0.731823c-0.287674,0.1131096-0.4157562,0.387867-0.3134918,0.6977425
|
||||
c0.0676422,0.1745644,0.2246552,0.3140068,0.3894043,0.365139c0.0666199,0.0340881,0.122406,0.059906,0.1740417,0.0836678
|
||||
c0.1528931,0.070755,0.2375793,0.1094894,0.489624,0.336216c0.3248444,0.2918015,0.8051605,1.0918007,0.8051605,1.8830185
|
||||
c0,0.4560356-0.1038208,0.7788239-0.20401,1.0907631c-0.130661,0.4059448-0.265976,0.8253098-0.1378937,1.4305992
|
||||
c0.1409912,0.7519722,0.5123291,1.501358,0.905365,2.2951584c0.4927063,0.995739,1.0024414,2.0255623,1.0024414,3.0280151
|
||||
c0,0.4198799-0.3109131,0.8702354-0.619751,1.1300163c-0.1704407,0.1430588-0.3563538,0.2649422-0.5422821,0.3873482
|
||||
c-0.212265,0.1394463-0.4322815,0.2840538-0.6357574,0.4606819c-0.4348755,0.377533-0.6843262,0.9306641-0.7411346,1.6433792
|
||||
c-0.0056763,0.0759201-0.0165253,0.1559677-0.0273743,0.2375717c-0.0557709,0.4209137-0.1487427,1.125885,0.5732727,1.2983856
|
||||
c0.4989014,0.1218796,0.8031158-0.3098755,1.0267334-0.6228561c0.0738525-0.1038055,0.2050323-0.2881851,0.2773438-0.333107
|
||||
c0.22052,0.0883102,0.351181,0.3052216,0.4999237,0.5531235c0.1926422,0.3202057,0.4322815,0.7189102,0.9363556,0.7189102
|
||||
h24.4100342L217.7933044,66.8032532z"/>
|
||||
<path class="st0" d="M194.9966888,38.5344658c-0.1146698-0.7964439-0.9970245-0.542347-0.7979431,0.0836067
|
||||
c0.4245453,1.9584198,1.0504761,4.6263962,0.4245453,6.298893
|
||||
C195.5304413,42.9569931,195.6148834,41.0582848,194.9966888,38.5344658z"/>
|
||||
<path class="st0" d="M200.2460022,7.8746614c-1.5274506-2.3263369-3.4675293-4.1429906-5.3639832-5.0278177
|
||||
c1.5036926,0.8848276,3.4548798,3.8626807,4.4560394,5.3981848C199.8741455,8.9776945,200.5001068,8.357295,200.2460022,7.8746614
|
||||
z"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M194.6893768,48.9474983c0.2262115-0.0038757,0.58078,0.0217552,0.7979431,0.0876656
|
||||
c0.2881927,0.0872192,0.557785,0.2287292,0.7994843,0.4071655c0.5017395,0.370369,0.8849487,0.8952866,1.11763,1.4715958
|
||||
c0.108963,0.270237,0.1817932,0.5515175,0.2230988,0.8398933c0.2122803,1.4921875-0.2016754,3.0194931,0.5941925,4.5467987
|
||||
c0.8320313,1.4230461,1.7727509,2.6776581,2.3318329,3.8874092c0.7607422,1.5282707,1.4241333,3.2951546,0.9340057,5.5588608
|
||||
c-0.0046387,0.0262222-0.0167847,0.0560379-0.0224609,0.0827103c1.2929688-0.1495819,2.5211182-0.4603653,3.5744324-1.0690155
|
||||
c3.1537781-1.874752,4.6845703-5.6450539,2.6148376-10.1344604c-0.6499481-1.4479027-1.3675842-2.2984467-2.272171-3.8299484
|
||||
c-1.7019958-2.4163971-1.3611298-6.7584839-0.5639648-9.5952797c0.6481476-2.6664276,2.1861877-5.7629356,2.2985077-5.7629356
|
||||
c0.1670685-0.3703041-0.2587433-0.6251755-0.4803009-0.2540359c-0.8888397,1.419239-1.4827728,2.7237587-1.9909668,4.1389885
|
||||
c-0.8242798,2.3032265-1.1669464,4.7147789-1.0504913,7.1287842c0.0550079,1.4446754,0.0410614,3.5284004-0.1712036,4.2027054
|
||||
s-0.7021179,1.1333122-1.3180084,0.6309242c-0.8552551-0.6976089-1.7830811-2.0375671-2.335434-3.4676514
|
||||
c-0.9125824-2.3635216-1.127182-5.1390533-1.0517883-7.3853378c0-3.7145195,1.413559-7.6934052,1.9194336-11.4087009
|
||||
c0.2881927-1.1652012-0.25177-4.4886322-0.171463-6.4725533c0.0490723-1.3293743,0.0046539-3.1167183-0.0748901-4.6101322
|
||||
c0.0258331-0.5437679,0.1694031-1.5550652,0.2463531-2.4612608c0.1208496-1.3021288,1.0799255-1.6486101,1.9326019-1.080761
|
||||
c0.3982086,0.2899284,0.5138855,0.6554537,1.0541077,0.9971581c1.5909576,0.8513231,2.7436981-0.5565519,2.497345-2.0436373
|
||||
c-0.2465973-1.4894114-1.2043915-2.766427-2.4746246-2.1139431c-0.6832733,0.3509359-0.5861816,1.3501606-1.2431183,1.856679
|
||||
c-0.9120789,0.5448036-1.8770905,0.143384-2.696701-0.4276295c-0.6848297-0.4754677-0.6848297-0.9095535-1.6229858-0.4754677
|
||||
c-1.5238342,0.6880569-3.1127167,2.635891-4.0307312,4.7936049c-1.0711365,2.5176849-2.2698517,4.7100639-3.4587402,7.1837215
|
||||
c1.6444092,1.0210438,3.0628815,2.1821785,4.5099945,3.4875355c2.3852844,2.132019,3.693222,5.5104523,2.9546814,8.6889496
|
||||
c-0.2548676,1.3921242-1.2650757,2.7194939-1.6741028,3.8029022c-0.4090424,1.0834084-0.3561096,2.2922478-0.5812836,3.1249123
|
||||
c-0.2249146,0.8326645-0.382431,1.2377663-0.8325348,1.8453865c-0.4501038,0.6076126-0.8883057,0.168232-0.6011658-0.2021332
|
||||
c0.2525635-0.6227875,0.2411957-1.6882477,0-3.0614548c-0.2411804-1.3732071-1.5602264-3.6093559-2.8160095-5.4275627
|
||||
c-0.4508667-0.6801796-0.6793976-1.2440262-0.8506165-1.8692017c-0.0048981-0.0169144-0.0064392-0.0369263-0.0110931-0.0541649
|
||||
c0.2109833,0.1408005,0.5458984-0.5922508,0.611496-0.9143295c0.0542145-0.3671417,0.4286652-1.159584,1.3014832-1.2200737
|
||||
c0.7932739,0,0.6306,0.6530647,1.8468628,0.9094849c0.7359467,0.1983261,1.1620331-0.0565529,1.3363495-0.5407333
|
||||
c0.4253082-1.0504818-0.2587585-3.1793365-1.0535889-3.8005123c-0.8482819-0.7988377-2.7468109-0.9087124-2.7468109,0.7342815
|
||||
c0.0787659,0.4284687,0,0.6506767,0,0.6506767c-0.2835236,0.1457062-0.6610565-0.1123314-0.9128418-0.6506767
|
||||
c-0.6267242-0.9883785-2.812912-2.5230446-4.0839081-2.4402161c0.0144501,0.0040646,0.0260773,0.0098095,0.0405426,0.0140057
|
||||
c-0.8707581-0.1128445-1.8029785,0.0057468-2.6848297,0.3570709c-2.2130432,0.8761101-3.6315002,2.7826939-4.2329254,4.8828831
|
||||
c-0.9053497,3.0606194,0.3695374,7.0387306,0,10.4705505c-0.2510071,2.5285988-1.0995483,6.8754616-2.8637695,8.8609314
|
||||
c-0.4402924,0.6376381-1.0029755,0.750164-1.2759247,0.2548752s-0.4552612-1.6764984-0.4785004-2.6996117
|
||||
c-0.0361481-1.5928307,0.1384125-3.295475-0.0351105-4.2260742c-0.0852203-0.3711433-0.3687744-0.2301483-0.3687744,0
|
||||
c-0.4810791,2.4418297-0.1107788,6.8078041,0.8823853,10.1041832c1.0241547,3.237442,1.930542,5.620327,1.930542,7.6345329
|
||||
c-0.0591431,1.9081917-2.1296387,2.9252357-1.7084656,2.9833336c2.0196228,0.0302811,4.4315033,0.1131058,6.6169128,0.0302811
|
||||
c0.6241302,0,0.5430603-0.1131058,1.1690063-0.7438354c0.616394-0.7335052,0.225174-1.5307274,0.111557-2.1814003
|
||||
c-0.5417633-3.9757233,1.4174194-8.6276245,2.5786896-12.2369804c1.1142578-3.3203316,2.1598511-6.7823639,2.8957977-10.8727417
|
||||
c0.0271149-0.1664276,0.1998749-0.2540359,0.34552-0.2038727c0.107666,0.0374451,0.1673279,0.179985,0.1967773,0.2922516
|
||||
c0.4836578,1.8772087,1.105484,4.6805611,1.7314301,7.1216164c0.2003937,0.7774086,0.4498444,1.6651382,0.7695313,2.5287895
|
||||
c0.3904419-0.2019997,0.7669373-0.4209175,1.2049103-0.5260124
|
||||
C194.1406555,48.9995308,194.3570404,48.9531746,194.6893768,48.9474983z"/>
|
||||
<g>
|
||||
<path class="st3" d="M191.9888,29.672699c0.0787659,0.4284687,0,0.6506767,0,0.6506767
|
||||
c-0.2835236,0.1457062-0.6610565-0.1123314-0.9128418-0.6506767c-0.6267242-0.9883785-2.812912-2.5230446-4.0839081-2.4402161
|
||||
c1.9256287,0.5423489,2.7220001,1.7059345,3.173645,3.236599c0.3718567,1.0226574,0.2541046,2.1870193,0.3718567,3.5512543
|
||||
c0.111557,1.1635857,0.6794128,0,0.7669525-0.4300842c0.0542145-0.3671417,0.4286652-1.159584,1.3014832-1.2200737
|
||||
c0.7932739,0,0.6306,0.6530647,1.8468628,0.9094849c0.7359467,0.1983261,1.1620331-0.0565529,1.3363495-0.5407333
|
||||
c0.4253082-1.0504818-0.2587585-3.1793365-1.0535889-3.8005123C193.8873291,28.1395798,191.9888,28.029705,191.9888,29.672699z
|
||||
"/>
|
||||
<path class="st3" d="M194.1987457,38.6180725c0.4245453,1.9584198,1.0504761,4.6263962,0.4245453,6.298893
|
||||
c0.9071503-1.9599724,0.9915924-3.8586807,0.3733978-6.3824997
|
||||
C194.882019,37.7380219,193.9996643,37.9921188,194.1987457,38.6180725z"/>
|
||||
<path class="st3" d="M200.2460022,7.8746614c-1.5274506-2.3263369-3.4675293-4.1429906-5.3639832-5.0278177
|
||||
c1.5036926,0.8848276,3.4548798,3.8626807,4.4560394,5.3981848
|
||||
C199.8741455,8.9776945,200.5001068,8.357295,200.2460022,7.8746614z"/>
|
||||
<path class="st3" d="M206.1220398,13.3524475c-0.2465973-1.4894114-1.2043915-2.766427-2.4746246-2.1139431
|
||||
c-0.6832733,0.3509359-0.5861816,1.3501606-1.2431183,1.856679c-0.9120789,0.5448036-1.8770905,0.143384-2.696701-0.4276295
|
||||
c-0.6848297-0.4754677-0.6848297-0.9095535-1.6229858-0.4754677c-0.3444824,0.1555843-0.691803,0.3831501-1.0344696,0.6554546
|
||||
c0.6171722,0.7480307,1.8210449,0.4854088,2.42659,1.6931543c0.5430603,1.0823746,0.8242645,2.4737225,0.9094849,3.5544186
|
||||
c0-0.0081348,0.0103302-0.0126534,0.0113678-0.020401c-0.0023346-0.0433826-0.0036163-0.0908337-0.0059509-0.1337643
|
||||
c0.0258331-0.5437679,0.1694031-1.5550652,0.2463531-2.4612608c0.1208496-1.3021288,1.0799255-1.6486101,1.9326019-1.080761
|
||||
c0.3982086,0.2899284,0.5138855,0.6554537,1.0541077,0.9971581
|
||||
C205.2156525,16.2474079,206.3683929,14.8395329,206.1220398,13.3524475z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st0" d="M197.8086853,62.3406677c-1.0478973-1.7299156-1.87883-4.827343-1.87883-6.7657013
|
||||
c0-1.1123238-0.7958679-0.7344437-1.387085-0.7344437c-0.6272278,0-1.2516022,0.0350571-1.7376556-0.1684914
|
||||
c-0.382782-0.1733856-0.3439331-0.4217796,0.0748444-0.4899178c1.2449799-0.3107681,1.9773712,0.4899178,2.842392-1.2097549
|
||||
c0.2747803-0.561058,0.3486786-1.189312,0.1724548-1.7396278c-0.2008667-0.6584892-0.8981934-1.0052643-1.6940765-1.0412636
|
||||
c-0.5608978,0-1.5661621,0.1383247-1.6372223,0.8328209c-0.0410461,0.3827744,0.1705627,0.9019089,0.7276611,0.9749413
|
||||
c0,0.2386856,0.0701141,0.5503998-0.2027588,0.6565132c-0.4235229,0.1412506-1.2857056-0.2064667-1.1540222,0
|
||||
c0.5880737,0.8318787,0.3126831,1.8069,0.0729675,2.6747017c-0.2706604,0.6604614,0.0761108,1.6646957,0.5561523,2.6348228
|
||||
c0.4481659,0.9418602,1.0043335,1.8447952,1.1777191,2.4263039c0.3173828,1.0509758,0.0672607,2.1584854,0.2823334,3.0263634
|
||||
c0.0644226,0.4539146,0.5192108,0.6282539,0.6218567,1.1484108c0.1408539,0.3467636,0.6600647,0.2746811,0.7974396,0.2746811
|
||||
c0.263092,0,0.5169983-0.2793427,0.680603-0.452652c0.3960419-0.419487,0.9427338-0.4178314,1.4891052-0.3925667
|
||||
c0.5179443,0.0240059,1.0962067,0.1494713,1.4411011,0.5705376c0.2832794,0,0.4547882,0,0.4547882-0.1744156
|
||||
C199.5084534,64.0091553,198.4343262,63.3867455,197.8086853,62.3406677z M194.2601166,50.7846603
|
||||
c0.2826538,0,0.736496,0.4480705,0.5261536,0.6584053c-0.1408539,0.1061974-0.2434998-0.2103348-0.698288-0.2103348
|
||||
c-0.2794952,0-0.5883636,0.2103348-0.5883636,0C193.4996185,50.991127,193.8084869,50.7846603,194.2601166,50.7846603z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st3" d="M192.5630341,66.4706802c-0.1486969-0.2479019-0.2792969-0.4648209-0.4999695-0.5531769
|
||||
c-0.0722046,0.0449142-0.2035217,0.2293167-0.2771454,0.3331528c-0.2237701,0.3129654-0.5278625,0.7447205-1.026886,0.6228256
|
||||
c-0.7220764-0.1724777-0.6289063-0.8774567-0.5731354-1.298378c0.0107269-0.0816193,0.0216827-0.1616287,0.0274048-0.237587
|
||||
c0.0567169-0.7127228,0.3062286-1.2658463,0.7411499-1.6433868c0.2035217-0.1765862,0.4234619-0.3212395,0.6355743-0.4606514
|
||||
c0.1861115-0.1224289,0.371994-0.2442665,0.5423889-0.3873749c0.3088379-0.2597542,0.6198425-0.7101021,0.6198425-1.1300049
|
||||
c0-1.002449-0.5099792-2.0322418-1.0025787-3.0280228c-0.3929749-0.7938042-0.7642517-1.5431709-0.9053345-2.2951622
|
||||
c-0.1282043-0.6052437,0.0071564-1.0246124,0.1377411-1.4305725c0.1003265-0.3119469,0.20401-0.6347351,0.20401-1.0908012
|
||||
c0-0.7911873-0.4801941-1.5911942-0.8050232-1.8830032c-0.2521362-0.2266922-0.336731-0.2654152-0.4894867-0.3361931
|
||||
c-0.051712-0.023777-0.1074829-0.0495682-0.1742096-0.0836525c-0.1646729-0.0511742-0.3217163-0.1905823-0.389389-0.3651466
|
||||
c-0.1022491-0.309864,0.0259705-0.5846329,0.3133698-0.697773c0.5547791-0.2127495,1.2163391-0.475666,1.8411865-0.7317886
|
||||
c-0.262146-0.7168961-0.4554138-1.4058495-0.6124573-1.9971542c-0.3102875-1.1729012-0.5731354-2.3948326-0.8045349-3.4732437
|
||||
c-0.0486145-0.2251434-0.0960388-0.4461746-0.1429749-0.6620827c-0.5852966,2.5761261-1.3879242,5.2942276-2.5380096,8.6145973
|
||||
l-0.3548431,1.0194893c-0.9327393,2.6679955-1.8974152,5.4275055-1.8974152,8.2220421
|
||||
c0,0.8154945,0.1291504,1.4771042,0.2433014,2.0606651c0.1889954,0.9673615,0.3386536,1.7312012-0.3348083,2.3845215
|
||||
c-0.2592926,0.2143631-0.4342041,0.399231-0.5860138,0.5583038c-0.3512726,0.3692627-0.6548767,0.6884155-1.3278503,0.6884155
|
||||
h3.1204224h2.2958679h4.9563751C192.9953308,67.189537,192.7558289,66.7908478,192.5630341,66.4706802z"/>
|
||||
<path class="st0" d="M219.4322205,66.6828918l-0.1151123-0.3818283c-0.0891113-0.2998581-0.3398132-0.902832-1.0187531-1.6218185
|
||||
c-0.3281555-0.3483505-0.7676086-0.648201-1.4487-1.1126099c-0.6303253-0.4299088-2.5477753-1.737278-2.7612915-2.3524742
|
||||
c-0.0145416-0.055645-0.0326538-0.1103401-0.0529022-0.1645546c-0.5147552-1.3690872-0.1060486-3.43964,0.2895508-5.4418602
|
||||
c0.1677704-0.8505249,0.3412628-1.7302475,0.4451599-2.5509872c1.3171387-8.9211807,0.8126373-13.7676277-0.8128815-21.2362537
|
||||
l-0.3267212-1.5916119c-0.7213593-3.5310955-1.3440704-6.5804462-2.5270386-9.8597679
|
||||
c-0.3148041-0.8783493-0.8047791-1.7797565-1.1946411-2.4972458l-0.3040924-0.5671787
|
||||
c-0.2890625-0.5501976-0.366745-0.8674469-0.5161591-1.4746599c-0.1551514-0.7279167-0.2037659-1.4799032-0.2550049-2.2752028
|
||||
c-0.074585-1.159317-0.1517944-2.3581934-0.5535889-3.594306c-0.4770966-1.5378723-1.82164-3.4024076-3.1499786-4.3523693
|
||||
c-0.7206573-0.4983063-1.5197144-0.7382836-2.1617126-0.9306593c-0.3531647-0.1060476-0.7185059-0.2159085-0.8862762-0.319633
|
||||
c-0.0321655-0.0197797-0.0645752-0.0386658-0.097702-0.0560622l-1.1252899-0.60203
|
||||
c-1.4968414-0.7967885-3.0451202-1.6203868-4.5362396-2.5599825c-0.7354126-0.5044421-2.3897705-1.0202039-3.7021332-0.2531444
|
||||
l-0.4008331,0.2347947c-0.5219116,0.30593-0.8529205,0.8566043-0.8779449,1.4609576
|
||||
c-0.0252533,0.6048896,0.2588043,1.1810033,0.7540283,1.5289354l0.3784332,0.2668476
|
||||
c1.9078979,1.3440671,2.9712372,2.8554869,3.4349823,4.8435855l0.1079559,0.5261288
|
||||
c0.0178833,0.0862675,0.0471954,0.2244272,0.0736389,0.3621111c-1.4115143,0.901823-2.9040375,2.1710014-4.4277954,5.2966719
|
||||
c-0.5540771,1.1361418-0.9627686,2.0140142-1.3621674,2.8710966c-0.4563751,0.9805832-0.8674469,1.8640575-1.4486847,3.0266533
|
||||
c-3.8479919-1.386488-7.2172089-1.2799625-10.221344,0.2960987c-5.9996796,2.8720493-9.3512878,9.3530006-9.6925354,18.7408524
|
||||
c-0.1248779,3.3406906,0.4677887,6.913723,1.0404663,10.3689766c0.6186523,3.7305603,1.258255,7.5874252,0.8764954,10.6664467
|
||||
c-0.2688141,1.7678986-1.0804901,2.6433868-2.8783112,3.083725c-1.1743774,0.0759048-1.7615814,0.7095108-2.0130005,1.0956268
|
||||
c-0.4217987,0.6477356-0.480423,1.4662018-0.1570435,2.1879807l0.079361,0.1772537
|
||||
c0.2862091,0.6406937,0.9220123,1.0531998,1.6240692,1.0531998h10.1906128h5.3653107h10.3369141h13.4566345h10.8688202
|
||||
c0.5624237,0,1.0924072-0.2658844,1.4279633-0.7175446C219.4925232,67.8053894,219.5942841,67.2217712,219.4322205,66.6828918z
|
||||
M206.8599548,67.189537h-13.3603516h-4.9563751h-2.2958679h-3.1204224h-5.4258423h-10.1839294l-0.079361-0.1776581
|
||||
c-0.1048431-0.2349701-0.0305023-0.4100723,0.0235901-0.493187c0.1277466-0.1962433,0.3843994-0.2964554,0.7630768-0.2964554
|
||||
c2.6023407-0.5846252,4.0257568-2.0498199,4.4139709-4.6032486c0.4220428-3.3946609-0.2416534-7.3951645-0.8831787-11.2640076
|
||||
c-0.5602722-3.3802452-1.1400757-6.8761864-1.0213928-10.0461807c0.2211456-6.07827,1.9124298-14.0069523,8.7130737-17.2622643
|
||||
c2.9552765-1.5488338,6.3397522-1.4331341,10.3336029,0.3615742c1.0342407-1.9951267,1.588089-3.1845303,2.2248535-4.5531425
|
||||
c0.3872528-0.831522,0.7964172-1.7105274,1.3526459-2.8519135c1.469162-3.013546,2.8168182-4.0041389,4.1823273-4.8299417
|
||||
c0.6319885-0.3827257,0.5783691-0.811862,0.3293457-1.9971542c-0.0340881-0.1606207-0.0691071-0.3299999-0.1043854-0.5087328
|
||||
c-0.5757599-2.4732971-1.9009857-4.3744125-4.1692352-5.9723387l-0.3812866-0.2685752l0.4024963-0.2355096
|
||||
c0.5814972-0.3403656,1.5240021-0.0051832,1.8018646,0.185405c1.6031189,1.011744,3.1890717,1.8566711,4.7235413,2.6732395
|
||||
l1.1257782,0.602149c0.3688965,0.2283006,0.8295593,0.3666987,1.3164215,0.5128427
|
||||
c0.5786285,0.1735487,1.1767731,0.3532939,1.6662598,0.6921105c0.9994812,0.7147508,2.1066742,2.2512527,2.4717407,3.4282641
|
||||
c0.3403168,1.0473719,0.4080048,2.0890827,0.478775,3.1922188c0.0536194,0.8274117,0.108902,1.6831808,0.2919312,2.5405016
|
||||
l0.0204926,0.0836468c0.1518097,0.6202583,0.2721405,1.1098671,0.6632233,1.8546448
|
||||
c0.0965118,0.1838551,0.2028046,0.3801041,0.3133698,0.5846329c0.3853455,0.709568,0.821701,1.5147552,1.090271,2.2625732
|
||||
c1.1436462,3.1705894,1.7570648,6.1727562,2.4672089,9.6485634l0.3283997,1.5989399
|
||||
c1.5871429,7.2945404,2.0792542,12.0144424,0.7978668,20.6905251c-0.1000977,0.7917252-0.2612,1.6092491-0.4320679,2.4743729
|
||||
c-0.4446869,2.2548256-0.9048615,4.5861511-0.2097168,6.4350777c0.3267212,1.2168083,2.2310638,2.5146446,3.4907684,3.3735085
|
||||
c0.5576477,0.3801651,0.9606323,0.6544037,1.1574707,0.8635788c0.4811401,0.5097427,0.6005402,0.8753738,0.6124573,0.9156418
|
||||
l0.1160583,0.3862991H206.8599548z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 39 KiB |
3
src/assets/GhIcon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.0001 0.296387C5.3735 0.296387 0 5.66889 0 12.2965C0 17.5984 3.43839 22.0966 8.2064 23.6833C8.80613 23.7944 9.0263 23.423 9.0263 23.1061C9.0263 22.8199 9.01518 21.8746 9.01001 20.8719C5.67157 21.5978 4.96712 19.456 4.96712 19.456C4.42125 18.069 3.63473 17.7002 3.63473 17.7002C2.54596 16.9554 3.7168 16.9707 3.7168 16.9707C4.92181 17.0554 5.55632 18.2073 5.55632 18.2073C6.6266 20.0419 8.36359 19.5115 9.04836 19.2049C9.15607 18.4293 9.46706 17.8999 9.81024 17.6002C7.14486 17.2968 4.34295 16.2678 4.34295 11.6697C4.34295 10.3596 4.81172 9.28911 5.57937 8.44874C5.45477 8.14649 5.04402 6.92597 5.69562 5.27305C5.69562 5.27305 6.70331 4.95053 8.9965 6.5031C9.95372 6.23722 10.9803 6.10388 12.0001 6.09931C13.0199 6.10388 14.0473 6.23722 15.0063 6.5031C17.2967 4.95053 18.303 5.27305 18.303 5.27305C18.9562 6.92597 18.5452 8.14649 18.4206 8.44874C19.1901 9.28911 19.6557 10.3596 19.6557 11.6697C19.6557 16.2788 16.8484 17.2936 14.1762 17.5907C14.6067 17.9631 14.9902 18.6934 14.9902 19.8129C14.9902 21.4186 14.9763 22.7108 14.9763 23.1061C14.9763 23.4254 15.1923 23.7996 15.8006 23.6818C20.566 22.0932 24 17.5967 24 12.2965C24 5.66889 18.6273 0.296387 12.0001 0.296387ZM4.49443 17.3908C4.468 17.4504 4.3742 17.4683 4.28876 17.4273C4.20172 17.3882 4.15283 17.3069 4.18105 17.2471C4.20688 17.1857 4.30088 17.1686 4.38772 17.2098C4.47495 17.2489 4.52463 17.331 4.49443 17.3908ZM5.0847 17.9175C5.02747 17.9705 4.91559 17.9459 4.83968 17.862C4.76119 17.7784 4.74648 17.6665 4.80451 17.6126C4.86353 17.5596 4.97203 17.5844 5.05072 17.6681C5.12921 17.7527 5.14451 17.8638 5.0847 17.9175ZM5.48965 18.5914C5.41612 18.6424 5.2959 18.5945 5.22158 18.4878C5.14805 18.3811 5.14805 18.2531 5.22317 18.2019C5.29769 18.1506 5.41612 18.1967 5.49144 18.3026C5.56476 18.4111 5.56476 18.5391 5.48965 18.5914ZM6.1745 19.3718C6.10873 19.4443 5.96863 19.4249 5.86609 19.3259C5.76117 19.2291 5.73196 19.0918 5.79793 19.0193C5.8645 18.9466 6.00539 18.967 6.10873 19.0652C6.21285 19.1618 6.24465 19.3001 6.1745 19.3718ZM7.05961 19.6353C7.0306 19.7293 6.89567 19.772 6.75975 19.7321C6.62402 19.6909 6.5352 19.5808 6.56262 19.4858C6.59084 19.3913 6.72636 19.3467 6.86328 19.3895C6.9988 19.4304 7.08783 19.5397 7.05961 19.6353ZM8.0669 19.747C8.07028 19.846 7.95502 19.9281 7.81235 19.9299C7.66887 19.933 7.55282 19.853 7.55123 19.7556C7.55123 19.6556 7.6639 19.5744 7.80738 19.572C7.95006 19.5692 8.0669 19.6487 8.0669 19.747ZM9.05645 19.7091C9.07354 19.8057 8.97438 19.9048 8.8327 19.9313C8.6934 19.9567 8.56443 19.8971 8.54674 19.8013C8.52945 19.7024 8.6304 19.6032 8.7695 19.5776C8.91139 19.5529 9.03837 19.6109 9.05645 19.7091Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 34 KiB |
@ -1,9 +0,0 @@
|
||||
<svg width="511" height="152" viewBox="0 0 511 152" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M78.1696 124.2C51.521 124.2 22.8867 119.2 22.8867 108.2C22.8867 97.2002 51.521 92.2002 78.1696 92.2002C104.818 92.2002 133.452 97.2002 133.452 108.2C133.557 119.2 104.818 124.2 78.1696 124.2ZM78.1696 98.5002C64.584 98.5002 51.8345 99.8002 42.2201 102.2C31.7696 104.8 29.4705 107.6 29.4705 108.2C29.4705 108.8 31.7696 111.6 42.2201 114.2C51.73 116.6 64.584 117.9 78.1696 117.9C91.7552 117.9 104.505 116.6 114.119 114.2C124.57 111.6 126.869 108.8 126.869 108.2C126.869 107.6 124.57 104.8 114.119 102.2C104.609 99.8002 91.7552 98.5002 78.1696 98.5002Z" fill="white"/>
|
||||
<path d="M78.1696 59.5C51.521 59.5 22.8867 54.5 22.8867 43.5C22.8867 32.5 51.521 27.5 78.1696 27.5C104.818 27.5 133.452 32.5 133.452 43.5C133.557 54.5 104.818 59.5 78.1696 59.5ZM78.1696 33.8C64.584 33.8 51.8345 35.1 42.2201 37.5C31.7696 40.1 29.4705 42.9 29.4705 43.5C29.4705 44.1 31.7696 46.9 42.2201 49.5C51.73 51.9 64.584 53.2 78.1696 53.2C91.7552 53.2 104.505 51.9 114.119 49.5C124.57 46.9 126.869 44.1 126.869 43.5C126.869 42.9 124.57 40.1 114.119 37.5C104.609 35.1 91.7552 33.8 78.1696 33.8Z" fill="white"/>
|
||||
<path d="M124.151 71.2998C86.4253 113.6 63.0163 119.4 57.7911 119.4C57.4776 119.4 29.3658 113.5 29.2613 113.5L28.3208 108.2C50.7893 105.9 92.0686 98.6998 124.151 71.2998Z" fill="white"/>
|
||||
<path d="M86.1118 87.3C93.9497 83.8 113.387 66.5 111.193 54.5L133.452 43.5C133.557 72 92.6956 83.9 86.1118 87.3Z" fill="white"/>
|
||||
<path d="M195.6 101.4H241.4V97.6002C241.4 93.4002 241.9 90.7002 242.8 89.5002C243.8 88.3002 245.5 87.6002 248 87.6002C250.1 87.6002 251.7 88.1002 252.8 89.1002C253.8 90.1002 254.4 91.5002 254.4 93.5002V105.6C254.4 107.8 253.6 109.4 252 110.3C250.4 111.3 247.6 111.7 243.5 111.7H187.8C184.4 111.7 181.8 111.1 180 110C178.2 108.9 177.3 107.2 177.3 105.1C177.3 103.8 177.7 102.5 178.4 101.4C179.2 100.2 181 98.5002 183.8 96.3002L236.4 54.5002H192.5V58.2002C192.5 62.5002 192 65.2002 191.1 66.4002C190.1 67.6002 188.4 68.2002 185.9 68.2002C183.7 68.2002 182 67.7002 180.9 66.7002C179.8 65.7002 179.2 64.3002 179.2 62.3002V50.0002C179.2 48.0002 180.2 46.5002 182.3 45.6002C184.4 44.7002 187.6 44.2002 192 44.2002H241.2C245.5 44.2002 248.7 44.8002 250.9 46.1002C253.1 47.3002 254.2 49.2002 254.2 51.6002C254.2 53.0002 253.5 54.6002 252.1 56.4002C250.7 58.2002 248.7 60.1002 245.9 62.2002L195.6 101.4Z" fill="white"/>
|
||||
<path d="M376.9 77.9996C376.9 88.3996 372.4 96.9996 363.5 103.6C354.5 110.2 342.9 113.5 328.6 113.5C314.4 113.5 302.8 110.2 293.8 103.6C284.8 96.9996 280.4 88.3996 280.4 77.9996C280.4 67.5996 284.9 59.0996 293.8 52.4996C302.8 45.8996 314.3 42.5996 328.6 42.5996C342.8 42.5996 354.4 45.8996 363.4 52.4996C372.4 59.0996 376.9 67.5996 376.9 77.9996ZM328.6 103.2C338 103.2 345.8 100.8 351.8 96.0996C357.8 91.3996 360.9 85.2996 360.9 77.9996C360.9 70.6996 357.9 64.5996 351.8 59.7996C345.7 54.9996 338 52.5996 328.6 52.5996C319.2 52.5996 311.5 54.9996 305.5 59.7996C299.5 64.5996 296.4 70.6996 296.4 77.9996C296.4 85.3996 299.4 91.4996 305.4 96.1996C311.5 100.8 319.2 103.2 328.6 103.2Z" fill="white"/>
|
||||
<path d="M466.8 48.2998C469.8 48.2998 472 48.6998 473.4 49.4998C474.8 50.2998 475.5 51.5998 475.5 53.3998C475.5 55.0998 474.9 56.3998 473.6 57.1998C472.3 58.0998 470.4 58.4998 467.8 58.4998H430.5V82.9998C430.5 90.9998 431.8 96.2998 434.4 98.7998C437 101.3 441.5 102.6 447.8 102.6C453.4 102.6 460.1 101.3 467.9 98.6998C475.7 96.0998 480.5 94.7998 482.3 94.7998C484 94.7998 485.5 95.2998 486.7 96.2998C487.9 97.2998 488.5 98.4998 488.5 99.8998C488.5 101.5 487.7 102.9 486.2 104.1C484.7 105.3 482.1 106.5 478.5 107.6C472.5 109.5 467.1 110.9 462.3 111.8C457.5 112.7 453 113.1 448.7 113.1C441.5 113.1 435.5 112.2 430.6 110.4C425.7 108.6 422.1 105.9 419.7 102.3C418.5 100.6 417.6 98.5998 417.1 96.1998C416.6 93.8998 416.3 90.2998 416.3 85.3998V82.9998V58.5998H400.7C398.1 58.5998 396.3 58.1998 395.1 57.3998C393.9 56.5998 393.4 55.2998 393.4 53.4998C393.4 51.4998 394.2 50.0998 395.8 49.3998C397.4 48.6998 400.8 48.2998 406 48.2998H416.4V33.2998V29.3998C416.4 27.2998 417 25.7998 418.1 24.7998C419.2 23.7998 421 23.2998 423.4 23.2998C426.1 23.2998 428 23.8998 429 25.0998C430 26.2998 430.6 29.0998 430.6 33.3998V48.1998H466.8V48.2998Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 86 KiB |
@ -1,6 +0,0 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M29.6046 49.1571C19.5123 49.1571 8.66797 47.1782 8.66797 42.8246C8.66797 38.4711 19.5123 36.4922 29.6046 36.4922C39.697 36.4922 50.5413 38.4711 50.5413 42.8246C50.5809 47.1782 39.697 49.1571 29.6046 49.1571ZM29.6046 38.9856C24.4595 38.9856 19.631 39.5001 15.9899 40.45C12.0321 41.479 11.1614 42.5872 11.1614 42.8246C11.1614 43.0621 12.0321 44.1703 15.9899 45.1993C19.5915 46.1492 24.4595 46.6637 29.6046 46.6637C34.7498 46.6637 39.5783 46.1492 43.2194 45.1993C47.1772 44.1703 48.0479 43.0621 48.0479 42.8246C48.0479 42.5872 47.1772 41.479 43.2194 40.45C39.6178 39.5001 34.7498 38.9856 29.6046 38.9856Z" fill="white"/>
|
||||
<path d="M29.6046 23.5477C19.5123 23.5477 8.66797 21.5688 8.66797 17.2153C8.66797 12.8617 19.5123 10.8828 29.6046 10.8828C39.697 10.8828 50.5413 12.8617 50.5413 17.2153C50.5809 21.5688 39.697 23.5477 29.6046 23.5477ZM29.6046 13.3762C24.4595 13.3762 19.631 13.8907 15.9899 14.8406C12.0321 15.8696 11.1614 16.9778 11.1614 17.2153C11.1614 17.4527 12.0321 18.5609 15.9899 19.5899C19.5915 20.5398 24.4595 21.0543 29.6046 21.0543C34.7498 21.0543 39.5783 20.5398 43.2194 19.5899C47.1772 18.5609 48.0479 17.4527 48.0479 17.2153C48.0479 16.9778 47.1772 15.8696 43.2194 14.8406C39.6178 13.8907 34.7498 13.3762 29.6046 13.3762Z" fill="white"/>
|
||||
<path d="M47.0194 28.2188C32.7318 44.9602 23.8664 47.2557 21.8875 47.2557C21.7688 47.2557 11.1223 44.9206 11.0828 44.9206L10.7266 42.823C19.2358 41.9127 34.869 39.0631 47.0194 28.2188Z" fill="white"/>
|
||||
<path d="M32.6133 34.5499C35.5816 33.1647 42.9431 26.3177 42.112 21.5684L50.542 17.2148C50.5816 28.4945 35.1067 33.2043 32.6133 34.5499Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 99 KiB |
@ -1,6 +0,0 @@
|
||||
<svg width="1000" height="1000" viewBox="0 0 1000 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M493.406 819.261C325.2 819.261 144.461 786.28 144.461 713.721C144.461 641.161 325.2 608.18 493.406 608.18C661.611 608.18 842.35 641.161 842.35 713.721C843.01 786.28 661.611 819.261 493.406 819.261ZM493.406 649.736C407.654 649.736 327.179 658.312 266.493 674.143C200.53 691.293 186.018 709.763 186.018 713.721C186.018 717.678 200.53 736.148 266.493 753.298C326.519 769.13 407.654 777.705 493.406 777.705C579.158 777.705 659.632 769.13 720.318 753.298C786.281 736.148 800.793 717.678 800.793 713.721C800.793 709.763 786.281 691.293 720.318 674.143C660.292 658.312 579.158 649.736 493.406 649.736Z" fill="#0F2139"/>
|
||||
<path d="M493.406 392.48C325.2 392.48 144.461 359.499 144.461 286.939C144.461 214.38 325.2 181.398 493.406 181.398C661.611 181.398 842.35 214.38 842.35 286.939C843.01 359.499 661.611 392.48 493.406 392.48ZM493.406 222.955C407.654 222.955 327.179 231.53 266.493 247.361C200.53 264.512 186.018 282.982 186.018 286.939C186.018 290.897 200.53 309.367 266.493 326.517C326.519 342.348 407.654 350.924 493.406 350.924C579.158 350.924 659.632 342.348 720.318 326.517C786.281 309.367 800.793 290.897 800.793 286.939C800.793 282.982 786.281 264.512 720.318 247.361C660.292 231.53 579.158 222.955 493.406 222.955Z" fill="#0F2139"/>
|
||||
<path d="M783.643 470.316C545.516 749.34 397.759 787.599 364.778 787.599C362.799 787.599 185.358 748.68 184.698 748.68L178.762 713.72C320.582 698.549 581.136 651.055 783.643 470.316Z" fill="#0F2139"/>
|
||||
<path d="M543.535 575.858C593.007 552.771 715.699 438.654 701.846 359.499L842.348 286.939C843.007 474.934 585.092 553.43 543.535 575.858Z" fill="#0F2139"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/altlinux-logo.gif
Normal file
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 4.6 MiB |
Before Width: | Height: | Size: 26 KiB |
93
src/assets/fonts/LICENSE
Normal file
@ -0,0 +1,93 @@
|
||||
Copyright © 2009 ParaType Ltd. All rights reserved.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
src/assets/fonts/pt-sans-cyrillic-400-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-400-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-400-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-400-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-700-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-700-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-700-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-700-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-400-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-400-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-400-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-400-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-700-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-700-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-700-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-700-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-400-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-400-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-400-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-400-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-700-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-700-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-700-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-700-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-400-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-400-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-400-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-400-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-700-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-700-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-700-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-700-normal.woff2
Normal file
15
src/assets/noData.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="235" height="240" viewBox="0 0 235 240" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="34.8711" y="16.2017" width="36" height="249.944" rx="18" transform="rotate(-26.7465 34.8711 16.2017)" fill="#F15527" fill-opacity="0.2"/>
|
||||
<rect x="156.871" y="36.2017" width="36" height="100.235" rx="18" transform="rotate(-26.7465 156.871 36.2017)" fill="#F15527" fill-opacity="0.2"/>
|
||||
<rect x="0.871094" y="138.037" width="26.745" height="74.4663" rx="13.3725" transform="rotate(-26.7465 0.871094 138.037)" fill="#F15527" fill-opacity="0.2"/>
|
||||
<g clip-path="url(#clip0_2865_33046)">
|
||||
<path d="M117.5 199C161.225 199 197 163.225 197 119.5C197 75.775 161.225 40 117.5 40C73.775 40 38 75.775 38 119.5C38 163.225 73.775 199 117.5 199ZM119.885 82.0555C121.475 80.6245 123.383 79.75 125.45 79.75C127.596 79.75 129.425 80.6245 131.094 82.0555C132.605 83.725 133.4 85.633 133.4 87.7C133.4 89.8465 132.605 91.675 131.094 93.3445C129.425 94.855 127.596 95.65 125.45 95.65C123.383 95.65 121.475 94.855 119.885 93.3445C118.375 91.675 117.5 89.8465 117.5 87.7C117.5 85.633 118.375 83.725 119.885 82.0555ZM100.01 119.261C100.01 119.261 117.261 105.588 123.542 105.031C129.425 104.554 128.232 111.311 127.676 114.809L127.597 115.286C126.484 119.5 125.132 124.588 123.78 129.438C120.759 140.488 117.818 151.3 118.534 153.287C119.328 155.991 124.257 152.572 127.835 150.187C128.312 149.869 128.709 149.551 129.107 149.312C129.107 149.312 129.743 148.676 130.379 149.551C130.538 149.789 130.697 150.028 130.856 150.187C131.572 151.3 131.969 151.697 131.015 152.333L130.697 152.492C128.948 153.685 121.475 158.932 118.454 160.84C115.194 162.987 102.713 170.142 104.621 156.229C106.291 146.451 108.517 138.023 110.266 131.425C113.525 119.5 114.956 114.094 107.642 118.785C104.701 120.534 102.951 121.646 101.918 122.362C101.043 122.998 100.964 122.998 100.408 121.965L100.169 121.487L99.7715 120.852C99.215 120.057 99.215 119.977 100.01 119.261Z" fill="#0F2139"/>
|
||||
<path d="M119.885 82.0555C121.475 80.6245 123.383 79.75 125.45 79.75C127.596 79.75 129.425 80.6245 131.094 82.0555C132.605 83.725 133.4 85.633 133.4 87.7C133.4 89.8465 132.605 91.675 131.094 93.3445C129.425 94.855 127.596 95.65 125.45 95.65C123.383 95.65 121.475 94.855 119.885 93.3445C118.375 91.675 117.5 89.8465 117.5 87.7C117.5 85.633 118.375 83.725 119.885 82.0555Z" fill="white"/>
|
||||
<path d="M100.01 119.261C100.01 119.261 117.261 105.588 123.542 105.031C129.425 104.554 128.232 111.311 127.676 114.809L127.597 115.286C126.484 119.5 125.132 124.588 123.78 129.438C120.759 140.488 117.818 151.3 118.534 153.287C119.328 155.991 124.257 152.572 127.835 150.187C128.312 149.869 128.709 149.551 129.107 149.312C129.107 149.312 129.743 148.676 130.379 149.551C130.538 149.789 130.697 150.028 130.856 150.187C131.572 151.3 131.969 151.697 131.015 152.333L130.697 152.492C128.948 153.685 121.475 158.932 118.454 160.84C115.194 162.987 102.713 170.142 104.621 156.229C106.291 146.451 108.517 138.023 110.266 131.425C113.525 119.5 114.956 114.094 107.642 118.785C104.701 120.534 102.951 121.646 101.918 122.362C101.043 122.998 100.964 122.998 100.408 121.965L100.169 121.487L99.7715 120.852C99.215 120.057 99.215 119.977 100.01 119.261Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2865_33046">
|
||||
<rect width="159" height="159" fill="white" transform="translate(38 40)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 28 KiB |
@ -1,18 +0,0 @@
|
||||
<svg width="489" height="152" viewBox="0 0 489 152" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1434_24034)">
|
||||
<path d="M75.8 151.6C117.663 151.6 151.6 117.663 151.6 75.8C151.6 33.9368 117.663 0 75.8 0C33.9368 0 0 33.9368 0 75.8C0 117.663 33.9368 151.6 75.8 151.6Z" fill="#231F20"/>
|
||||
<path d="M74.7999 124.2C49.2999 124.2 21.8999 119.2 21.8999 108.2C21.8999 97.2002 49.2999 92.2002 74.7999 92.2002C100.3 92.2002 127.7 97.2002 127.7 108.2C127.8 119.2 100.3 124.2 74.7999 124.2ZM74.7999 98.5002C61.7999 98.5002 49.5999 99.8002 40.3999 102.2C30.3999 104.8 28.1999 107.6 28.1999 108.2C28.1999 108.8 30.3999 111.6 40.3999 114.2C49.4999 116.6 61.7999 117.9 74.7999 117.9C87.7999 117.9 99.9999 116.6 109.2 114.2C119.2 111.6 121.4 108.8 121.4 108.2C121.4 107.6 119.2 104.8 109.2 102.2C100.1 99.8002 87.7999 98.5002 74.7999 98.5002Z" fill="white"/>
|
||||
<path d="M46.8999 100.8C55.0999 99.4998 75.0999 93.3998 84.3999 89.2998L103.3 92.0998C99.3999 95.6998 96.6999 98.8998 93.1999 101.8L46.8999 100.8Z" fill="#231F20"/>
|
||||
<path d="M74.7999 59.5C49.2999 59.5 21.8999 54.5 21.8999 43.5C21.8999 32.5 49.2999 27.5 74.7999 27.5C100.3 27.5 127.7 32.5 127.7 43.5C127.8 54.5 100.3 59.5 74.7999 59.5ZM74.7999 33.8C61.7999 33.8 49.5999 35.1 40.3999 37.5C30.3999 40.1 28.1999 42.9 28.1999 43.5C28.1999 44.1 30.3999 46.9 40.3999 49.5C49.4999 51.9 61.7999 53.2 74.7999 53.2C87.7999 53.2 99.9999 51.9 109.2 49.5C119.2 46.9 121.4 44.1 121.4 43.5C121.4 42.9 119.2 40.1 109.2 37.5C100.1 35.1 87.7999 33.8 74.7999 33.8Z" fill="white"/>
|
||||
<path d="M118.8 71.2998C82.7001 113.6 60.3001 119.4 55.3001 119.4C55.0001 119.4 28.1001 113.5 28.0001 113.5L27.1001 108.2C48.6001 105.9 88.1001 98.6998 118.8 71.2998Z" fill="white"/>
|
||||
<path d="M82.3999 87.3C89.8999 83.8 108.5 66.5 106.4 54.5L127.7 43.5C127.8 72 88.6999 83.9 82.3999 87.3Z" fill="white"/>
|
||||
<path d="M195.6 99.4002H241.4V95.6002C241.4 91.4002 241.9 88.7002 242.8 87.5002C243.8 86.3002 245.5 85.6002 248 85.6002C250.1 85.6002 251.7 86.1002 252.8 87.1002C253.8 88.1002 254.4 89.5002 254.4 91.5002V103.6C254.4 105.8 253.6 107.4 252 108.3C250.4 109.3 247.6 109.7 243.5 109.7H187.8C184.4 109.7 181.8 109.1 180 108C178.2 106.9 177.3 105.2 177.3 103.1C177.3 101.8 177.7 100.5 178.4 99.4002C179.2 98.2002 181 96.5002 183.8 94.3002L236.4 52.5002H192.5V56.2002C192.5 60.5002 192 63.2002 191.1 64.4002C190.1 65.6002 188.4 66.2002 185.9 66.2002C183.7 66.2002 182 65.7002 180.9 64.7002C179.8 63.7002 179.2 62.3002 179.2 60.3002V48.0002C179.2 46.0002 180.2 44.5002 182.3 43.6002C184.4 42.7002 187.6 42.2002 192 42.2002H241.2C245.5 42.2002 248.7 42.8002 250.9 44.1002C253.1 45.3002 254.2 47.2002 254.2 49.6002C254.2 51.0002 253.5 52.6002 252.1 54.4002C250.7 56.2002 248.7 58.1002 245.9 60.2002L195.6 99.4002Z" fill="#231F20"/>
|
||||
<path d="M376.9 75.9996C376.9 86.3996 372.4 94.9996 363.5 101.6C354.5 108.2 342.9 111.5 328.6 111.5C314.4 111.5 302.8 108.2 293.8 101.6C284.8 94.9996 280.4 86.3996 280.4 75.9996C280.4 65.5996 284.9 57.0996 293.8 50.4996C302.8 43.8996 314.3 40.5996 328.6 40.5996C342.8 40.5996 354.4 43.8996 363.4 50.4996C372.4 57.0996 376.9 65.5996 376.9 75.9996ZM328.6 101.2C338 101.2 345.8 98.7996 351.8 94.0996C357.8 89.3996 360.9 83.2996 360.9 75.9996C360.9 68.6996 357.9 62.5996 351.8 57.7996C345.7 52.9996 338 50.5996 328.6 50.5996C319.2 50.5996 311.5 52.9996 305.5 57.7996C299.5 62.5996 296.4 68.6996 296.4 75.9996C296.4 83.3996 299.4 89.4996 305.4 94.1996C311.5 98.7996 319.2 101.2 328.6 101.2Z" fill="#231F20"/>
|
||||
<path d="M466.8 46.2998C469.8 46.2998 472 46.6998 473.4 47.4998C474.8 48.2998 475.5 49.5998 475.5 51.3998C475.5 53.0998 474.9 54.3998 473.6 55.1998C472.3 56.0998 470.4 56.4998 467.8 56.4998H430.5V80.9998C430.5 88.9998 431.8 94.2998 434.4 96.7998C437 99.2998 441.5 100.6 447.8 100.6C453.4 100.6 460.1 99.2998 467.9 96.6998C475.7 94.0998 480.5 92.7998 482.3 92.7998C484 92.7998 485.5 93.2998 486.7 94.2998C487.9 95.2998 488.5 96.4998 488.5 97.8998C488.5 99.4998 487.7 100.9 486.2 102.1C484.7 103.3 482.1 104.5 478.5 105.6C472.5 107.5 467.1 108.9 462.3 109.8C457.5 110.7 453 111.1 448.7 111.1C441.5 111.1 435.5 110.2 430.6 108.4C425.7 106.6 422.1 103.9 419.7 100.3C418.5 98.5998 417.6 96.5998 417.1 94.1998C416.6 91.8998 416.3 88.2998 416.3 83.3998V80.9998V56.5998H400.7C398.1 56.5998 396.3 56.1998 395.1 55.3998C393.9 54.5998 393.4 53.2998 393.4 51.4998C393.4 49.4998 394.2 48.0998 395.8 47.3998C397.4 46.6998 400.8 46.2998 406 46.2998H416.4V31.2998V27.3998C416.4 25.2998 417 23.7998 418.1 22.7998C419.2 21.7998 421 21.2998 423.4 21.2998C426.1 21.2998 428 21.8998 429 23.0998C430 24.2998 430.6 27.0998 430.6 31.3998V46.1998H466.8V46.2998Z" fill="#231F20"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1434_24034">
|
||||
<rect width="488.4" height="151.6" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 12 KiB |
@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 460 460" style="enable-background:new 0 0 460 460;" xml:space="preserve">
|
||||
<g id="XMLID_1135_">
|
||||
<polygon id="XMLID_1136_" style="fill:#FB992D;" points="200.002,210 230.002,460 430.002,345 430.002,115 "/>
|
||||
<polygon id="XMLID_1137_" style="fill:#FFB739;" points="230.001,200 230.001,460 30.001,345 30.001,115 "/>
|
||||
<polygon id="XMLID_1138_" style="fill:#FB992D;" points="29.998,115 99.913,155.199 232.373,116.28 299.907,40.193 229.998,0 "/>
|
||||
<polygon id="XMLID_1139_" style="fill:#F67A21;" points="160.096,189.804 229.998,230 429.998,115 360.098,74.798 226.657,114.279
|
||||
"/>
|
||||
<polygon id="XMLID_1140_" style="fill:#FFEAC3;" points="160.096,289.803 99.913,255.199 99.913,155.199 157.924,159.73
|
||||
160.096,189.804 "/>
|
||||
<polygon id="XMLID_1141_" style="fill:#FFD488;" points="99.913,155.199 299.907,40.193 360.098,74.798 160.096,189.804 "/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.3 KiB |
@ -17,7 +17,7 @@ import { host } from '../../host';
|
||||
import { mapToRepo } from 'utilities/objectModels.js';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import FilterCard from '../Shared/FilterCard.jsx';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, isNil } from 'lodash';
|
||||
import filterConstants from 'utilities/filterConstants.js';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria.js';
|
||||
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants.js';
|
||||
@ -34,24 +34,19 @@ const useStyles = makeStyles((theme) => ({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
exploreText: {
|
||||
color: '#C0C0C0',
|
||||
display: 'flex',
|
||||
alignItems: 'left'
|
||||
},
|
||||
resultsRow: {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
color: '#00000099'
|
||||
alignItems: 'center'
|
||||
},
|
||||
results: {
|
||||
marginLeft: '1rem'
|
||||
marginLeft: '1rem',
|
||||
color: theme.palette.secondary.dark
|
||||
},
|
||||
sortForm: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#E0E0E0',
|
||||
borderRadius: '0.375em',
|
||||
width: '25%',
|
||||
width: '23%',
|
||||
textAlign: 'left'
|
||||
},
|
||||
filterButton: {
|
||||
@ -60,17 +55,22 @@ const useStyles = makeStyles((theme) => ({
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
filterCardsContainer: {
|
||||
[theme.breakpoints.down('md')]: {
|
||||
display: 'none'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
function Explore() {
|
||||
function Explore({ searchInputValue }) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [exploreData, setExploreData] = useState([]);
|
||||
const [sortFilter, setSortFilter] = useState(sortByCriteria.relevance.value);
|
||||
const [queryParams] = useSearchParams();
|
||||
const search = queryParams.get('search');
|
||||
// filtercard filters
|
||||
const [imageFilters, setImageFilters] = useState(false);
|
||||
const [imageFilters, setImageFilters] = useState({});
|
||||
const [osFilters, setOSFilters] = useState([]);
|
||||
const [archFilters, setArchFilters] = useState([]);
|
||||
// pagination props
|
||||
@ -88,8 +88,8 @@ function Explore() {
|
||||
let filter = {};
|
||||
filter = !isEmpty(osFilters) ? { ...filter, Os: osFilters } : filter;
|
||||
filter = !isEmpty(archFilters) ? { ...filter, Arch: archFilters } : filter;
|
||||
if (imageFilters) {
|
||||
filter = { ...filter, HasToBeSigned: imageFilters };
|
||||
if (!isEmpty(Object.keys(imageFilters))) {
|
||||
filter = { ...filter, ...imageFilters };
|
||||
}
|
||||
return filter;
|
||||
};
|
||||
@ -101,6 +101,8 @@ function Explore() {
|
||||
setOSFilters([...osFilters, preselectedFilter]);
|
||||
} else if (filterConstants.archFilters.map((f) => f.value).includes(preselectedFilter)) {
|
||||
setArchFilters([...archFilters, preselectedFilter]);
|
||||
} else if (filterConstants.imageFilters.map((f) => f.value).includes(preselectedFilter)) {
|
||||
setImageFilters({ ...imageFilters, [preselectedFilter]: true });
|
||||
}
|
||||
queryParams.delete('filter');
|
||||
}
|
||||
@ -119,7 +121,7 @@ function Explore() {
|
||||
api
|
||||
.get(
|
||||
`${host()}${endpoints.globalSearch({
|
||||
searchQuery: search,
|
||||
searchQuery: !isNil(searchInputValue) ? searchInputValue : search,
|
||||
pageNumber,
|
||||
pageSize: EXPLORE_PAGE_SIZE,
|
||||
sortBy: sortFilter,
|
||||
@ -218,7 +220,10 @@ function Explore() {
|
||||
version={item.latestVersion}
|
||||
description={item.description}
|
||||
downloads={item.downloads}
|
||||
isSigned={item.isSigned}
|
||||
stars={item.stars}
|
||||
signatureInfo={item.signatureInfo}
|
||||
isBookmarked={item.isBookmarked}
|
||||
isStarred={item.isStarred}
|
||||
vendor={item.vendor}
|
||||
platforms={item.platforms}
|
||||
key={index}
|
||||
@ -269,7 +274,7 @@ function Explore() {
|
||||
if (!isLoading && !isEndOfList) {
|
||||
return <div ref={listBottom} />;
|
||||
}
|
||||
return '';
|
||||
return;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -311,7 +316,7 @@ function Explore() {
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container item xs={12} spacing={5} pt={1}>
|
||||
<Grid item xs={3} md={3} className="hide-on-mobile">
|
||||
<Grid item xs={3} md={3} className={classes.filterCardsContainer}>
|
||||
<Sticky>{renderFilterCards()}</Sticky>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={9}>
|
||||
@ -324,7 +329,7 @@ function Explore() {
|
||||
</div>
|
||||
</Grid>
|
||||
) : (
|
||||
<Stack direction="column" spacing={{ xs: 4, md: 2 }}>
|
||||
<Stack direction="column">
|
||||
{renderRepoCards()}
|
||||
{renderListBottom()}
|
||||
</Stack>
|
||||
|
@ -13,25 +13,31 @@ import React from 'react';
|
||||
const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
exploreHeader: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: 'transparent',
|
||||
minHeight: 50,
|
||||
paddingLeft: '3rem',
|
||||
padding: '2.75rem 0 1.25rem 0',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '2rem',
|
||||
justifyContent: 'flex-start',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '1rem'
|
||||
}
|
||||
},
|
||||
explore: {
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '0.813rem',
|
||||
fontWeight: '600',
|
||||
letterSpacing: '0.009375rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
fontSize: '0.8rem'
|
||||
}
|
||||
},
|
||||
arrowIcon: {
|
||||
color: theme.palette.secondary.dark,
|
||||
marginRight: '1.75rem',
|
||||
fontSize: { xs: '1.5rem', md: '2rem' },
|
||||
cursor: 'pointer'
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -48,10 +54,7 @@ function ExploreHeader() {
|
||||
|
||||
return (
|
||||
<div className={classes.exploreHeader}>
|
||||
<ArrowBackIcon
|
||||
sx={{ color: '#14191F', fontSize: { xs: '1.5rem', md: '2rem' }, cursor: 'pointer' }}
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
<ArrowBackIcon className={classes.arrowIcon} onClick={() => navigate(-1)} />
|
||||
<Breadcrumbs separator="/" aria-label="breadcrumb">
|
||||
<Link to="/">
|
||||
<Typography variant="body1" className={classes.explore}>
|
||||
|
@ -1,18 +1,16 @@
|
||||
// react global
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
// components
|
||||
import { AppBar, Toolbar, Stack, Grid } from '@mui/material';
|
||||
|
||||
import { AppBar, Toolbar, Grid } from '@mui/material';
|
||||
import SearchSuggestion from './SearchSuggestion';
|
||||
// styling
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import logo from '../../assets/zotLogo.svg';
|
||||
import logoxs from '../../assets/zotLogoSmall.png';
|
||||
import { useState, useEffect } from 'react';
|
||||
import SearchSuggestion from './SearchSuggestion';
|
||||
import logo from '!file-loader!../../assets/Alt_Linux_Team.svg';
|
||||
import { invert } from 'lodash';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
barOpen: {
|
||||
position: 'sticky',
|
||||
minHeight: '10%'
|
||||
@ -28,11 +26,11 @@ const useStyles = makeStyles(() => ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 0,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: '#f5f5f5',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
borderBottom: '0.0625rem solid #BDBDBD',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)'
|
||||
boxShadow: '0 0.125rem 0.25rem -0.0625rem rgba(3,3,3,0.16)'
|
||||
},
|
||||
headerContainer: {
|
||||
minWidth: '60%'
|
||||
@ -42,7 +40,7 @@ const useStyles = makeStyles(() => ({
|
||||
paddingRight: '3%'
|
||||
},
|
||||
input: {
|
||||
color: '#464141',
|
||||
color: '#f5f5f5',
|
||||
marginLeft: 1,
|
||||
width: '90%'
|
||||
},
|
||||
@ -58,20 +56,47 @@ const useStyles = makeStyles(() => ({
|
||||
logoWrapper: {},
|
||||
logo: {
|
||||
maxWidth: '130px',
|
||||
maxHeight: '50px'
|
||||
maxHeight: '30px'
|
||||
},
|
||||
userAvatar: {
|
||||
height: 46,
|
||||
width: 46
|
||||
ghlogo: {
|
||||
maxWidth: '130px',
|
||||
maxHeight: '30px',
|
||||
color: invert
|
||||
// filter: 1
|
||||
},
|
||||
headerLinkContainer: {
|
||||
[theme.breakpoints.down('md')]: {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
link: {
|
||||
color: '#000'
|
||||
color: '#000000',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600
|
||||
},
|
||||
grid: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '2.875rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
justifyContent: 'space-between'
|
||||
}
|
||||
},
|
||||
gridItem: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
signInBtn: {
|
||||
border: '1px solid #F6F7F9',
|
||||
borderRadius: '0.625rem',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#F6F7F9',
|
||||
fontSize: '1rem',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600
|
||||
}
|
||||
}));
|
||||
|
||||
@ -103,32 +128,35 @@ function setNavShow() {
|
||||
return show;
|
||||
}
|
||||
|
||||
function Header() {
|
||||
function Header({ setSearchCurrentValue = () => {} }) {
|
||||
const show = setNavShow();
|
||||
const classes = useStyles();
|
||||
const path = useLocation().pathname;
|
||||
|
||||
return (
|
||||
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '10vh' }}>
|
||||
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '5rem' }}>
|
||||
<Toolbar className={classes.header}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" className={classes.headerContainer}>
|
||||
<Grid container className={classes.grid}>
|
||||
<Grid item xs={2} sx={{ display: 'flex', justifyContent: 'start' }}>
|
||||
<Link to="/home" className={classes.grid}>
|
||||
<Grid container className={classes.grid}>
|
||||
<Grid item container xs={3} md={4} spacing="1.5rem" className={classes.gridItem}>
|
||||
<Grid item>
|
||||
<Link to="/home">
|
||||
<picture>
|
||||
<source media="(min-width:600px)" srcSet={logo} />
|
||||
<img alt="zot" src={logoxs} className={classes.logo} />
|
||||
<img alt="ALT Linux" src={logo} className={classes.logo} />
|
||||
</picture>
|
||||
</Link>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
{path !== '/' && <SearchSuggestion />}
|
||||
</Grid>
|
||||
<Grid item md={2} xs={0}>
|
||||
<div>{''}</div>
|
||||
<Grid item className={classes.headerLinkContainer}>
|
||||
<a className={classes.link} href="https://www.altlinux.org/Registry" target="_blank" rel="noreferrer">
|
||||
Docs
|
||||
</a>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
<Grid item xs={6} md={4} className={classes.gridItem}>
|
||||
{path !== '/' && <SearchSuggestion setSearchCurrentValue={setSearchCurrentValue} />}
|
||||
</Grid>
|
||||
<Grid item container xs={2} md={3} spacing="1.5rem" className={`${classes.gridItem}`}></Grid>
|
||||
</Grid>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
|
@ -14,31 +14,30 @@ import { HEADER_SEARCH_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||
const useStyles = makeStyles(() => ({
|
||||
searchContainer: {
|
||||
display: 'inline-block',
|
||||
backgroundColor: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '2.5rem',
|
||||
minWidth: '60%',
|
||||
marginLeft: 16,
|
||||
backgroundColor: '#f5f5f5',
|
||||
boxShadow: '0 0.313rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '0.625rem',
|
||||
minWidth: '100%',
|
||||
position: 'relative',
|
||||
zIndex: 1150
|
||||
},
|
||||
searchContainerFocused: {
|
||||
backgroundColor: '#FFFFFF'
|
||||
},
|
||||
search: {
|
||||
position: 'relative',
|
||||
minWidth: '100%',
|
||||
flexDirection: 'row',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
border: '0.125rem solid #E7E7E7',
|
||||
borderRadius: '2.5rem',
|
||||
border: '0.063rem solid #8A96A8',
|
||||
borderRadius: '0.625rem',
|
||||
zIndex: 1155
|
||||
},
|
||||
searchFocused: {
|
||||
border: '0.125rem solid #E0E5EB',
|
||||
backgroundColor: '#FFFFF'
|
||||
},
|
||||
searchFailed: {
|
||||
position: 'relative',
|
||||
minWidth: '100%',
|
||||
flexDirection: 'row',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
border: '0.125rem solid #ff0303',
|
||||
borderRadius: '2.5rem',
|
||||
zIndex: 1155
|
||||
border: '0.125rem solid #ff0303'
|
||||
},
|
||||
resultsWrapper: {
|
||||
margin: '0',
|
||||
@ -47,28 +46,41 @@ const useStyles = makeStyles(() => ({
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: '#2B3A4E',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderBottomLeftRadius: '2.5rem',
|
||||
borderBottomRightRadius: '2.5rem',
|
||||
borderBottomLeftRadius: '0.625rem',
|
||||
borderBottomRightRadius: '0.625rem',
|
||||
// border: '0.125rem solid #E7E7E7',
|
||||
borderTop: 0,
|
||||
width: '100%',
|
||||
overflowY: 'auto',
|
||||
zIndex: 1
|
||||
},
|
||||
resultsWrapperFocused: {
|
||||
backgroundColor: '#FFFFFF'
|
||||
},
|
||||
resultsWrapperHidden: {
|
||||
display: 'none'
|
||||
},
|
||||
searchIcon: {
|
||||
color: '#52637A',
|
||||
color: '#000000',
|
||||
paddingRight: '3%',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
input: {
|
||||
color: '#464141',
|
||||
marginLeft: 1,
|
||||
width: '90%'
|
||||
width: '90%',
|
||||
paddingLeft: 10,
|
||||
height: '40px',
|
||||
fontSize: '1rem',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '0.625rem',
|
||||
color: '#8A96A8'
|
||||
},
|
||||
inputFocused: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: '0.625rem',
|
||||
color: 'rgba(0, 0, 0, 0.6);'
|
||||
},
|
||||
searchItem: {
|
||||
alignItems: 'center',
|
||||
@ -95,14 +107,14 @@ const useStyles = makeStyles(() => ({
|
||||
}
|
||||
}));
|
||||
|
||||
function SearchSuggestion() {
|
||||
const [queryParams, setQueryParams] = useSearchParams();
|
||||
const search = queryParams.get('search');
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState(search || '');
|
||||
function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [suggestionData, setSuggestionData] = useState([]);
|
||||
const [queryParams] = useSearchParams();
|
||||
const search = queryParams.get('search') || '';
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isFailedSearch, setIsFailedSearch] = useState(false);
|
||||
const [isComponentFocused, setIsComponentFocused] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
|
||||
@ -120,8 +132,14 @@ function SearchSuggestion() {
|
||||
|
||||
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() });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -174,13 +192,15 @@ function SearchSuggestion() {
|
||||
const handleSeachChange = (event) => {
|
||||
const value = event?.inputValue;
|
||||
setSearchQuery(value);
|
||||
// used to lift up the state for pages that need to know the current value of the search input (currently only Explore) not used in other cases
|
||||
// one way binding, other components shouldn't set the value of the search input, but using this prop can read it
|
||||
setSearchCurrentValue(value);
|
||||
setIsFailedSearch(false);
|
||||
setIsLoading(true);
|
||||
setSuggestionData([]);
|
||||
};
|
||||
|
||||
const searchCall = (value) => {
|
||||
setQueryParams((prevState) => createSearchParams({ ...prevState, search: searchQuery }));
|
||||
if (value !== '') {
|
||||
// if search term inclused the ':' character, search for images, if not, search repos
|
||||
if (value?.includes(':')) {
|
||||
@ -216,15 +236,18 @@ function SearchSuggestion() {
|
||||
getComboboxProps,
|
||||
isOpen,
|
||||
openMenu
|
||||
// closeMenu
|
||||
} = useCombobox({
|
||||
items: suggestionData,
|
||||
onInputValueChange: handleSeachChange,
|
||||
onSelectedItemChange: handleSuggestionSelected,
|
||||
initialInputValue: search ?? '',
|
||||
itemToString: (item) => item.name ?? item
|
||||
initialInputValue: !isEmpty(searchQuery) ? searchQuery : search,
|
||||
itemToString: (item) => item?.name || item
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsComponentFocused(isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
const renderSuggestions = () => {
|
||||
return suggestionData.map((suggestion, index) => (
|
||||
<ListItem
|
||||
@ -252,9 +275,11 @@ function SearchSuggestion() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.searchContainer}>
|
||||
<div className={`${classes.searchContainer} ${isComponentFocused && classes.searchContainerFocused}`}>
|
||||
<Stack
|
||||
className={isFailedSearch && !isLoading ? classes.searchFailed : classes.search}
|
||||
className={`${classes.search} ${isComponentFocused && classes.searchFocused} ${
|
||||
isFailedSearch && !isLoading && classes.searchFailed
|
||||
}`}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
@ -262,9 +287,9 @@ function SearchSuggestion() {
|
||||
{...getComboboxProps()}
|
||||
>
|
||||
<InputBase
|
||||
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
|
||||
placeholder={'Search for content...'}
|
||||
className={classes.input}
|
||||
className={`${classes.input} ${isComponentFocused && classes.inputFocused}`}
|
||||
sx={{ input: { '&::placeholder': { opacity: 1 } } }}
|
||||
onKeyUp={handleSearch}
|
||||
onFocus={() => openMenu()}
|
||||
{...getInputProps()}
|
||||
@ -275,14 +300,32 @@ function SearchSuggestion() {
|
||||
</Stack>
|
||||
<List
|
||||
{...getMenuProps()}
|
||||
className={isOpen && !isLoading && !isFailedSearch ? classes.resultsWrapper : classes.resultsWrapperHidden}
|
||||
className={
|
||||
isOpen && !isFailedSearch
|
||||
? `${classes.resultsWrapper} ${isComponentFocused && classes.resultsWrapperFocused}`
|
||||
: classes.resultsWrapperHidden
|
||||
}
|
||||
>
|
||||
{isOpen && suggestionData?.length > 0 && renderSuggestions()}
|
||||
{isOpen && isEmpty(searchQuery) && (
|
||||
{isOpen && isLoading && !isEmpty(searchQuery) && isEmpty(suggestionData) && (
|
||||
<>
|
||||
<ListItem
|
||||
className={classes.searchItem}
|
||||
style={{ color: '#52637A', fontSize: '1rem', textOverflow: 'ellipsis' }}
|
||||
style={{ color: '#000000', fontSize: '1rem', textOverflow: 'ellipsis' }}
|
||||
{...getItemProps({ item: '', index: 0 })}
|
||||
spacing={2}
|
||||
>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Typography>Loading...</Typography>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
{isOpen && isEmpty(searchQuery) && isEmpty(suggestionData) && (
|
||||
<>
|
||||
<ListItem
|
||||
className={classes.searchItem}
|
||||
style={{ color: '#000000', fontSize: '1rem', textOverflow: 'ellipsis' }}
|
||||
{...getItemProps({ item: '', index: 0 })}
|
||||
spacing={2}
|
||||
onClick={() => {}}
|
||||
@ -293,7 +336,7 @@ function SearchSuggestion() {
|
||||
</ListItem>
|
||||
<ListItem
|
||||
className={classes.searchItem}
|
||||
style={{ color: '#52637A', fontSize: '1rem', textOverflow: 'ellipsis' }}
|
||||
style={{ color: '#000000', fontSize: '1rem', textOverflow: 'ellipsis' }}
|
||||
{...getItemProps({ item: '', index: 0 })}
|
||||
spacing={2}
|
||||
onClick={() => {}}
|
||||
|
59
src/components/Header/UserAccountMenu.jsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material';
|
||||
|
||||
import { getLoggedInUser, logoutUser, isApiKeyEnabled } from '../../utilities/authUtilities';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const handleUserClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={handleUserClick}
|
||||
size="small"
|
||||
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>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={openMenu}
|
||||
onClose={handleUserClose}
|
||||
onClick={handleUserClose}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserAccountMenu;
|
@ -8,8 +8,16 @@ import { mapToRepo } from 'utilities/objectModels';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { useNavigate, createSearchParams } from 'react-router-dom';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||
import {
|
||||
HOME_POPULAR_PAGE_SIZE,
|
||||
HOME_RECENT_PAGE_SIZE,
|
||||
HOME_BOOKMARKS_PAGE_SIZE,
|
||||
HOME_STARS_PAGE_SIZE
|
||||
} from 'utilities/paginationConstants';
|
||||
import { isEmpty } from 'lodash';
|
||||
import NoDataComponent from 'components/Shared/NoDataComponent';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
gridWrapper: {
|
||||
marginTop: 10,
|
||||
marginBottom: '5rem'
|
||||
@ -30,17 +38,34 @@ const useStyles = makeStyles(() => ({
|
||||
},
|
||||
title: {
|
||||
fontWeight: '700',
|
||||
color: '#0F2139',
|
||||
color: '#000000',
|
||||
width: '100%',
|
||||
display: 'inline',
|
||||
fontSize: '2.5rem',
|
||||
textAlign: 'center',
|
||||
letterSpacing: '-0.02rem'
|
||||
},
|
||||
sectionHeaderContainer: {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
paddingTop: '1rem',
|
||||
marginBottom: '1rem',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
alignItems: 'flex-end',
|
||||
flexDirection: 'row'
|
||||
}
|
||||
},
|
||||
sectionTitle: {
|
||||
fontWeight: '700',
|
||||
color: '#000000DE',
|
||||
width: '100%'
|
||||
color: '#000000',
|
||||
width: '100%',
|
||||
fontSize: '2rem',
|
||||
textAlign: 'center',
|
||||
lineHeight: '2.375rem',
|
||||
letterSpacing: '-0.01rem',
|
||||
marginLeft: '0.5rem'
|
||||
},
|
||||
subtitle: {
|
||||
color: '#00000099',
|
||||
@ -52,57 +77,193 @@ const useStyles = makeStyles(() => ({
|
||||
width: '65%'
|
||||
},
|
||||
viewAll: {
|
||||
color: '#00000099',
|
||||
color: '#52637A',
|
||||
fontWeight: '600',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5rem',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left'
|
||||
marginRight: '0.5rem'
|
||||
}
|
||||
}));
|
||||
|
||||
function Home() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [homeData, setHomeData] = useState([]);
|
||||
const [popularData, setPopularData] = useState([]);
|
||||
const [isLoadingPopular, setIsLoadingPopular] = useState(true);
|
||||
const [recentData, setRecentData] = useState([]);
|
||||
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
|
||||
const [bookmarkData, setBookmarkData] = useState([]);
|
||||
const [isLoadingBookmarks, setIsLoadingBookmarks] = useState(true);
|
||||
const [starData, setStarData] = useState([]);
|
||||
const [isLoadingStars, setIsLoadingStars] = useState(true);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
const classes = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
setIsLoading(true);
|
||||
const getPopularData = () => {
|
||||
setIsLoadingPopular(true);
|
||||
api
|
||||
.get(`${host()}${endpoints.repoList()}`, abortController.signal)
|
||||
.get(
|
||||
`${host()}${endpoints.globalSearch({
|
||||
searchQuery: '',
|
||||
pageNumber: 1,
|
||||
pageSize: HOME_POPULAR_PAGE_SIZE,
|
||||
sortBy: sortByCriteria.downloads?.value
|
||||
})}`,
|
||||
abortController.signal
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let repoList = response.data.data.RepoListWithNewestImage.Results;
|
||||
let repoList = response.data.data.GlobalSearch.Repos;
|
||||
let repoData = repoList.map((responseRepo) => {
|
||||
return mapToRepo(responseRepo);
|
||||
});
|
||||
setHomeData(repoData);
|
||||
setPopularData(repoData);
|
||||
setIsLoading(false);
|
||||
setIsLoadingPopular(false);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setIsLoading(false);
|
||||
setIsLoadingPopular(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getRecentData = () => {
|
||||
setIsLoadingRecent(true);
|
||||
api
|
||||
.get(
|
||||
`${host()}${endpoints.globalSearch({
|
||||
searchQuery: '',
|
||||
pageNumber: 1,
|
||||
pageSize: HOME_RECENT_PAGE_SIZE,
|
||||
sortBy: sortByCriteria.updateTime?.value
|
||||
})}`,
|
||||
abortController.signal
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let repoList = response.data.data.GlobalSearch.Repos;
|
||||
let repoData = repoList.map((responseRepo) => {
|
||||
return mapToRepo(responseRepo);
|
||||
});
|
||||
setRecentData(repoData);
|
||||
setIsLoading(false);
|
||||
setIsLoadingRecent(false);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
setIsLoading(false);
|
||||
setIsLoadingRecent(false);
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const getBookmarks = () => {
|
||||
setIsLoadingBookmarks(true);
|
||||
api
|
||||
.get(
|
||||
`${host()}${endpoints.globalSearch({
|
||||
searchQuery: '',
|
||||
pageNumber: 1,
|
||||
pageSize: HOME_BOOKMARKS_PAGE_SIZE,
|
||||
sortBy: sortByCriteria.relevance?.value,
|
||||
filter: { IsBookmarked: true }
|
||||
})}`,
|
||||
abortController.signal
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let repoList = response.data.data.GlobalSearch.Repos;
|
||||
let repoData = repoList.map((responseRepo) => {
|
||||
return mapToRepo(responseRepo);
|
||||
});
|
||||
setBookmarkData(repoData);
|
||||
setIsLoading(false);
|
||||
setIsLoadingBookmarks(false);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
setIsLoading(false);
|
||||
setIsLoadingBookmarks(false);
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const getStars = () => {
|
||||
setIsLoadingStars(true);
|
||||
api
|
||||
.get(
|
||||
`${host()}${endpoints.globalSearch({
|
||||
searchQuery: '',
|
||||
pageNumber: 1,
|
||||
pageSize: HOME_STARS_PAGE_SIZE,
|
||||
sortBy: sortByCriteria.relevance?.value,
|
||||
filter: { IsStarred: true }
|
||||
})}`,
|
||||
abortController.signal
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let repoList = response.data.data.GlobalSearch.Repos;
|
||||
let repoData = repoList.map((responseRepo) => {
|
||||
return mapToRepo(responseRepo);
|
||||
});
|
||||
setStarData(repoData);
|
||||
setIsLoading(false);
|
||||
setIsLoadingStars(false);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
setIsLoading(false);
|
||||
setIsLoadingStars(false);
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
setIsLoading(true);
|
||||
getPopularData();
|
||||
getRecentData();
|
||||
getBookmarks();
|
||||
getStars();
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClickViewAll = (target) => {
|
||||
navigate({ pathname: `/explore`, search: createSearchParams({ sortby: target }).toString() });
|
||||
const handleClickViewAll = (type, value) => {
|
||||
navigate({ pathname: `/explore`, search: createSearchParams({ [type]: value }).toString() });
|
||||
};
|
||||
|
||||
const renderMostPopular = () => {
|
||||
const isNoData = () =>
|
||||
!isLoading &&
|
||||
!isLoadingBookmarks &&
|
||||
!isLoadingStars &&
|
||||
!isLoadingPopular &&
|
||||
!isLoadingRecent &&
|
||||
bookmarkData.length === 0 &&
|
||||
starData.length === 0 &&
|
||||
popularData.length === 0 &&
|
||||
recentData.length === 0;
|
||||
|
||||
const renderCards = (cardArray) => {
|
||||
return (
|
||||
homeData &&
|
||||
homeData.slice(0, 3).map((item, index) => {
|
||||
cardArray &&
|
||||
cardArray.map((item, index) => {
|
||||
return (
|
||||
<RepoCard
|
||||
name={item.name}
|
||||
version={item.latestVersion}
|
||||
description={item.description}
|
||||
downloads={item.downloads}
|
||||
isSigned={item.isSigned}
|
||||
stars={item.stars}
|
||||
signatureInfo={item.signatureInfo}
|
||||
isBookmarked={item.isBookmarked}
|
||||
isStarred={item.isStarred}
|
||||
vendor={item.vendor}
|
||||
platforms={item.platforms}
|
||||
key={index}
|
||||
@ -118,81 +279,89 @@ function Home() {
|
||||
);
|
||||
};
|
||||
|
||||
const renderRecentlyUpdated = () => {
|
||||
return (
|
||||
homeData &&
|
||||
homeData.slice(0, 2).map((item, index) => {
|
||||
return (
|
||||
<RepoCard
|
||||
name={item.name}
|
||||
version={item.latestVersion}
|
||||
description={item.description}
|
||||
downloads={item.downloads}
|
||||
isSigned={item.isSigned}
|
||||
vendor={item.vendor}
|
||||
platforms={item.platforms}
|
||||
key={index}
|
||||
vulnerabilityData={{
|
||||
vulnerabilitySeverity: item.vulnerabiltySeverity,
|
||||
count: item.vulnerabilityCount
|
||||
}}
|
||||
lastUpdated={item.lastUpdated}
|
||||
logo={item.logo}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Stack spacing={4} alignItems="center" className={classes.gridWrapper}>
|
||||
<Stack
|
||||
justifyContent="space-between"
|
||||
alignItems={{ xs: 'flex-start', md: 'flex-end' }}
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
sx={{ width: '100%', paddingTop: '3rem' }}
|
||||
>
|
||||
<div>
|
||||
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||
Most popular images
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.viewAll} onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}>
|
||||
<Typography variant="body2">View all</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
{renderMostPopular()}
|
||||
{/* currently most popular will be by downloads until stars are implemented */}
|
||||
<Stack
|
||||
justifyContent="space-between"
|
||||
alignItems={{ xs: 'flex-start', md: 'flex-end' }}
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
sx={{ width: '100%', paddingTop: '1rem' }}
|
||||
>
|
||||
<div>
|
||||
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||
Recently updated images
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className={classes.viewAll}
|
||||
onClick={() => handleClickViewAll(sortByCriteria.updateTime.value)}
|
||||
>
|
||||
View all
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
{renderRecentlyUpdated()}
|
||||
const renderContent = () => {
|
||||
return isNoData() === true ? (
|
||||
<NoDataComponent text="No images" />
|
||||
) : (
|
||||
<Stack alignItems="center" className={classes.gridWrapper}>
|
||||
<Stack className={classes.sectionHeaderContainer} sx={{ paddingTop: '3rem' }}>
|
||||
<div>
|
||||
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||
Most popular images
|
||||
</Typography>
|
||||
</div>
|
||||
<div onClick={() => handleClickViewAll('sortby', sortByCriteria.downloads.value)}>
|
||||
<Typography variant="body2" className={classes.viewAll}>
|
||||
View all
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
{isLoadingPopular ? <Loading /> : renderCards(popularData, isLoadingPopular)}
|
||||
{/* currently most popular will be by downloads until stars are implemented */}
|
||||
<Stack className={classes.sectionHeaderContainer}>
|
||||
<div>
|
||||
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||
Recently updated images
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className={classes.viewAll}
|
||||
onClick={() => handleClickViewAll('sortby', sortByCriteria.updateTime.value)}
|
||||
>
|
||||
View all
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
{isLoadingRecent ? <Loading /> : renderCards(recentData, isLoadingRecent)}
|
||||
{!isEmpty(bookmarkData) && (
|
||||
<>
|
||||
<Stack className={classes.sectionHeaderContainer}>
|
||||
<div>
|
||||
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||
Bookmarks
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className={classes.viewAll}
|
||||
onClick={() => handleClickViewAll('filter', 'IsBookmarked')}
|
||||
>
|
||||
View all
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
{isLoadingBookmarks ? <Loading /> : renderCards(bookmarkData, isLoadingBookmarks)}
|
||||
</>
|
||||
)}
|
||||
{!isEmpty(starData) && (
|
||||
<>
|
||||
<Stack className={classes.sectionHeaderContainer}>
|
||||
<div>
|
||||
<Typography variant="h4" align="left" className={classes.sectionTitle}>
|
||||
Stars
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className={classes.viewAll}
|
||||
onClick={() => handleClickViewAll('filter', 'IsStarred')}
|
||||
>
|
||||
View all
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
{isLoadingStars ? <Loading /> : renderCards(starData, isLoadingStars)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return <>{isLoading ? <Loading /> : renderContent()}</>;
|
||||
}
|
||||
|
||||
export default Home;
|
||||
|
@ -1,32 +1,35 @@
|
||||
// react global
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { host } from '../../host';
|
||||
|
||||
// utility
|
||||
import { api } from '../../api';
|
||||
import { api, endpoints } from '../../api';
|
||||
import { host } from '../../host';
|
||||
import { isEmpty, isObject } from 'lodash';
|
||||
|
||||
// components
|
||||
import { Card, CardContent, CssBaseline } from '@mui/material';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Box from '@mui/material/Box';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import TermsOfService from './TermsOfService';
|
||||
import Loading from '../Shared/Loading';
|
||||
|
||||
import { GoogleLoginButton, GithubLoginButton, OIDCLoginButton } from './ThirdPartyLoginComponents';
|
||||
|
||||
// styling
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import { Card, CardContent } from '@mui/material';
|
||||
import Loading from '../Shared/Loading';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
cardContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto',
|
||||
padding: '0.625rem',
|
||||
position: 'relative'
|
||||
},
|
||||
loginCard: {
|
||||
@ -34,45 +37,82 @@ const useStyles = makeStyles(() => ({
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: '20%',
|
||||
width: '60%',
|
||||
height: '60%',
|
||||
background: '#FFFFFF',
|
||||
gap: '0.625em',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.5rem',
|
||||
borderRadius: '0.75rem',
|
||||
minWidth: '30rem'
|
||||
},
|
||||
loginCardContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: '0.1875rem black',
|
||||
maxWidth: '73%',
|
||||
height: '90%'
|
||||
width: '100%',
|
||||
padding: '3rem'
|
||||
},
|
||||
text: {
|
||||
color: '#14191F',
|
||||
width: '100%',
|
||||
fontSize: '1.5rem'
|
||||
fontSize: '1.5rem',
|
||||
lineHeight: '2.25rem',
|
||||
letterSpacing: '-0.01rem',
|
||||
marginBottom: '0.25rem'
|
||||
},
|
||||
subtext: {
|
||||
color: '#52637A',
|
||||
width: '100%',
|
||||
fontSize: '1rem'
|
||||
fontSize: '1rem',
|
||||
marginBottom: '2.375rem'
|
||||
},
|
||||
textField: {
|
||||
borderRadius: '0.25rem'
|
||||
borderRadius: '0.25rem',
|
||||
marginTop: 0,
|
||||
marginBottom: '1.5rem'
|
||||
},
|
||||
button: {
|
||||
textColor: {
|
||||
color: '#8596AD'
|
||||
},
|
||||
labelColor: {
|
||||
color: '#667C99',
|
||||
'&:focused': {
|
||||
color: '#667C99'
|
||||
}
|
||||
},
|
||||
continueButton: {
|
||||
textTransform: 'none',
|
||||
color: '##FFFFFF',
|
||||
fontSize: '1.4375rem',
|
||||
fontWeight: '500',
|
||||
background: '#F15527',
|
||||
color: '#FFFFFF',
|
||||
fontSize: '1.438rem',
|
||||
fontWeight: '600',
|
||||
height: '3.125rem',
|
||||
borderRadius: '0.25rem',
|
||||
letterSpacing: '0.01rem'
|
||||
letterSpacing: '0.01rem',
|
||||
marginBottom: '1rem',
|
||||
padding: 0,
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '#F15527',
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
continueAsGuestButton: {
|
||||
textTransform: 'none',
|
||||
background: '#FFFFFF',
|
||||
color: '#52637A',
|
||||
fontSize: '1.438rem',
|
||||
fontWeight: '600',
|
||||
height: '3.125rem',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #52637A',
|
||||
letterSpacing: '0.01rem',
|
||||
marginBottom: '1rem',
|
||||
padding: 0,
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFFFFF',
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
gitLogo: {
|
||||
height: '24px',
|
||||
@ -94,6 +134,15 @@ const useStyles = makeStyles(() => ({
|
||||
fontWeight: '400',
|
||||
paddingLeft: '1rem',
|
||||
paddingRight: '1rem'
|
||||
},
|
||||
divider: {
|
||||
color: '#C2CBD6',
|
||||
marginBottom: '2rem',
|
||||
width: '100%'
|
||||
},
|
||||
thirdPartyLoginContainer: {
|
||||
width: '100%',
|
||||
marginBottom: '2rem'
|
||||
}
|
||||
}));
|
||||
|
||||
@ -105,6 +154,8 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
const [requestProcessing, setRequestProcessing] = useState(false);
|
||||
const [requestError, setRequestError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [authMethods, setAuthMethods] = useState({});
|
||||
const [isGuestLoginEnabled, setIsGuestLoginEnabled] = useState(false);
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
const navigate = useNavigate();
|
||||
const classes = useStyles();
|
||||
@ -117,17 +168,31 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
navigate('/home');
|
||||
} else {
|
||||
api
|
||||
.get(`${host()}/v2/`, abortController.signal)
|
||||
.get(`${host()}${endpoints.authConfig}`, abortController.signal)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
localStorage.setItem('token', '-');
|
||||
if (response.data?.http && isEmpty(response.data?.http?.auth)) {
|
||||
localStorage.setItem('authConfig', '{}');
|
||||
setIsLoggedIn(true);
|
||||
navigate('/home');
|
||||
} else if (response.data?.http?.auth) {
|
||||
setAuthMethods(response.data?.http?.auth);
|
||||
localStorage.setItem('authConfig', JSON.stringify(response.data?.http?.auth));
|
||||
setIsLoading(false);
|
||||
wrapperSetLoading(false);
|
||||
navigate('/home');
|
||||
api
|
||||
.get(`${host()}${endpoints.status}`)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setIsGuestLoginEnabled(true);
|
||||
}
|
||||
})
|
||||
.catch(() => console.log('could not obtain guest login status'));
|
||||
}
|
||||
setIsLoading(false);
|
||||
wrapperSetLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setIsLoading(false);
|
||||
wrapperSetLoading(false);
|
||||
});
|
||||
@ -137,22 +202,20 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
const handleBasicAuth = () => {
|
||||
setRequestProcessing(true);
|
||||
let cfg = {};
|
||||
const token = btoa(username + ':' + password);
|
||||
cfg = {
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`
|
||||
}
|
||||
},
|
||||
withCredentials: host() !== window?.location?.origin
|
||||
};
|
||||
api
|
||||
.get(`${host()}/v2/`, abortController.signal, cfg)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
const token = btoa(username + ':' + password);
|
||||
localStorage.setItem('token', token);
|
||||
setRequestProcessing(false);
|
||||
setRequestError(false);
|
||||
setIsLoggedIn(true);
|
||||
@ -165,11 +228,34 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
if (Object.keys(authMethods).includes('htpasswd')) {
|
||||
handleBasicAuth();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGuestClick = () => {
|
||||
setRequestProcessing(false);
|
||||
setRequestError(false);
|
||||
setIsLoggedIn(true);
|
||||
navigate('/home');
|
||||
};
|
||||
|
||||
const handleClickExternalLogin = (event, provider) => {
|
||||
event.preventDefault();
|
||||
window.location.replace(
|
||||
`${host()}${endpoints.openidAuth}?callback_ui=${encodeURIComponent(
|
||||
window?.location?.origin
|
||||
)}/home&provider=${provider}`
|
||||
);
|
||||
};
|
||||
|
||||
const handleChange = (event, type) => {
|
||||
event.preventDefault();
|
||||
setRequestError(false);
|
||||
|
||||
const val = event.target.value;
|
||||
const val = event.target?.value;
|
||||
const isEmpty = val === '';
|
||||
|
||||
switch (type) {
|
||||
@ -194,8 +280,25 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
}
|
||||
};
|
||||
|
||||
const renderThirdPartyLoginMethods = () => {
|
||||
let isGoogle = isObject(authMethods.openid?.providers?.google);
|
||||
// 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;
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing="1rem" className={classes.thirdPartyLoginContainer}>
|
||||
{isGithub && <GithubLoginButton handleClick={handleClickExternalLogin} />}
|
||||
{isGoogle && <GoogleLoginButton handleClick={handleClickExternalLogin} />}
|
||||
{/* {isGitlab && <GitlabLoginButton handleClick={handleClickExternalLogin} />} */}
|
||||
{isOIDC && <OIDCLoginButton handleClick={handleClickExternalLogin} oidcName={oidcName} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={classes.cardContainer} data-testid="signin-container">
|
||||
<div className={classes.cardContainer} data-testid="signin-container">
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
@ -203,68 +306,76 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
<CardContent className={classes.loginCardContent}>
|
||||
<CssBaseline />
|
||||
<Typography align="left" className={classes.text} component="h1" variant="h4">
|
||||
Sign in
|
||||
Sign In
|
||||
</Typography>
|
||||
<Typography align="left" className={classes.subtext} variant="body1" gutterBottom>
|
||||
Welcome back! Please enter your details.
|
||||
Welcome back! Please login.
|
||||
</Typography>
|
||||
|
||||
<Box component="form" onSubmit={null} noValidate autoComplete="off" sx={{ mt: 1 }}>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="username"
|
||||
label="Username"
|
||||
name="username"
|
||||
className={classes.textField}
|
||||
onInput={(e) => handleChange(e, 'username')}
|
||||
error={usernameError != null}
|
||||
helperText={usernameError}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Enter password"
|
||||
type="password"
|
||||
id="password"
|
||||
className={classes.textField}
|
||||
onInput={(e) => handleChange(e, 'password')}
|
||||
error={passwordError != null}
|
||||
helperText={passwordError}
|
||||
/>
|
||||
{requestProcessing && <CircularProgress style={{ marginTop: 20 }} color="secondary" />}
|
||||
{requestError && (
|
||||
<Alert style={{ marginTop: 20 }} severity="error">
|
||||
Authentication Failed. Please try again.
|
||||
</Alert>
|
||||
{renderThirdPartyLoginMethods()}
|
||||
{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>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
{Object.keys(authMethods).includes('htpasswd') && (
|
||||
<Box component="form" onSubmit={null} noValidate autoComplete="off">
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
sx={{
|
||||
mt: 3,
|
||||
mb: 1,
|
||||
background: '#1479FF',
|
||||
'&:hover': {
|
||||
backgroundColor: '#1565C0'
|
||||
}
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{' '}
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
<TermsOfService sx={{ mt: 2, mb: 4 }} />
|
||||
id="username"
|
||||
label="Username"
|
||||
name="username"
|
||||
className={classes.textField}
|
||||
inputProps={{ className: classes.textColor }}
|
||||
InputLabelProps={{ className: classes.labelColor }}
|
||||
onInput={(e) => handleChange(e, 'username')}
|
||||
error={usernameError != null}
|
||||
helperText={usernameError}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Enter password"
|
||||
type="password"
|
||||
id="password"
|
||||
className={classes.textField}
|
||||
inputProps={{ className: classes.textColor }}
|
||||
InputLabelProps={{ className: classes.labelColor }}
|
||||
onInput={(e) => handleChange(e, 'password')}
|
||||
error={passwordError != null}
|
||||
helperText={passwordError}
|
||||
/>
|
||||
{requestProcessing && <CircularProgress style={{ marginTop: 20 }} color="secondary" />}
|
||||
{requestError && (
|
||||
<Alert style={{ marginTop: 20 }} severity="error">
|
||||
Authentication Failed. Please try again.
|
||||
</Alert>
|
||||
)}
|
||||
<div>
|
||||
<Button fullWidth variant="contained" className={classes.continueButton} onClick={handleClick}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
{isGuestLoginEnabled && (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
className={classes.continueAsGuestButton}
|
||||
onClick={handleGuestClick}
|
||||
>
|
||||
Continue as guest
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,55 +1,52 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import logoWhite from '../../assets/Zot-white.svg';
|
||||
import loginDrawing from '../../assets/codeReviewSignIn.png';
|
||||
import backgroundImage from '../../assets/backgroundSignIn.png';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
import logoWhite from '../../assets/zotLogoWhiteHorizontal.svg';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
backgroundImage: `url(${backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
minHeight: '100%',
|
||||
alignItems: 'center'
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
contentContainer: {
|
||||
width: '51%',
|
||||
height: '22%'
|
||||
},
|
||||
logoContainer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
logo: {
|
||||
maxHeight: 96,
|
||||
maxWidth: 320,
|
||||
marginTop: '17%'
|
||||
},
|
||||
loginDrawing: {
|
||||
maxHeight: 298,
|
||||
maxWidth: 464,
|
||||
marginTop: '4%'
|
||||
width: '64%'
|
||||
},
|
||||
mainText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 700,
|
||||
maxWidth: '45%',
|
||||
marginTop: '4%',
|
||||
fontSize: '2.5rem'
|
||||
},
|
||||
captionText: {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
maxWidth: '48%',
|
||||
marginTop: '2%',
|
||||
fontSize: '1.1875rem'
|
||||
color: '#F6F7F9',
|
||||
fontWeight: '700',
|
||||
width: '100%',
|
||||
fontSize: '2.5rem',
|
||||
lineHeight: '3rem'
|
||||
}
|
||||
}));
|
||||
|
||||
export default function SigninPresentation() {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Stack spacing={0} className={classes.container} data-testid="presentation-container">
|
||||
<img src={logoWhite} alt="zot logo" className={classes.logo}></img>
|
||||
<Typography variant="h2" className={classes.mainText}>
|
||||
Welcome to our repository
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.captionText}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Amet, dis pellentesque posuere nulla tortor ac eu arcu
|
||||
nunc.
|
||||
</Typography>
|
||||
<img src={loginDrawing} alt="drawing" className={classes.loginDrawing}></img>
|
||||
</Stack>
|
||||
<div className={classes.container}>
|
||||
<Stack spacing={'3rem'} className={classes.contentContainer} data-testid="presentation-container">
|
||||
<div className={classes.logoContainer}>
|
||||
<img src={logoWhite} alt="zot logo" className={classes.logo}></img>
|
||||
</div>
|
||||
<Typography variant="h2" className={classes.mainText}>
|
||||
OCI-native container image registry, simplified
|
||||
</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
subtext: {
|
||||
color: '#52637A',
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: '400',
|
||||
lineHeight: '154%',
|
||||
letterSpacing: '0.025rem',
|
||||
marginBottom: '0'
|
||||
},
|
||||
text: {
|
||||
color: '#0F2139',
|
||||
fontSize: '0.8125rem',
|
||||
lineHeight: '154%',
|
||||
fontWeight: '600',
|
||||
letterSpacing: '0.025rem'
|
||||
}
|
||||
}));
|
||||
|
||||
export default function TermsOfService(props) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Stack spacing={0}>
|
||||
<Typography variant="caption" className={classes.subtext} align="justify" {...props} pb={6}>
|
||||
By creating an account, you agree to the Terms of Service. For more information about our privacy practices, see
|
||||
the ZOT's Privacy Policy.
|
||||
</Typography>
|
||||
<Typography variant="caption" className={classes.text} align="center" {...props}>
|
||||
Privacy Policy | Terms of Service
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
94
src/components/Login/ThirdPartyLoginComponents.jsx
Normal file
@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import SvgIcon from '@mui/material/SvgIcon';
|
||||
import githubLogo from '../../assets/GhIcon.svg';
|
||||
|
||||
// styling
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
githubButton: {
|
||||
textTransform: 'none',
|
||||
background: '#161614',
|
||||
color: '#FFFFFF',
|
||||
borderRadius: '0.25rem',
|
||||
padding: 0,
|
||||
height: '3.125rem',
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '#161614',
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
googleButton: {
|
||||
textTransform: 'none',
|
||||
background: '#FFFFFF',
|
||||
color: '#52637A',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #52637A',
|
||||
padding: 0,
|
||||
height: '3.125rem',
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFFFFF',
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
buttonsText: {
|
||||
lineHeight: '2.125rem',
|
||||
height: '2.125rem',
|
||||
fontSize: '1.438rem',
|
||||
fontWeight: '600',
|
||||
letterSpacing: '0.01rem'
|
||||
}
|
||||
}));
|
||||
|
||||
function GithubLoginButton({ handleClick }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
className={classes.githubButton}
|
||||
endIcon={<SvgIcon fontSize="medium">{githubLogo}</SvgIcon>}
|
||||
onClick={(e) => handleClick(e, 'github')}
|
||||
>
|
||||
<span className={classes.buttonsText}>Continue with Github</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function GoogleLoginButton({ handleClick }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Button fullWidth variant="contained" className={classes.googleButton} onClick={(e) => handleClick(e, 'google')}>
|
||||
<span className={classes.buttonsText}>Continue with Google</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function GitlabLoginButton({ handleClick }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Button fullWidth variant="contained" className={classes.button} onClick={(e) => handleClick(e, 'gitlab')}>
|
||||
Sign in with Gitlab
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function OIDCLoginButton({ handleClick, oidcName }) {
|
||||
const classes = useStyles();
|
||||
const loginWithName = oidcName || 'OIDC';
|
||||
|
||||
return (
|
||||
<Button fullWidth variant="contained" className={classes.button} onClick={(e) => handleClick(e, 'oidc')}>
|
||||
Sign in with {loginWithName}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { GithubLoginButton, GoogleLoginButton, GitlabLoginButton, OIDCLoginButton };
|
@ -1,50 +1,52 @@
|
||||
// react global
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
// external
|
||||
import { DateTime } from 'luxon';
|
||||
import { isEmpty, uniq } from 'lodash';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../api';
|
||||
import { host } from '../../host';
|
||||
import { useParams, useNavigate, createSearchParams } from 'react-router-dom';
|
||||
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';
|
||||
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 Tags from './Tabs/Tags.jsx';
|
||||
import { Box, Card, CardContent, CardMedia, Chip, Grid, Stack, Tab, Typography } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../../host';
|
||||
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';
|
||||
import repocube2 from '../../assets/repocube-2.png';
|
||||
import repocube3 from '../../assets/repocube-3.png';
|
||||
import repocube4 from '../../assets/repocube-4.png';
|
||||
import { TabContext, TabList, TabPanel } from '@mui/lab';
|
||||
|
||||
import RepoDetailsMetadata from './RepoDetailsMetadata';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
pageWrapper: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
backgroundColor: 'transparent',
|
||||
height: '100%'
|
||||
},
|
||||
container: {
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
marginTop: 100,
|
||||
backgroundColor: '#FFFFFF'
|
||||
},
|
||||
repoName: {
|
||||
fontWeight: '700',
|
||||
color: '#0F2139',
|
||||
fontWeight: '600',
|
||||
fontSize: '1.5rem',
|
||||
color: theme.palette.secondary.main,
|
||||
textAlign: 'left'
|
||||
},
|
||||
avatar: {
|
||||
height: '3rem',
|
||||
width: '3rem',
|
||||
height: '1.438rem',
|
||||
width: '1.438rem',
|
||||
objectFit: 'fill'
|
||||
},
|
||||
cardBtn: {
|
||||
@ -54,31 +56,16 @@ const useStyles = makeStyles((theme) => ({
|
||||
media: {
|
||||
borderRadius: '3.125em'
|
||||
},
|
||||
tabs: {
|
||||
marginTop: '3rem',
|
||||
padding: '0.5rem',
|
||||
tags: {
|
||||
marginTop: '1.5rem',
|
||||
height: '100%',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0'
|
||||
}
|
||||
},
|
||||
tabContent: {
|
||||
height: '100%'
|
||||
},
|
||||
selectedTab: {
|
||||
background: '#D83C0E',
|
||||
borderRadius: '1.5rem'
|
||||
},
|
||||
tabPanel: {
|
||||
height: '100%',
|
||||
paddingLeft: '0rem!important',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '1.5rem 0'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
marginTop: '8rem',
|
||||
paddingLeft: '1.5rem',
|
||||
marginTop: '1.5rem',
|
||||
paddingLeft: '1.25rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
marginTop: '1rem',
|
||||
paddingLeft: '0'
|
||||
@ -88,17 +75,17 @@ const useStyles = makeStyles((theme) => ({
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'start',
|
||||
alignItems: 'flex-start',
|
||||
background: '#FFFFFF',
|
||||
border: '0.0625rem solid #E0E5EB',
|
||||
borderRadius: '2rem',
|
||||
flex: 'none',
|
||||
borderRadius: '0.75rem',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%',
|
||||
boxShadow: 'none!important'
|
||||
},
|
||||
tagsContent: {
|
||||
padding: '1.5rem'
|
||||
},
|
||||
platformText: {
|
||||
backgroundColor: '#EDE7F6',
|
||||
color: '#220052',
|
||||
@ -117,7 +104,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
boxShadow: 'none!important'
|
||||
},
|
||||
header: {
|
||||
paddingLeft: '2rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0'
|
||||
}
|
||||
@ -127,17 +113,41 @@ const useStyles = makeStyles((theme) => ({
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5rem',
|
||||
color: 'rgba(0, 0, 0, 0.6)',
|
||||
padding: '0.5rem 0 0 4rem',
|
||||
padding: '1rem 0 0 0',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0.5rem 0 0 0'
|
||||
}
|
||||
},
|
||||
platformChipsContainer: {
|
||||
alignItems: 'center',
|
||||
padding: '0.5rem 0 0 4rem',
|
||||
padding: '0.15rem 0 0 0',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0.5rem 0 0 0'
|
||||
}
|
||||
},
|
||||
platformChips: {
|
||||
backgroundColor: '#E0E5EB',
|
||||
color: '#52637A',
|
||||
fontSize: '0.813rem',
|
||||
lineHeight: '0.813rem',
|
||||
borderRadius: '0.375rem',
|
||||
padding: '0.313rem 0.625rem'
|
||||
},
|
||||
chipLabel: {
|
||||
padding: '0'
|
||||
},
|
||||
vendor: {
|
||||
color: theme.palette.primary.main,
|
||||
fontSize: '0.75rem',
|
||||
maxWidth: '50%',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: '1.125rem'
|
||||
},
|
||||
versionLast: {
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem',
|
||||
textOverflow: 'ellipsis'
|
||||
}
|
||||
}));
|
||||
|
||||
@ -154,9 +164,8 @@ const randomImage = () => {
|
||||
function RepoDetails() {
|
||||
const [repoDetailData, setRepoDetailData] = useState({});
|
||||
const [tags, setTags] = useState([]);
|
||||
|
||||
const placeholderImage = useRef(randomImage());
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedTab, setSelectedTab] = useState('Overview');
|
||||
// get url param from <Route here (i.e. image name)
|
||||
const { name } = useParams();
|
||||
const navigate = useNavigate();
|
||||
@ -164,6 +173,7 @@ function RepoDetails() {
|
||||
const classes = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
api
|
||||
.get(`${host()}${endpoints.detailedRepoInfo(name)}`, abortController.signal)
|
||||
.then((response) => {
|
||||
@ -188,6 +198,10 @@ function RepoDetails() {
|
||||
};
|
||||
}, [name]);
|
||||
|
||||
const handleDeleteTag = (removed) => {
|
||||
setTags((prevState) => prevState.filter((tag) => tag.tag !== removed));
|
||||
};
|
||||
|
||||
const handlePlatformChipClick = (event) => {
|
||||
const { textContent } = event.target;
|
||||
event.stopPropagation();
|
||||
@ -197,56 +211,76 @@ function RepoDetails() {
|
||||
|
||||
const platformChips = () => {
|
||||
const platforms = repoDetailData?.platforms || [];
|
||||
const filteredPlatforms = platforms?.flatMap((platform) => [platform.Os, platform.Arch]);
|
||||
|
||||
return platforms.map((platform, index) => (
|
||||
<Stack key={`stack${platform?.Os}${platform?.Arch}`} alignItems="center" direction="row" spacing={2}>
|
||||
<Chip
|
||||
key={`${name}${platform?.Os}${index}`}
|
||||
label={platform?.Os}
|
||||
onClick={handlePlatformChipClick}
|
||||
sx={{
|
||||
backgroundColor: '#E0E5EB',
|
||||
color: '#52637A',
|
||||
fontSize: '0.8125rem'
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
key={`${name}${platform?.Arch}${index}`}
|
||||
label={platform?.Arch}
|
||||
onClick={handlePlatformChipClick}
|
||||
sx={{
|
||||
backgroundColor: '#E0E5EB',
|
||||
color: '#52637A',
|
||||
fontSize: '0.8125rem'
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
return uniq(filteredPlatforms).map((platform, index) => (
|
||||
<Chip
|
||||
key={`${name}${platform}${index}`}
|
||||
label={platform}
|
||||
onClick={handlePlatformChipClick}
|
||||
className={classes.platformChips}
|
||||
classes={{
|
||||
label: classes.chipLabel
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setSelectedTab(newValue);
|
||||
const handleBookmarkClick = () => {
|
||||
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
|
||||
if (response && response.status === 200) {
|
||||
setRepoDetailData((prevState) => ({
|
||||
...prevState,
|
||||
isBookmarked: !prevState.isBookmarked
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderOverview = () => {
|
||||
return (
|
||||
<Card className={classes.card} data-testid="overview-container">
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: 'rgba(0, 0, 0, 0.6)',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '150%',
|
||||
marginTop: '1.3rem',
|
||||
alignSelf: 'stretch'
|
||||
}}
|
||||
>
|
||||
{repoDetailData.description || 'Description not available'}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
const handleStarClick = () => {
|
||||
api.put(`${host()}${endpoints.starToggle(name)}`, abortController.signal).then((response) => {
|
||||
if (response.status === 200) {
|
||||
setRepoDetailData((prevState) => ({
|
||||
...prevState,
|
||||
isStarred: !prevState.isStarred
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getVendor = () => {
|
||||
return `${repoDetailData.newestTag?.Vendor || 'Vendor not available'} •`;
|
||||
};
|
||||
const getVersion = () => {
|
||||
return `published ${repoDetailData.newestTag?.Tag} •`;
|
||||
};
|
||||
const getLast = () => {
|
||||
const lastDate = repoDetailData.lastUpdated
|
||||
? DateTime.fromISO(repoDetailData.lastUpdated).toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] })
|
||||
: `Timestamp N/A`;
|
||||
return lastDate;
|
||||
};
|
||||
|
||||
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 (
|
||||
@ -254,87 +288,106 @@ function RepoDetails() {
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<div className={classes.pageWrapper}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent>
|
||||
<Grid container className={classes.header}>
|
||||
<Grid item xs={12} md={8}>
|
||||
<Stack alignItems="center" direction={{ xs: 'column', md: 'row' }} spacing={2}>
|
||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
|
||||
<CardMedia
|
||||
classes={{
|
||||
root: classes.media,
|
||||
img: classes.avatar
|
||||
}}
|
||||
component="img"
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
image={
|
||||
!isEmpty(repoDetailData?.logo)
|
||||
? `data:image/png;base64, ${repoDetailData?.logo}`
|
||||
: randomImage()
|
||||
}
|
||||
alt="icon"
|
||||
/>
|
||||
<Typography variant="h4" className={classes.repoName}>
|
||||
{name}
|
||||
</Typography>
|
||||
<Grid container className={classes.pageWrapper}>
|
||||
<Grid item xs={12} md={12}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent>
|
||||
<Grid container className={classes.header}>
|
||||
<Grid item xs={12} md={8}>
|
||||
<Stack alignItems="center" direction={{ xs: 'column', md: 'row' }} spacing={2}>
|
||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
|
||||
<CardMedia
|
||||
classes={{
|
||||
root: classes.media,
|
||||
img: classes.avatar
|
||||
}}
|
||||
component="img"
|
||||
image={placeholderImage.current}
|
||||
alt="icon"
|
||||
/>
|
||||
<Typography variant="h4" className={classes.repoName}>
|
||||
{name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
|
||||
<VulnerabilityIconCheck vulnerabilitySeverity={repoDetailData?.vulnerabilitySeverity} />
|
||||
{getSignatureChips()}
|
||||
</Stack>
|
||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={1}>
|
||||
{isAuthenticated() && (
|
||||
<IconButton component="span" onClick={handleStarClick} data-testid="star-button">
|
||||
{repoDetailData?.isStarred ? (
|
||||
<StarIcon data-testid="starred" />
|
||||
) : (
|
||||
<StarBorderIcon data-testid="not-starred" />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
{isAuthenticated() && (
|
||||
<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>
|
||||
<Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}>
|
||||
<VulnerabilityIconCheck
|
||||
vulnerabilitySeverity={repoDetailData.vulnerabiltySeverity}
|
||||
count={repoDetailData?.vulnerabilityCount}
|
||||
/>
|
||||
<SignatureIconCheck isSigned={repoDetailData.isSigned} />
|
||||
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
|
||||
<Typography gutterBottom className={classes.repoTitle}>
|
||||
{repoDetailData?.title || 'Title not available'}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} className={classes.platformChipsContainer}>
|
||||
{platformChips()}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Typography gutterBottom className={classes.repoTitle}>
|
||||
{repoDetailData?.title || 'Title not available'}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={2} className={classes.platformChipsContainer}>
|
||||
{platformChips()}
|
||||
</Stack>
|
||||
<Stack alignItems="center" direction="row" spacing={1} pt={'0.5rem'}>
|
||||
<Tooltip title={getVendor()} placement="top" className="hide-on-mobile">
|
||||
<Typography className={classes.vendor} variant="body2" noWrap>
|
||||
{<Markdown options={{ forceInline: true }}>{getVendor()}</Markdown>}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Tooltip title={getVersion()} placement="top" className="hide-on-mobile">
|
||||
<Typography className={classes.versionLast} variant="body2" noWrap>
|
||||
{getVersion()}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Tooltip title={repoDetailData.lastUpdated?.slice(0, 16) || ' '} placement="top">
|
||||
<Typography className={classes.versionLast} variant="body2" noWrap>
|
||||
{getLast()}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={8} className={classes.tabs}>
|
||||
<TabContext value={selectedTab}>
|
||||
<Box>
|
||||
<TabList
|
||||
onChange={handleTabChange}
|
||||
TabIndicatorProps={{ className: classes.selectedTab }}
|
||||
sx={{ '& button.Mui-selected': { color: '#14191F', fontWeight: '600' } }}
|
||||
>
|
||||
<Tab value="Overview" label="Overview" className={classes.tabContent} />
|
||||
<Tab value="Tags" label="Tags" className={classes.tabContent} />
|
||||
</TabList>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<TabPanel value="Overview" className={classes.tabPanel}>
|
||||
{renderOverview()}
|
||||
</TabPanel>
|
||||
<TabPanel value="Tags" className={classes.tabPanel}>
|
||||
<Tags tags={tags} />
|
||||
</TabPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</TabContext>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} className={classes.metadata}>
|
||||
<RepoDetailsMetadata
|
||||
totalDownloads={repoDetailData?.downloads}
|
||||
repoURL={repoDetailData?.source}
|
||||
lastUpdated={repoDetailData?.lastUpdated}
|
||||
size={repoDetailData?.size}
|
||||
latestTag={repoDetailData?.newestTag}
|
||||
license={repoDetailData?.license}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8} className={classes.tags}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent className={classes.tagsContent}>
|
||||
<Tags tags={tags} repoName={name} onTagDelete={handleDeleteTag} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} className={classes.metadata}>
|
||||
<RepoDetailsMetadata
|
||||
totalDownloads={repoDetailData?.downloads}
|
||||
repoURL={repoDetailData?.source}
|
||||
lastUpdated={repoDetailData?.lastUpdated}
|
||||
size={repoDetailData?.size}
|
||||
latestTag={repoDetailData?.newestTag}
|
||||
license={repoDetailData?.license}
|
||||
description={repoDetailData?.description}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -5,27 +5,34 @@ import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import React from 'react';
|
||||
import transform from '../../utilities/transform';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'start',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.5rem',
|
||||
border: '0',
|
||||
borderRadius: '0.5rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%'
|
||||
},
|
||||
cardContent: {
|
||||
'&:last-child': {
|
||||
padding: '0.5rem 1rem'
|
||||
}
|
||||
},
|
||||
metadataHeader: {
|
||||
color: 'rgba(0, 0, 0, 0.6)'
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem'
|
||||
},
|
||||
metadataBody: {
|
||||
color: 'rgba(0, 0, 0, 0.87)',
|
||||
fontFamily: 'Roboto',
|
||||
color: theme.palette.primary.main,
|
||||
fontFamily: 'PT Sans',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: 400,
|
||||
fontSize: '1rem',
|
||||
@ -36,7 +43,7 @@ const useStyles = makeStyles(() => ({
|
||||
|
||||
function RepoDetailsMetadata(props) {
|
||||
const classes = useStyles();
|
||||
const { repoURL, totalDownloads, lastUpdated, size, license } = props;
|
||||
const { repoURL, totalDownloads, lastUpdated, size, license, description } = props;
|
||||
|
||||
const lastDate = lastUpdated
|
||||
? DateTime.fromISO(lastUpdated).toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] })
|
||||
@ -45,7 +52,7 @@ function RepoDetailsMetadata(props) {
|
||||
<Grid container spacing={1}>
|
||||
<Grid container item xs={12}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Repository
|
||||
</Typography>
|
||||
@ -57,7 +64,7 @@ function RepoDetailsMetadata(props) {
|
||||
</Grid>
|
||||
<Grid container item xs={12}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Total downloads
|
||||
</Typography>
|
||||
@ -70,7 +77,7 @@ function RepoDetailsMetadata(props) {
|
||||
<Grid container item xs={12} spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Last publish
|
||||
</Typography>
|
||||
@ -84,7 +91,7 @@ function RepoDetailsMetadata(props) {
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Total size
|
||||
</Typography>
|
||||
@ -98,7 +105,7 @@ function RepoDetailsMetadata(props) {
|
||||
<Grid container item xs={12} spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
License
|
||||
</Typography>
|
||||
@ -111,6 +118,20 @@ function RepoDetailsMetadata(props) {
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container item xs={12} spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography variant="body2" align="left" className={classes.metadataHeader}>
|
||||
Description
|
||||
</Typography>
|
||||
<Typography variant="body1" align="left" className={classes.metadataBody}>
|
||||
{description ? <Markdown>{description}</Markdown> : `Description not available`}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
@ -1,83 +1,55 @@
|
||||
// react global
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { head } from 'lodash';
|
||||
|
||||
// components
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Card, CardContent, Divider, Stack, InputBase, FormControl, Select, InputLabel, MenuItem } from '@mui/material';
|
||||
import { Stack, InputBase, FormControl, Select, InputLabel, MenuItem } from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import TagCard from '../../Shared/TagCard';
|
||||
import { tagsSortByCriteria } from 'utilities/sortCriteria';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
tagCard: {
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: 'none!important',
|
||||
borderRadius: '1.875rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%'
|
||||
},
|
||||
card: {
|
||||
marginBottom: '2rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.875rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%'
|
||||
},
|
||||
content: {
|
||||
textAlign: 'left',
|
||||
color: '#606060',
|
||||
padding: '2% 3% 2% 3%',
|
||||
width: '100%'
|
||||
},
|
||||
clickCursor: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
search: {
|
||||
position: 'relative',
|
||||
minWidth: '100%',
|
||||
maxWidth: '100%',
|
||||
flexDirection: 'row',
|
||||
marginBottom: '1.7rem',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
border: '0.125rem solid #E7E7E7',
|
||||
borderRadius: '1rem',
|
||||
zIndex: 1155
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'none',
|
||||
border: '0.063rem solid #E7E7E7',
|
||||
borderRadius: '0.625rem'
|
||||
},
|
||||
searchIcon: {
|
||||
color: '#52637A',
|
||||
paddingRight: '3%'
|
||||
},
|
||||
searchInputBase: {
|
||||
width: '90%',
|
||||
paddingLeft: '1.5rem',
|
||||
height: 40
|
||||
},
|
||||
input: {
|
||||
color: '#464141',
|
||||
marginLeft: 1,
|
||||
width: '90%'
|
||||
fontSize: '1rem',
|
||||
'&::placeholder': {
|
||||
opacity: '1'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default function Tags(props) {
|
||||
const classes = useStyles();
|
||||
const { tags } = props;
|
||||
const { tags, repoName, onTagDelete } = props;
|
||||
const [tagsFilter, setTagsFilter] = useState('');
|
||||
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
|
||||
|
||||
const renderTags = (tags) => {
|
||||
const selectedSort = Object.values(tagsSortByCriteria).find((sc) => sc.value === sortFilter);
|
||||
const filteredTags = tags.filter((t) => t.Tag?.includes(tagsFilter));
|
||||
const filteredTags = tags.filter((t) => t.tag?.includes(tagsFilter));
|
||||
if (selectedSort) {
|
||||
filteredTags.sort(selectedSort.func);
|
||||
}
|
||||
@ -86,13 +58,14 @@ export default function Tags(props) {
|
||||
filteredTags.map((tag) => {
|
||||
return (
|
||||
<TagCard
|
||||
key={tag.Tag}
|
||||
tag={tag.Tag}
|
||||
lastUpdated={tag.LastUpdated}
|
||||
digest={head(tag.Manifests)?.Digest}
|
||||
vendor={tag.Vendor}
|
||||
size={tag.Size}
|
||||
platform={head(tag.Manifests)?.Platform}
|
||||
key={tag.tag}
|
||||
tag={tag.tag}
|
||||
lastUpdated={tag.lastUpdated}
|
||||
vendor={tag.vendor}
|
||||
manifests={tag.manifests}
|
||||
repo={repoName}
|
||||
onTagDelete={onTagDelete}
|
||||
isDeletable={tag.isDeletable}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@ -110,65 +83,45 @@ export default function Tags(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={classes.tagCard} data-testid="tags-container">
|
||||
<CardContent className={classes.content}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
component="div"
|
||||
align="left"
|
||||
style={{ color: 'rgba(0, 0, 0, 0.87)', fontSize: '1.5rem', fontWeight: '600' }}
|
||||
>
|
||||
Tags History
|
||||
</Typography>
|
||||
<div>
|
||||
<FormControl sx={{ m: '1', minWidth: '4.6875rem' }} className={classes.sortForm} size="small">
|
||||
<InputLabel>Sort</InputLabel>
|
||||
<Select
|
||||
label="Sort"
|
||||
value={sortFilter}
|
||||
onChange={handleTagsSortChange}
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
>
|
||||
{Object.values(tagsSortByCriteria).map((el) => (
|
||||
<MenuItem key={el.value} value={el.value}>
|
||||
{el.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</Stack>
|
||||
<Divider
|
||||
variant="fullWidth"
|
||||
sx={{
|
||||
margin: '5% 0% 5% 0%',
|
||||
background: 'rgba(0, 0, 0, 0.38)',
|
||||
height: '0.00625rem',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
className={classes.search}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
spacing={2}
|
||||
<Stack direction="column" spacing="1rem">
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
component="div"
|
||||
align="left"
|
||||
style={{ color: 'rgba(0, 0, 0, 0.87)', fontSize: '1.5rem', fontWeight: '600' }}
|
||||
>
|
||||
<InputBase
|
||||
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
|
||||
placeholder={'Search for Tags...'}
|
||||
className={classes.input}
|
||||
value={tagsFilter}
|
||||
onChange={handleTagsFilterChange}
|
||||
/>
|
||||
<div className={classes.searchIcon}>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
</Stack>
|
||||
{renderTags(tags)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
Tags History
|
||||
</Typography>
|
||||
<FormControl sx={{ m: '1', minWidth: '4.6875rem' }} className={classes.sortForm} size="small">
|
||||
<InputLabel>Sort</InputLabel>
|
||||
<Select
|
||||
label="Sort"
|
||||
value={sortFilter}
|
||||
onChange={handleTagsSortChange}
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
>
|
||||
{Object.values(tagsSortByCriteria).map((el) => (
|
||||
<MenuItem key={el.value} value={el.value}>
|
||||
{el.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<Stack className={classes.search}>
|
||||
<InputBase
|
||||
placeholder={'Search tags...'}
|
||||
classes={{ root: classes.searchInputBase, input: classes.input }}
|
||||
value={tagsFilter}
|
||||
onChange={handleTagsFilterChange}
|
||||
/>
|
||||
<div className={classes.searchIcon}>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
</Stack>
|
||||
{renderTags(tags)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
54
src/components/Shared/DeleteTag.jsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../api';
|
||||
|
||||
// components
|
||||
import DeleteTagConfirmDialog from 'components/Shared/DeleteTagConfirmDialog';
|
||||
import { host } from '../../host';
|
||||
|
||||
export default function DeleteTag(props) {
|
||||
const { repo, tag, onTagDelete } = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const deleteTag = (repo, tag) => {
|
||||
api
|
||||
.delete(`${host()}${endpoints.deleteImage(repo, tag)}`)
|
||||
.then((response) => {
|
||||
if (response && response.status == 202) {
|
||||
onTagDelete(tag);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
deleteTag(repo, tag);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<IconButton onClick={handleClickOpen}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<DeleteTagConfirmDialog
|
||||
onClose={handleClose}
|
||||
open={open}
|
||||
title={`Permanently delete image ${repo}:${tag}?`}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|