70 Commits

Author SHA1 Message Date
3dffc1e87b Set correct docs link in Header
Some checks failed
Running Code Coverage / build (16.x) (push) Failing after 19s
2024-05-27 15:33:11 +03:00
0fc4aef84e Remove Product link from Header 2024-05-27 15:27:36 +03:00
fd069b2a9b Remove GitHub link from Header 2024-05-27 15:23:49 +03:00
348af7882f Correction of the site Header display 2024-05-27 13:43:02 +03:00
7ed5c9b7cb Remove "Sign in" button from header
Because we don't have a login page
2024-05-24 03:01:40 +03:00
21a1878539 Set PT Sans font 2024-05-24 02:56:27 +03:00
20f5cfcb74 Set correct color scheme 2024-05-24 01:27:36 +03:00
1846f463b5 Set ALT Linux Team icons 2024-05-23 16:49:44 +03:00
b5ba403c8c Set correct titles 2024-05-23 01:42:42 +03:00
b4d248f2ff Remove login page
It is not needed because it is useless in the public repository
(thx Nadezhda Fedorova)
2024-05-23 01:16:48 +03:00
9de2337809 fix: don't display divider for API Keys menu item when API key is disabled (#433)
Signed-off-by: Vishwas Rajashekar <vrajashe@cisco.com>
2024-03-24 22:26:23 +02:00
c4d595c782 patch: signature display redesign (#427)
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
Co-authored-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2024-03-24 22:12:31 +02:00
09ab4474e9 fix(export-cves): use a constant string('vulnerabilities') to set xlsx sheet name (#431)
Some checks failed
Running Code Coverage / build (16.x) (push) Failing after 32s
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2024-03-05 19:25:20 +02:00
177406df41 feat(search-bar): redirect to image view on enter when search maches … (#422)
* feat(search-bar): redirect to image view on enter when search maches a repo:tag

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
2024-03-01 12:07:56 +02:00
e2367c2a33 feat: include PkgPath information in image cve list and list export (#428)
Signed-off-by: Vishwas Rajashekar <vrajashe@cisco.com>
2024-02-29 12:23:15 +02:00
33524ce3cc feat: Implement api key management (#403)
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2024-02-18 15:53:30 +02:00
e037c6c577 feat(cve): filter cves by severity
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2024-02-15 13:17:43 -08:00
c268991495 fix(cve): make cards collapsed by default
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2024-02-14 09:20:52 -08:00
0edfe0f73a feat(cve): add more information
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2024-02-05 10:11:57 -08:00
f4a6030d93 feat(cve): added option to exclude from returned search results a given string (#415)
Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
2024-01-31 10:28:30 +02:00
9358539e0c fix: remove --or-- divider if social login is not enabled (#418)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2024-01-30 10:11:38 +02:00
5bf7d5652c feat: add cve summary in vulnerability tab (#416)
Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
2024-01-18 22:00:19 +02:00
12f9229320 fix(export vuln): change sheet name and download options name (#417)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2024-01-18 15:41:18 +02:00
df19fa811c feat: add expand/collapse view list buttons for vulnerabilities (#409)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2024-01-17 15:38:39 +02:00
6cda89c710 fix: change 'csv' to 'CSV' in the vulnerabilities download options list (#413)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2024-01-17 14:16:46 +02:00
12b474e126 fix: update zot documentation urls (#411)
resolves #410

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2024-01-12 13:47:16 +02:00
a9db66bd34 feat: add freebsd as an OS filter (#407) (#408)
Signed-off-by: Doug Rabson <dfr@rabson.org>
2023-12-28 14:52:27 +02:00
f4600b8b79 feat: export vulnerabilities list
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2023-12-20 09:23:47 -08:00
c375c0697a feat: added button to delete tag
Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
2023-12-15 15:32:28 -08:00
2e1e2e92b7 fix: show a loading message while waiting for a response
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2023-12-11 13:14:30 -08:00
d9370fb9c1 feat: starred repos implementation (#399)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2023-12-07 16:36:54 +02:00
e97e04eee5 ci: dco job should run only on PRs (#396)
See also message in b919279eef

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-11-28 11:23:15 +02:00
a288523a3f feat: vulnerability chips - show icon before string (#392)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
2023-11-28 10:51:23 +02:00
fad5572db4 feat: add prefix zot to /auth urls (#389)
See: https://github.com/project-zot/zot/issues/1883

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-10-20 13:17:24 +03:00
19e366ee1f fix: use the official icon
Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>
2023-10-11 19:56:02 -07:00
b41fb2f841 patch: update nodata display on homepage
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-10-02 14:16:15 -07:00
b787273b84 fix: fixed display of new signature tooltips in some cases (#379)
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-29 19:46:13 +03:00
9ecd46e4d0 ci(end-to-end): Fix a few issues with the workflow (#380)
- free up disk space before running tests
- remove uneeded call to /v2/_catalog.
- add a check to make sure the images are scanned for CVEs before tests start

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-08-29 19:09:52 +03:00
845726cd08 feat: Update signature integration to display extra info (#378)
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-28 18:03:32 +03:00
ac84c375c0 feat: add customizable generic oidc login button
Rebased and modified to reflect https://github.com/project-zot/zot/pull/1691 conclusion

Signed-off-by: Damien Degois <damien@degois.info>
2023-08-28 15:15:17 +03:00
96008d67be feat: Implement no data component
- Implement customizeable component for no data display
- Added component to homepage

Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-18 20:09:28 +03:00
087b42693f patch: update integration tests
Signed-off-by: raulkele <raulkeleblk@gmail.com>
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-15 20:12:30 +03:00
8f4c23bf40 fix: Update tooltip for vulnerability chips
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-08-08 21:33:22 +03:00
54c764c996 patch: update cve api usage
- updated CVEListForImage api calls
- updated ImageListWithCVEFixed api calls
- now cves are shown for specific tag
- fixed tags now only shows tags that match platform with current digest
- moved platform selector on tagdetails page

Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-07-27 08:51:27 +03:00
44289c751f fix: fixed login page refresh bug
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-07-19 17:44:44 +03:00
8086f6880d feat: Update auth flow and add third party auth
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-07-17 10:21:26 -07:00
a55248774c fix: login bug when both anonymous and auth is enabled
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-06-16 13:46:55 +03:00
936590d822 feat:Integration tests
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
2023-05-30 10:54:24 +03:00
05d5f744b0 feat: bookmark implementation
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-05-12 15:59:40 +03:00
769ffdc60d fix: change login page logic
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-05-08 17:22:19 +03:00
70a870a616 fix: fixed layer history not updating for multiarch images
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-05-04 09:15:38 +03:00
c09a12facc test(test-data): add layers information to the image metadata json (#347)
* test(test-data): add layers information to the image metadata json

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>

* fix(tests): fix username userd as password, fix prerequisite validation

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>

* fix(tests): auto-confirm cosign upload to private registry

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>

---------

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-04-27 18:06:52 +03:00
415973e23c build(go): upgrade go version used in end-to-end tests to 1.20 (#346)
Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-04-27 14:08:59 +03:00
cb2d8795f5 patch: followup dependency cleanup
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-27 10:38:34 +03:00
ac9d023272 patch: updated vulnerability design, styling cleanup
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-27 10:03:28 +03:00
6a2fc8d867 patch: tag page design updates
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-26 10:07:09 +03:00
ba73af24b3 patch: cleanup peer dependencies in lockfile
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-24 14:24:22 +03:00
c1a51afede patch: ux update for repo page
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-20 15:33:44 +03:00
ecd584c4e2 patch: explore page ux updates
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-19 14:10:48 +03:00
e0d4417bf7 patch: updated usage of fonts, self hosting from assets
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-13 11:05:24 -07:00
63ff8dabc0 test(end-to-end): provide CVE information for the tests to consume (#330)
Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-04-12 14:43:39 +03:00
f9cafd0b90 patch: homepage and header ux updates
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-12 13:38:24 +03:00
089d79087f patch: update placeholder image logic
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-03 11:53:32 +03:00
2f94cc30ae patch: referrers from image query
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-03-22 14:12:15 +02:00
ddf1d9224b feat: cve list filtering
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-03-21 12:51:16 +02:00
7471fb58a8 ci(tests): some updates to the scripts to make them work better on macos (#324)
Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-03-20 16:51:50 +02:00
2b3058fb14 ci(tests): add a new workflow for running integration tests against a zot server (#322)
Integration tests will use the latest zot on main
The test data consists of images:
- downloaded from dockerhub
- converted to OCI format
- having all needed annotations
- having a logo as an OCI artifact

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-03-17 09:54:08 +02:00
ecff33fe01 fix: homepage incorrect data
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-03-10 12:53:27 +02:00
60ca6d21d5 fix: fixed the bugs present in explore page
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-03-09 16:54:36 +02:00
9029b97b47 feat: multi-arch image features
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-03-09 10:44:43 +02:00
151 changed files with 14073 additions and 25274 deletions

View File

@ -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
View 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
View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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>

View File

@ -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
}
}

View File

@ -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>

View File

@ -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();
});

View File

@ -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;

View File

@ -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);
});
});

View File

@ -11,6 +11,14 @@ jest.mock(
}
);
jest.mock(
'components/Header/Header',
() =>
function Header() {
return <div />;
}
);
it('renders the explore page component', () => {
render(
<BrowserRouter>

View File

@ -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();

View 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();
});
});

View File

@ -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()
});
});
});

View File

@ -11,6 +11,14 @@ jest.mock(
}
);
jest.mock(
'components/Header/Header',
() =>
function Header() {
return <div />;
}
);
it('renders the homepage component', () => {
render(
<BrowserRouter>

View File

@ -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();

View File

@ -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(() => {

View File

@ -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();
});
});

View File

@ -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();
});

View File

@ -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);

View File

@ -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({

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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();
});
});

View File

@ -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 });
});
});

View File

@ -24,6 +24,14 @@ jest.mock(
}
);
jest.mock(
'components/Header/Header',
() =>
function Header() {
return <div />;
}
);
it('renders the tags page component', async () => {
render(
<BrowserRouter>

View File

@ -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
View 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');
});
});

View File

@ -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 };

View 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
View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

93
src/assets/fonts/LICENSE Normal file
View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

15
src/assets/noData.svg Normal file
View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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

View File

@ -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>

View File

@ -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}>

View File

@ -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>
);

View File

@ -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={() => {}}

View 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;

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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&apos;s Privacy Policy.
</Typography>
<Typography variant="caption" className={classes.text} align="center" {...props}>
Privacy Policy | Terms of Service
</Typography>
</Stack>
);
}

View 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 };

View File

@ -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>
)}
</>
);

View File

@ -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>
);
}

View File

@ -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>
);
}

View 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>
);
}

Some files were not shown because too many files have changed in this diff Show More