Compare commits
No commits in common. "master" and "patches" have entirely different histories.
10
.eslintignore
Normal file
@ -0,0 +1,10 @@
|
||||
**/.git
|
||||
**/.svn
|
||||
**/.hg
|
||||
**/node_modules
|
||||
**/.github
|
||||
README.md
|
||||
LICENSE
|
||||
Makefile
|
||||
**/coverage
|
||||
**/build
|
54
.eslintrc.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended"],
|
||||
"overrides": [],
|
||||
"rules": {
|
||||
"react/prop-types": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react"],
|
||||
"settings": {
|
||||
"react": {
|
||||
"createClass": "createReactClass", // Regex for Component Factory to use,
|
||||
// default to "createReactClass"
|
||||
"pragma": "React", // Pragma to use, default to "React"
|
||||
"fragment": "Fragment", // Fragment to use (may be a property of <pragma>), default to "Fragment"
|
||||
"version": "detect", // React version. "detect" automatically picks the version you have installed.
|
||||
// You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
|
||||
// It will default to "latest" and warn if missing, and to "detect" in the future
|
||||
"flowVersion": "0.53" // Flow version
|
||||
},
|
||||
"propWrapperFunctions": [
|
||||
// The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
|
||||
"forbidExtraProps",
|
||||
{ "property": "freeze", "object": "Object" },
|
||||
{ "property": "myFavoriteWrapper" },
|
||||
// for rules that check exact prop wrappers
|
||||
{ "property": "forbidExtraProps", "exact": true }
|
||||
],
|
||||
"componentWrapperFunctions": [
|
||||
// The name of any function used to wrap components, e.g. Mobx `observer` function. If this isn't set, components wrapped by these functions will be skipped.
|
||||
"observer", // `property`
|
||||
{ "property": "styled" }, // `object` is optional
|
||||
{ "property": "observer", "object": "Mobx" },
|
||||
{ "property": "observer", "object": "<pragma>" } // sets `object` to whatever value `settings.react.pragma` is set to
|
||||
],
|
||||
"formComponents": [
|
||||
// Components used as alternatives to <form> for forms, eg. <Form endpoint={ url } />
|
||||
"CustomForm",
|
||||
{ "name": "Form", "formAttribute": "endpoint" }
|
||||
],
|
||||
"linkComponents": [
|
||||
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
|
||||
"Hyperlink",
|
||||
{ "name": "Link", "linkAttribute": "to" }
|
||||
]
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
name: Building zot from binaries with patch for login page
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build-process:
|
||||
runs-on: alt-sisyphus
|
||||
steps:
|
||||
- name: Update apt
|
||||
uses: actions/init-alt-env@v1
|
||||
- name: Install req-s
|
||||
run: |
|
||||
apt-get install -y podman
|
||||
- name: Check out zot
|
||||
uses: actions/checkout@master
|
||||
- name: Build image
|
||||
run: |
|
||||
podman build --tag alt/zot-wo-auth:$ZOT_VER --build-arg="ZOT_VER=$ZOT_VER" --build-arg="ZUI_VER=$ZUI_VER" .
|
||||
env:
|
||||
ZOT_VER: 'v2.0.4'
|
||||
ZUI_VER: 'commit-9de2337'
|
||||
- name: Push image
|
||||
run: |
|
||||
podman login --username $P_USER --password $P_PASS $URL
|
||||
podman push alt/zot-wo-auth:$ZOT_VER docker://$URL/alt/zot-wo-auth
|
||||
podman rmi --all
|
||||
env:
|
||||
P_USER: ${{ secrets.PODMAN_USER }}
|
||||
P_PASS: ${{ secrets.PODMAN_PASS }}
|
||||
ZOT_VER: 'v2.0.4'
|
||||
URL: 'gitea.basealt.ru'
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
60
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
<!-- Thanks for sending a pull request! Here are some tips for you:
|
||||
1. Ensure you have added the unit tests for your changes.
|
||||
2. Ensure you have included output of manual testing done in the Testing section.
|
||||
3. Ensure number of lines of code for new or existing methods are within the reasonable limit.
|
||||
4. Ensure your change works on existing clusters after upgrade.
|
||||
5. If your mounting any new file or directory, make sure its not opening up any security attack vector for aws-vpc-cni-k8s modules.
|
||||
6. If AWS apis are invoked, document the call rate in the description section.
|
||||
7. If EC2 Metadata apis are invoked, ensure to handle stale information returned from metadata.
|
||||
-->
|
||||
**What type of PR is this?**
|
||||
|
||||
<!--
|
||||
Add one of the following:
|
||||
bug
|
||||
cleanup
|
||||
documentation
|
||||
feature
|
||||
-->
|
||||
|
||||
**Which issue does this PR fix**:
|
||||
|
||||
|
||||
**What does this PR do / Why do we need it**:
|
||||
|
||||
|
||||
**If an issue # is not available please add repro steps and logs from IPAMD/CNI showing the issue**:
|
||||
|
||||
|
||||
**Testing done on this change**:
|
||||
<!--
|
||||
output of manual testing/integration tests results and also attach logs
|
||||
showing the fix being resolved
|
||||
-->
|
||||
|
||||
**Automation added to e2e**:
|
||||
<!--
|
||||
Test case added to lib/integration.sh
|
||||
If no, create an issue with enhancement/testing label
|
||||
-->
|
||||
|
||||
**Will this break upgrades or downgrades. Has updating a running cluster been tested?**:
|
||||
|
||||
|
||||
**Does this change require updates to the CNI daemonset config files to work?**:
|
||||
<!--
|
||||
If this change does not work with a "kubectl patch" of the image tag, please explain why.
|
||||
-->
|
||||
|
||||
**Does this PR introduce any user-facing change?**:
|
||||
<!--
|
||||
If yes, a release note update is required:
|
||||
Enter your extended release note in the block below. If the PR requires additional actions
|
||||
from users switching to the new release, include the string "action required".
|
||||
-->
|
||||
|
||||
```release-note
|
||||
|
||||
```
|
||||
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
80
.github/workflows/build-test.yml
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests
|
||||
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build zui
|
||||
env:
|
||||
CI: ""
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
|
||||
- name: Package app
|
||||
run: tar -czvf /tmp/zui.tgz ./build
|
||||
|
||||
- name: Publish artifacts on releases
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: /tmp/zui.tgz
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
- name: Generate commit sha
|
||||
if: github.ref_name == 'main'
|
||||
uses: benjlevesque/short-sha@v2.1
|
||||
# This creates and environment variable SHA containing the short commit ID
|
||||
|
||||
- name: Create new tag for builds on main branch
|
||||
if: github.ref_name == 'main'
|
||||
uses: mathieudutour/github-tag-action@v6.1
|
||||
with:
|
||||
custom_tag: commit-${{ env.SHA }}
|
||||
tag_prefix: ""
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish artifact for builds on main branch
|
||||
if: github.ref_name == 'main'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: /tmp/zui.tgz
|
||||
tag: commit-${{ env.SHA }}
|
||||
prerelease: true # Mark as prerelease and avoid triggering another workflow for the new tag
|
||||
overwrite: true
|
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '17 11 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
34
.github/workflows/coverage.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: Running Code Coverage
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run the tests
|
||||
run: npm test -- --coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
24
.github/workflows/dco.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# .github/workflows/dco.yml
|
||||
name: DCO
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.x
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Check DCO
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
pip3 install -U dco-check
|
||||
dco-check
|
142
.github/workflows/end-to-end-test.yml
vendored
Normal file
@ -0,0 +1,142 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
name: end-to-end-test
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Test zui/zot integration
|
||||
env:
|
||||
CI: ""
|
||||
REGISTRY_HOST: "localhost"
|
||||
REGISTRY_PORT: "8080"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Cleanup disk space
|
||||
run: |
|
||||
# To free up ~15 GB of disk space
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
|
||||
- name: Checkout zui repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: 'npm'
|
||||
|
||||
- name: Build zui
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
make install
|
||||
make build
|
||||
|
||||
- name: Install container image tooling
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgpgme-dev libassuan-dev libbtrfs-dev libdevmapper-dev pkg-config rpm snapd jq
|
||||
git clone https://github.com/containers/skopeo -b v1.9.0 $GITHUB_WORKSPACE/src/github.com/containers/skopeo
|
||||
cd $GITHUB_WORKSPACE/src/github.com/containers/skopeo && make bin/skopeo
|
||||
chmod +x bin/skopeo
|
||||
sudo mv bin/skopeo /usr/local/bin/skopeo
|
||||
which skopeo
|
||||
skopeo -v
|
||||
curl -L https://github.com/regclient/regclient/releases/download/v0.4.7/regctl-linux-amd64 -o regctl
|
||||
chmod +x regctl
|
||||
sudo mv regctl /usr/local/bin/regctl
|
||||
which regctl
|
||||
regctl version
|
||||
curl -L https://github.com/sigstore/cosign/releases/download/v1.13.0/cosign-linux-amd64 -o cosign
|
||||
chmod +x cosign
|
||||
sudo mv cosign /usr/local/bin/cosign
|
||||
which cosign
|
||||
cosign version
|
||||
pushd $(mktemp -d)
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.38.3/trivy_0.38.3_Linux-64bit.tar.gz -o trivy.tar.gz
|
||||
tar -xzvf trivy.tar.gz
|
||||
sudo mv trivy /usr/local/bin/trivy
|
||||
popd
|
||||
which trivy
|
||||
trivy version
|
||||
cd $GITHUB_WORKSPACE
|
||||
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
|
||||
- name: Checkout zot repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
repository: project-zot/zot
|
||||
ref: main
|
||||
path: zot
|
||||
|
||||
- name: Build zot
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/zot
|
||||
make binary ZUI_BUILD_PATH=$GITHUB_WORKSPACE/build
|
||||
ls -l bin/
|
||||
|
||||
- name: Bringup zot server
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/zot
|
||||
mkdir /tmp/zot
|
||||
./bin/zot-linux-amd64 serve examples/config-ui.json &
|
||||
while true; do x=0; curl -f http://$REGISTRY_HOST:$REGISTRY_PORT/v2/ || x=1; if [ $x -eq 0 ]; then break; fi; sleep 1; done
|
||||
|
||||
- name: Load image test data from cache into a local folder
|
||||
id: restore-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: tests/data/images
|
||||
key: image-config-${{ hashFiles('**/tests/data/config.yaml') }}
|
||||
restore-keys: |
|
||||
image-config-
|
||||
|
||||
- name: Load image test data into zot server
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
regctl registry set --tls disabled $REGISTRY_HOST:$REGISTRY_PORT
|
||||
make test-data REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT
|
||||
|
||||
- name: Install playwright dependencies
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
make playwright-browsers
|
||||
|
||||
- name: Trigger CVE scanning
|
||||
run: |
|
||||
# trigger CVE scanning for all images before running the tests
|
||||
curl -X POST -H "Content-Type: application/json" -m 600 --data '{ "query": "{ ImageListForCVE (id:\"CVE-2021-43616\") { Results { RepoName Tag } } }" }' http://$REGISTRY_HOST:$REGISTRY_PORT/v2/_zot/ext/search
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
make integration-tests REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT
|
||||
|
||||
- name: Upload playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
24
.github/workflows/stale.yaml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'
|
||||
days-before-issue-stale: 30
|
||||
days-before-pr-stale: 45
|
||||
days-before-issue-close: 5
|
||||
days-before-pr-close: 10
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
exempt-issue-labels: 'awaiting-approval,work-in-progress'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
exempt-pr-labels: 'awaiting-approval,work-in-progress'
|
||||
only-labels: 'awaiting-feedback,awaiting-answers'
|
136
.gitignore
vendored
Normal file
@ -0,0 +1,136 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
#IDE
|
||||
/.vscode
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/tests/data/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
data.md
|
10
.prettierignore
Normal file
@ -0,0 +1,10 @@
|
||||
**/.git
|
||||
**/.svn
|
||||
**/.hg
|
||||
**/node_modules
|
||||
**/.github
|
||||
README.md
|
||||
LICENSE
|
||||
Makefile
|
||||
**/coverage
|
||||
**/build
|
8
.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"endOfLine": "auto"
|
||||
}
|
55
Dockerfile
@ -1,55 +0,0 @@
|
||||
FROM registry.altlinux.org/alt/alt:sisyphus AS builder
|
||||
ARG ZOT_VER=v2.0.4
|
||||
ARG ZUI_VER=commit-09ab447
|
||||
ARG ZOT_ALT_BRANCH=zot-alt
|
||||
ARG ZUI_ALT_BRANCH=zui-alt
|
||||
ARG ZOT_ALT_REPO=https://gitea.basealt.ru/alt/zot-wo-auth.git
|
||||
|
||||
WORKDIR /workdir
|
||||
RUN apt-get update && apt-get install -y podman git \
|
||||
golang npm rpm-build-golang rpm-build-nodejs rpm-macros-golang && \
|
||||
rm -f /var/cache/apt/archives/*.rpm \
|
||||
/var/cache/apt/*.bin \
|
||||
/var/lib/apt/lists/*.*
|
||||
RUN git clone -q --branch $ZOT_ALT_BRANCH $ZOT_ALT_REPO zot
|
||||
WORKDIR zot
|
||||
RUN git clone -q --branch $ZUI_ALT_BRANCH $ZOT_ALT_REPO zui
|
||||
|
||||
WORKDIR zui
|
||||
RUN npm install && npm run build
|
||||
|
||||
WORKDIR /workdir/zot
|
||||
RUN make COMMIT=$ZOT_VER ZUI_BUILD_PATH="/workdir/zot/zui/build" binary cli bench
|
||||
RUN export ARCH=$(go env GOARCH); bin/zli-linux-$ARCH completion bash > zli.bash
|
||||
RUN export ARCH=$(go env GOARCH); bin/zot-linux-$ARCH completion bash > zot.bash
|
||||
|
||||
FROM registry.altlinux.org/alt/alt:sisyphus
|
||||
MAINTAINER alt-cloud
|
||||
|
||||
LABEL org.opencontainers.image.title="zot"
|
||||
LABEL org.opencontainers.image.description="A production-ready vendor-neutral OCI-native container image registry (purely based on OCI Distribution Specification)"
|
||||
LABEL org.opencontainers.image.source="https://github.com/project-zot/zot"
|
||||
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
||||
LABEL org.opencontainers.image.vendor="ALT Linux Team"
|
||||
|
||||
COPY --from=builder /workdir/zot/bin/zot-linux-* /usr/bin/zot
|
||||
COPY --from=builder /workdir/zot/bin/zli-linux-* /usr/bin/zli
|
||||
COPY --from=builder /workdir/zot/bin/zb-linux-* /usr/bin/zb
|
||||
COPY ./config.json /etc/zot/config.json
|
||||
COPY --from=builder /workdir/zot/zot.bash /usr/share/bash-completion/completions/zot
|
||||
COPY --from=builder /workdir/zot/zli.bash /usr/share/bash-completion/completions/zli
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates && \
|
||||
rm -f /var/cache/apt/archives/*.rpm \
|
||||
/var/cache/apt/*.bin \
|
||||
/var/lib/apt/lists/*.*
|
||||
RUN groupadd -r -f -g 10001 _zot
|
||||
RUN useradd -r -g _zot -M -d /var/lib/zot -s /dev/null -c "Zot registry user" -u 10001 _zot
|
||||
|
||||
USER _zot:_zot
|
||||
|
||||
VOLUME ["/var/lib/zot"]
|
||||
EXPOSE 5000
|
||||
|
||||
ENTRYPOINT ["/usr/bin/zot"]
|
||||
CMD ["serve", "/etc/zot/config.json"]
|
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
42
Makefile
Normal file
@ -0,0 +1,42 @@
|
||||
REGISTRY_HOST ?= localhost
|
||||
REGISTRY_PORT ?= 8080
|
||||
|
||||
.PHONY: all
|
||||
all: install audit build
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
npm install --no-audit
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
npm run build
|
||||
|
||||
.PHONY: update
|
||||
update:
|
||||
npm update
|
||||
|
||||
.PHONY: audit
|
||||
audit:
|
||||
npm audit --omit=dev
|
||||
|
||||
.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
|
61
README.md
Normal file
@ -0,0 +1,61 @@
|
||||
# zot UI [![build-test](https://github.com/project-zot/zui/actions/workflows/build-test.yml/badge.svg?branch=main)](https://github.com/project-zot/zui/actions/workflows/build-test.yml) [![codecov.io](http://codecov.io/github/project-zot/zui/coverage.svg?branch=main)](http://codecov.io/github/project-zot/zui?branch=main) [![CodeQL](https://github.com/project-zot/zui/workflows/CodeQL/badge.svg)](https://github.com/project-zot/zui/actions?query=workflow%3ACodeQL)
|
||||
A graphical user interface to interact with a [zot](https://github.com/project-zot/zot) server instance.
|
||||
|
||||
Built with [React JS](https://reactjs.org/) and [Material UI](https://mui.com/).
|
||||
|
||||
|
||||
|
||||
## TL;DR
|
||||
|
||||
To start this app, run
|
||||
### `npm install`
|
||||
### `npm start`
|
||||
|
||||
If `zui` is ran separately from the `zot` back-end, the manual host configuration must be changed in the `./src/host.js` file
|
||||
```js
|
||||
const hostConfig = {
|
||||
auto:false,
|
||||
default:'http://localhost:5000' // replace with zot host
|
||||
}
|
||||
```
|
||||
The app will open in your default browser.
|
||||
If not, you can manually open [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, run:
|
||||
|
||||
### `npm install`
|
||||
|
||||
Do this first. Installs all dependencies needed by the app.
|
||||
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode. It should open your app in your default browser.
|
||||
If not, you can manually open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
The app is ready to be deployed!
|
||||
|
||||
See this section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
|
||||
## Learn More
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), and built with [React JS](https://reactjs.org/) and [Material UI](https://mui.com/).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
To learn Material UI, check out the [Material UI Library](https://mui.com/).
|
29
config.json
@ -1,29 +0,0 @@
|
||||
{
|
||||
"storage":{
|
||||
"rootDirectory":"/var/lib/zot"
|
||||
},
|
||||
"http":{
|
||||
"address":"0.0.0.0",
|
||||
"port":"5000"
|
||||
},
|
||||
"log":{
|
||||
"level":"debug"
|
||||
},
|
||||
"extensions": {
|
||||
"search": {
|
||||
"enable": true,
|
||||
"cve": {
|
||||
"trivy": {
|
||||
"dbRepository": "registry.altlinux.org/alt/trivy-db"
|
||||
},
|
||||
"updateInterval": "24h"
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
"enable": true
|
||||
},
|
||||
"mgmt": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}
|
7
jsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src",
|
||||
"checkJs": true,
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
19454
package-lock.json
generated
Normal file
71
package.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "zot-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.9.3",
|
||||
"@emotion/styled": "^11.9.3",
|
||||
"@mui/icons-material": "^5.2.5",
|
||||
"@mui/lab": "^5.0.0-alpha.89",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@mui/styles": "^5.8.6",
|
||||
"@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": "^3.4.4",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-sticky-el": "^2.0.9",
|
||||
"web-vitals": "^2.1.3",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"prettier": "^2.7.1",
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --detectOpenHandles",
|
||||
"test:coverage": "react-scripts test --detectOpenHandles --coverage",
|
||||
"test: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",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
114
playwright.config.js
Normal file
@ -0,0 +1,114 @@
|
||||
// @ts-check
|
||||
const { devices } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
* @type {import('@playwright/test').PlaywrightTestConfig}
|
||||
*/
|
||||
const config = {
|
||||
testDir: './tests',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 50 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 15000
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: 2,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 2,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'only-on-failure'
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
module.exports = config;
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 1.4 KiB |
54
public/index.html
Normal file
@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="/">
|
||||
<meta http-equiv="Content-Security-Policy" content="
|
||||
default-src 'none';
|
||||
script-src 'self' 'unsafe-inline';
|
||||
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" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="ALT Linux OCI-native Container Image Registry"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
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>ALT Linux OCI-native Container Image Registry</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
public/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
86
src/App.css
Normal file
@ -0,0 +1,86 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
height: 100vh;
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 6vh;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.App-logo.nav-logo {
|
||||
height: 8vh;
|
||||
}
|
||||
|
||||
.App-logo.Loading {
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: bounce 1s;
|
||||
animation-direction: alternate;
|
||||
animation-timing-function: cubic-bezier(0.5, 0.05, 1, 0.5);
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes bounce {
|
||||
from {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
to {
|
||||
transform: translate3d(0, 50px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
from {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
to {
|
||||
transform: translate3d(0, 50px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.bounce {
|
||||
-webkit-animation-name: bounce;
|
||||
animation-name: bounce;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.hide-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
.hide-on-small {
|
||||
display:none
|
||||
}
|
||||
}
|
32
src/App.js
Normal file
@ -0,0 +1,32 @@
|
||||
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';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App" data-testid="app-container">
|
||||
<Router>
|
||||
<Routes>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
13
src/App.test.js
Normal file
@ -0,0 +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(
|
||||
<MockThemeProvider>
|
||||
<App />
|
||||
</MockThemeProvider>
|
||||
);
|
||||
expect(screen.getByTestId('app-container')).toBeInTheDocument();
|
||||
});
|
10
src/__mocks__/MockThemeProvider.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme();
|
||||
|
||||
function MockThemeProvider({ children }) {
|
||||
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||
}
|
||||
|
||||
export default MockThemeProvider;
|
438
src/__tests__/Explore/Explore.test.js
Normal file
@ -0,0 +1,438 @@
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import Explore from 'components/Explore/Explore';
|
||||
import React from 'react';
|
||||
import { createSearchParams, MemoryRouter } from 'react-router-dom';
|
||||
import filterConstants from 'utilities/filterConstants.js';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria.js';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// router mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
const StateExploreWrapper = (props) => {
|
||||
const queryString = props.search || '';
|
||||
return (
|
||||
<MockThemeProvider>
|
||||
<MemoryRouter initialEntries={[`/explore?${queryString.toString()}`]}>
|
||||
<Explore />
|
||||
</MemoryRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
const mockImageList = {
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 20, ItemCount: 10 },
|
||||
Repos: [
|
||||
{
|
||||
Name: 'alpine',
|
||||
Size: '2806985',
|
||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||
IsBookmarked: false,
|
||||
IsStarred: false,
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
SignatureInfo: [],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
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: '',
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: false,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
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: '',
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
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: '',
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
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: '',
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
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: '',
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: ''
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
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: '',
|
||||
SignatureInfo: [
|
||||
{
|
||||
Tool: 'cosign',
|
||||
IsTrusted: true,
|
||||
Author: 'author1'
|
||||
},
|
||||
{
|
||||
Tool: 'notation',
|
||||
IsTrusted: true,
|
||||
Author: 'author2'
|
||||
}
|
||||
],
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
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();
|
||||
mockIntersectionObserver.mockReturnValue({
|
||||
observe: () => null,
|
||||
unobserve: () => null,
|
||||
disconnect: () => null
|
||||
});
|
||||
window.IntersectionObserver = mockIntersectionObserver;
|
||||
Object.defineProperty(window.document, 'cookie', {
|
||||
writable: true,
|
||||
value: 'user=test'
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Explore component', () => {
|
||||
it("fetches image data and renders the list of images based on it's filters", async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<StateExploreWrapper />);
|
||||
expect(await screen.findByText(/alpine/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/mongo/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/centos/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the no data message if no data is received', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: { GlobalSearch: { Repos: [] } } } });
|
||||
render(<StateExploreWrapper />);
|
||||
expect(await screen.findByText(/Looks like/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders signature icons', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<StateExploreWrapper />);
|
||||
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(1);
|
||||
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 () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<StateExploreWrapper />);
|
||||
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(1);
|
||||
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(1);
|
||||
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
|
||||
expect(await screen.findAllByTestId('none-vulnerability-icon')).toHaveLength(1);
|
||||
expect(await screen.findAllByTestId('medium-vulnerability-icon')).toHaveLength(1);
|
||||
expect(await screen.findAllByTestId('unknown-vulnerability-icon')).toHaveLength(1);
|
||||
expect(await screen.findAllByTestId('failed-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(<StateExploreWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it("should render the sort filter and be able to change it's value", async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<StateExploreWrapper />);
|
||||
const selectFilter = await screen.findByText('Relevance');
|
||||
expect(selectFilter).toBeInTheDocument();
|
||||
userEvent.click(selectFilter);
|
||||
const newOption = await screen.findByText('Alphabetical');
|
||||
userEvent.click(newOption);
|
||||
expect(await screen.findByText('Alphabetical')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should get preselected filters and sorting order from query params', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(
|
||||
<StateExploreWrapper
|
||||
search={createSearchParams({
|
||||
filter: filterConstants.osFilters[0].value,
|
||||
sortby: sortByCriteria.downloads.value
|
||||
})}
|
||||
/>
|
||||
);
|
||||
const sortyBySelect = await screen.findByText(sortByCriteria.downloads.label);
|
||||
expect(sortyBySelect).toBeInTheDocument();
|
||||
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);
|
||||
});
|
||||
});
|
31
src/__tests__/Explore/ExplorePage.test.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ExplorePage from 'pages/ExplorePage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
jest.mock(
|
||||
'components/Explore/Explore',
|
||||
() =>
|
||||
function Explore() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the explore page component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<ExplorePage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
expect(screen.getByTestId('explore-container')).toBeInTheDocument();
|
||||
});
|
31
src/__tests__/Explore/FilterCard.test.js
Normal file
@ -0,0 +1,31 @@
|
||||
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 (
|
||||
<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(3);
|
||||
|
||||
const checkbox = screen.getAllByRole('checkbox');
|
||||
expect(checkbox[0]).not.toBeChecked();
|
||||
fireEvent.click(checkbox[0]);
|
||||
await waitFor(() => expect(checkbox[0]).toBeChecked());
|
||||
});
|
||||
});
|
40
src/__tests__/Header/UserAccountMenu.test.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import UserAccountMenu from 'components/Header/UserAccountMenu';
|
||||
import React from 'react';
|
||||
|
||||
const mockIsApiKeyEnabled = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => {}
|
||||
}));
|
||||
|
||||
jest.mock('../../utilities/authUtilities', () => ({
|
||||
isApiKeyEnabled: () => {
|
||||
return mockIsApiKeyEnabled();
|
||||
},
|
||||
getLoggedInUser: () => {
|
||||
return 'jest-user';
|
||||
},
|
||||
logoutUser: () => {}
|
||||
}));
|
||||
|
||||
describe('Account Menu', () => {
|
||||
it('displays Api Keys menu item with its divider when the API Keys config is enabled', async () => {
|
||||
mockIsApiKeyEnabled.mockReturnValue(true);
|
||||
render(<UserAccountMenu />);
|
||||
const userIconButton = await screen.getByTestId('user-icon-header-button');
|
||||
fireEvent.click(userIconButton);
|
||||
expect(await screen.queryByTestId('api-keys-menu-item')).toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('api-keys-menu-item-divider')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display Api Keys menu item and divider when the API Keys config is disabled', async () => {
|
||||
mockIsApiKeyEnabled.mockReturnValue(false);
|
||||
render(<UserAccountMenu />);
|
||||
const userIconButton = await screen.getByTestId('user-icon-header-button');
|
||||
fireEvent.click(userIconButton);
|
||||
expect(await screen.queryByTestId('api-keys-menu-item')).not.toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('api-keys-menu-item-divider')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
315
src/__tests__/HomePage/Home.test.js
Normal file
@ -0,0 +1,315 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import Home from 'components/Home/Home';
|
||||
import React from 'react';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
const HomeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvider>
|
||||
<Home />
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const mockImageList = {
|
||||
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',
|
||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
IsSigned: false,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'LOW',
|
||||
Count: 7
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'mongo',
|
||||
Size: '231383863',
|
||||
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const mockImageListStars = {
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 3, ItemCount: 2 },
|
||||
Repos: [
|
||||
{
|
||||
Name: 'alpine',
|
||||
Size: '2806985',
|
||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
IsSigned: false,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'LOW',
|
||||
Count: 7
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'mongo',
|
||||
Size: '231383863',
|
||||
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: true,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: '',
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.scrollTo = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Home component', () => {
|
||||
it('fetches image data and renders popular, bookmarks and recently updated', async () => {
|
||||
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').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').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(<HomeWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(4));
|
||||
});
|
||||
|
||||
it('should redirect to explore page when clicking view all popular', async () => {
|
||||
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(4);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: [] } });
|
||||
fireEvent.click(viewAllButtons[0]);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
pathname: `/explore`,
|
||||
search: createSearchParams({ sortby: sortByCriteria.downloads.value }).toString()
|
||||
});
|
||||
fireEvent.click(viewAllButtons[1]);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
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()
|
||||
});
|
||||
});
|
||||
});
|
31
src/__tests__/HomePage/HomePage.test.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import HomePage from 'pages/HomePage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
jest.mock(
|
||||
'components/Home/Home',
|
||||
() =>
|
||||
function Home() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the homepage component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<HomePage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
expect(screen.getByTestId('homepage-container')).toBeInTheDocument();
|
||||
});
|
25
src/__tests__/LoginPage/LoginPage.test.js
Normal file
@ -0,0 +1,25 @@
|
||||
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(
|
||||
<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();
|
||||
expect(screen.getByTestId('signin-container')).toBeInTheDocument();
|
||||
});
|
97
src/__tests__/LoginPage/SignIn.test.js
Normal file
@ -0,0 +1,97 @@
|
||||
import { render, waitFor, screen, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
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', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Signin component automatic navigation', () => {
|
||||
it('navigates to homepage when user is already logged in', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={true} setIsLoggedIn={() => {}} />);
|
||||
await expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
|
||||
it('navigates to homepage when auth is disabled', async () => {
|
||||
// mock request to check auth
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { http: {} } });
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sign in form', () => {
|
||||
beforeEach(() => {
|
||||
// mock auth check request
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 401,
|
||||
data: mockMgmtResponse
|
||||
});
|
||||
});
|
||||
|
||||
it('should change username and password values on user input', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
fireEvent.change(usernameInput, { target: { value: 'test' } });
|
||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||
expect(usernameInput).toHaveValue('test');
|
||||
expect(passwordInput).toHaveValue('test');
|
||||
expect(screen.getByTestId('openid-divider')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display error if username and password values are empty after change', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
const usernameInput = await screen.findByLabelText(/^Username/i);
|
||||
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
|
||||
userEvent.click(usernameInput);
|
||||
userEvent.type(usernameInput, 't');
|
||||
userEvent.type(usernameInput, '{backspace}');
|
||||
userEvent.click(passwordInput);
|
||||
userEvent.type(passwordInput, 't');
|
||||
userEvent.type(passwordInput, '{backspace}');
|
||||
const usernameError = await screen.findByText(/enter a username/i);
|
||||
const passwordError = await screen.findByText(/enter a password/i);
|
||||
await waitFor(() => expect(usernameError).toBeInTheDocument());
|
||||
await waitFor(() => expect(passwordError).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should log in the user and navigate to homepage if login is successful', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
const submitButton = await screen.findByText('Continue');
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {} } });
|
||||
fireEvent.click(submitButton);
|
||||
await waitFor(() => {
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
});
|
||||
|
||||
it('should should display login error if login not successful', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
const submitButton = await screen.findByText('Continue');
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} });
|
||||
fireEvent.click(submitButton);
|
||||
const errorDisplay = await screen.findByText(/Authentication Failed/i);
|
||||
await waitFor(() => {
|
||||
expect(errorDisplay).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
355
src/__tests__/RepoPage/Repo.test.js
Normal file
@ -0,0 +1,355 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import RepoDetails from 'components/Repo/RepoDetails';
|
||||
import React from 'react';
|
||||
import { api } from 'api';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const RepoDetailsThemeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvider>
|
||||
<RepoDetails />
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// uselocation mock
|
||||
const mockUseLocationValue = {
|
||||
pathname: "'localhost:3000/image/test'",
|
||||
search: ''
|
||||
};
|
||||
|
||||
const mockUseNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => {
|
||||
return { name: 'test' };
|
||||
},
|
||||
useLocation: () => {
|
||||
return mockUseLocationValue;
|
||||
},
|
||||
useNavigate: () => mockUseNavigate
|
||||
}));
|
||||
|
||||
const mockRepoDetailsData = {
|
||||
ExpandedRepoInfo: {
|
||||
Images: [
|
||||
{
|
||||
Digest: '2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718',
|
||||
Tag: 'latest'
|
||||
}
|
||||
],
|
||||
Summary: {
|
||||
Name: 'test',
|
||||
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
|
||||
}
|
||||
},
|
||||
Platforms: [
|
||||
{
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
],
|
||||
Digest: 'sha256:a8f5a986a9b5324ed3ffdcc30d798c4b0ac24f2c3d1a7cdb3f15ee8908377d74',
|
||||
Tag: 'slim',
|
||||
Title: 'node',
|
||||
Documentation: 'Node.js is a JavaScript-based platform for server-side and networking applications.\n',
|
||||
DownloadCount: 0,
|
||||
Source: 'https://github.com/nodejs/docker-node',
|
||||
Description: 'Node.js is a JavaScript-based platform for server-side and networking applications.',
|
||||
Licenses:
|
||||
'View [license information](https://github.com/nodejs/node/blob/master/LICENSE) for Node.js or [license information](https://github.com/nodejs/docker-node/blob/master/LICENSE) for the Node.js Docker project.',
|
||||
History: null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockRepoDetailsWithMissingData = {
|
||||
ExpandedRepoInfo: {
|
||||
Images: [
|
||||
{
|
||||
Digest: '2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718',
|
||||
Tag: 'latest'
|
||||
}
|
||||
],
|
||||
Summary: {
|
||||
Name: 'test',
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'CRITICAL',
|
||||
Count: 15
|
||||
}
|
||||
},
|
||||
Platforms: [
|
||||
{
|
||||
Os: 'linux',
|
||||
Arch: 'amd64'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
const mockRepoDetailsNone = {
|
||||
ExpandedRepoInfo: {
|
||||
Images: [
|
||||
{
|
||||
Digest: '2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718',
|
||||
Tag: 'latest'
|
||||
}
|
||||
],
|
||||
Summary: {
|
||||
Name: 'test1',
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'NONE',
|
||||
Count: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockRepoDetailsUnknown = {
|
||||
ExpandedRepoInfo: {
|
||||
Images: [
|
||||
{
|
||||
Digest: '2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718',
|
||||
Tag: 'latest'
|
||||
}
|
||||
],
|
||||
Summary: {
|
||||
Name: 'test1',
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'UNKNOWN',
|
||||
Count: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockRepoDetailsFailed = {
|
||||
ExpandedRepoInfo: {
|
||||
Images: [
|
||||
{
|
||||
Digest: '2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718',
|
||||
Tag: 'latest'
|
||||
}
|
||||
],
|
||||
Summary: {
|
||||
Name: 'test1',
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: '',
|
||||
Count: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockRepoDetailsLow = {
|
||||
ExpandedRepoInfo: {
|
||||
Images: [
|
||||
{
|
||||
Digest: '2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718',
|
||||
Tag: 'latest'
|
||||
}
|
||||
],
|
||||
Summary: {
|
||||
Name: 'test1',
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'LOW',
|
||||
Count: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockRepoDetailsMedium = {
|
||||
ExpandedRepoInfo: {
|
||||
Images: [
|
||||
{
|
||||
Digest: '2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718',
|
||||
Tag: 'latest'
|
||||
}
|
||||
],
|
||||
Summary: {
|
||||
Name: 'test1',
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'MEDIUM',
|
||||
Count: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockRepoDetailsHigh = {
|
||||
ExpandedRepoInfo: {
|
||||
Images: [
|
||||
{
|
||||
Digest: '2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718',
|
||||
Tag: 'latest'
|
||||
}
|
||||
],
|
||||
Summary: {
|
||||
Name: 'test1',
|
||||
NewestImage: {
|
||||
RepoName: 'mongo',
|
||||
IsSigned: true,
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window.document, 'cookie', {
|
||||
writable: true,
|
||||
value: 'user=test'
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Repo details component', () => {
|
||||
it('fetches repo detailed data and renders', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findByText('test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders placeholders for unavailable data', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsWithMissingData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findByText('test')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(/timestamp n\/a/i)).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders vulnerability icons', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsData } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsNone } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findAllByTestId('none-vulnerability-icon')).toHaveLength(1);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsUnknown } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findAllByTestId('unknown-vulnerability-icon')).toHaveLength(1);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsFailed } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findAllByTestId('failed-vulnerability-icon')).toHaveLength(1);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsLow } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(1);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsMedium } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
expect(await screen.findAllByTestId('medium-vulnerability-icon')).toHaveLength(1);
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsHigh } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
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(() => {});
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should redirect to homepage if it receives invalid data', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: null, errors: ['testerror'] } });
|
||||
render(<RepoDetailsThemeWrapper />);
|
||||
await waitFor(() => expect(mockUseNavigate).toBeCalledWith('/home'));
|
||||
});
|
||||
|
||||
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 />);
|
||||
const osChip = await screen.findByText(/linux/i);
|
||||
fireEvent.click(osChip);
|
||||
expect(mockUseNavigate).toHaveBeenCalledWith({
|
||||
pathname: '/explore',
|
||||
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();
|
||||
});
|
||||
});
|
39
src/__tests__/RepoPage/RepoPage.test.js
Normal file
@ -0,0 +1,39 @@
|
||||
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 MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: 'localhost:3000/image/test',
|
||||
state: { lastDate: '' }
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'components/Repo/RepoDetails',
|
||||
() =>
|
||||
function RepoDetails() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders the repository page component', () => {
|
||||
render(
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<RepoPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
expect(screen.getByTestId('repo-container')).toBeInTheDocument();
|
||||
});
|
136
src/__tests__/RepoPage/Tags.test.js
Normal file
@ -0,0 +1,136 @@
|
||||
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', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
const mockedTagsData = [
|
||||
{
|
||||
tag: 'latest',
|
||||
vendor: 'test1',
|
||||
isDeletable: true,
|
||||
manifests: [
|
||||
{
|
||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||
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'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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(<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(<TagsThemeWrapper />);
|
||||
const tagLink = await screen.findByText('latest');
|
||||
fireEvent.click(tagLink);
|
||||
await waitFor(() => {
|
||||
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(<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');
|
||||
await waitFor(() => expect(screen.queryByText(/latest/i)).not.toBeInTheDocument());
|
||||
expect(await screen.findByText(/bullseye/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should sort tags based on the picked sort criteria', async () => {
|
||||
render(<TagsThemeWrapper />);
|
||||
const selectFilter = await screen.findByText('Newest');
|
||||
expect(selectFilter).toBeInTheDocument();
|
||||
userEvent.click(selectFilter);
|
||||
const newOption = await screen.findByText('A - Z');
|
||||
userEvent.click(newOption);
|
||||
expect(await screen.findByText('A - Z')).toBeInTheDocument();
|
||||
expect(await screen.queryByText('Newest')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
38
src/__tests__/Shared/PreviewCard.test.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import PreviewCard from 'components/Shared/PreviewCard';
|
||||
|
||||
// usenavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
// image mock
|
||||
const mockImage = {
|
||||
name: 'alpine',
|
||||
latestVersion: 'latest',
|
||||
lastUpdated: '2022-05-23T19:19:30.413290187Z',
|
||||
description: '',
|
||||
licenses: '',
|
||||
vendor: '',
|
||||
size: '585',
|
||||
tags: ''
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Preview card component', () => {
|
||||
it('navigates to repo page when clicked', async () => {
|
||||
render(<PreviewCard name={mockImage.name} lastUpdated={mockImage.lastUpdated} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
expect(mockedUsedNavigate).toBeCalledWith(`/image/${mockImage.name}`);
|
||||
});
|
||||
});
|
102
src/__tests__/Shared/RepoCard.test.js
Normal file
@ -0,0 +1,102 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import RepoCard from 'components/Shared/RepoCard';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// usenavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
// image mock
|
||||
const mockImage = {
|
||||
name: 'alpine',
|
||||
latestVersion: 'latest',
|
||||
lastUpdated: '2022-05-23T19:19:30.413290187Z',
|
||||
description: '',
|
||||
licenses: '',
|
||||
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();
|
||||
});
|
||||
|
||||
describe('Repo card component', () => {
|
||||
it('navigates to repo page when clicked', async () => {
|
||||
render(<RepoCardWrapper image={mockImage} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
expect(mockedUsedNavigate).toBeCalledWith(`/image/${mockImage.name}`);
|
||||
});
|
||||
|
||||
it('renders placeholders for missing data', async () => {
|
||||
render(<RepoCardWrapper image={{ ...mockImage, lastUpdated: '' }} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
expect(mockedUsedNavigate).toBeCalledWith(`/image/${mockImage.name}`);
|
||||
expect(await screen.findByText(/timestamp n\/a/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to explore page when platform chip is clicked', async () => {
|
||||
render(<RepoCardWrapper image={mockImage} />);
|
||||
const osChip = await screen.findByText(/linux/i);
|
||||
fireEvent.click(osChip);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
pathname: '/explore',
|
||||
search: createSearchParams({ filter: 'linux' }).toString()
|
||||
});
|
||||
});
|
||||
});
|
128
src/__tests__/Shared/SearchSuggestion.test.js
Normal file
@ -0,0 +1,128 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import SearchSuggestion from 'components/Header/SearchSuggestion';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
// router mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
const RouterSearchWrapper = (props) => {
|
||||
const queryString = props.search || '';
|
||||
return (
|
||||
<MemoryRouter initialEntries={[queryString]}>
|
||||
<SearchSuggestion />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
const mockImageList = {
|
||||
GlobalSearch: {
|
||||
Repos: [
|
||||
{
|
||||
Name: 'alpine',
|
||||
Size: '2806985',
|
||||
LastUpdated: '2022-08-09T17:19:53.274069586Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: 'w',
|
||||
IsSigned: true,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'mongo',
|
||||
Size: '231383863',
|
||||
LastUpdated: '2022-08-02T01:30:49.193203152Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: false,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
Name: 'nodeUnique',
|
||||
Size: '369311301',
|
||||
LastUpdated: '2022-08-23T00:20:40.144281895Z',
|
||||
NewestImage: {
|
||||
Tag: 'latest',
|
||||
Description: '',
|
||||
IsSigned: false,
|
||||
Licenses: '',
|
||||
Vendor: '',
|
||||
Labels: ''
|
||||
}
|
||||
}
|
||||
],
|
||||
Images: [
|
||||
{
|
||||
RepoName: 'debian',
|
||||
Tag: 'testTag'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Search component', () => {
|
||||
it('should display suggestions when user searches', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<RouterSearchWrapper />);
|
||||
const searchInput = screen.getByPlaceholderText(/search for content/i);
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
userEvent.type(searchInput, 'test');
|
||||
expect(await screen.findByText(/alpine/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to repo page when a repo suggestion is clicked', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<RouterSearchWrapper />);
|
||||
const searchInput = screen.getByPlaceholderText(/search for content/i);
|
||||
userEvent.type(searchInput, 'test');
|
||||
const suggestionItemRepo = await screen.findByText(/alpine/i);
|
||||
userEvent.click(suggestionItemRepo);
|
||||
await waitFor(() => expect(mockedUsedNavigate).toHaveBeenCalledWith('/image/alpine'));
|
||||
});
|
||||
|
||||
it('should navigate to repo page when a image suggestion is clicked', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } });
|
||||
render(<RouterSearchWrapper />);
|
||||
const searchInput = screen.getByPlaceholderText(/search for content/i);
|
||||
userEvent.type(searchInput, 'debian:test');
|
||||
const suggestionItemImage = await screen.findByText(/debian:testTag/i);
|
||||
userEvent.click(suggestionItemImage);
|
||||
await waitFor(() => expect(mockedUsedNavigate).toHaveBeenCalledWith('/image/debian/tag/testTag'));
|
||||
});
|
||||
|
||||
it('should log an error if it doesnt receive an ok response for repo search', async () => {
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<RouterSearchWrapper />);
|
||||
const searchInput = screen.getByPlaceholderText(/search for content/i);
|
||||
userEvent.type(searchInput, 'debian');
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should log an error if it doesnt receive an ok response for image search', async () => {
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<RouterSearchWrapper />);
|
||||
const searchInput = screen.getByPlaceholderText(/search for content/i);
|
||||
userEvent.type(searchInput, 'debian:test');
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
});
|
118
src/__tests__/TagPage/DependsOn.test.js
Normal file
@ -0,0 +1,118 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import DependsOn from 'components/Tag/Tabs/DependsOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const mockDependenciesList = {
|
||||
data: {
|
||||
BaseImageList: {
|
||||
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 RouterDependsWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<DependsOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
// IntersectionObserver isn't available in test environment
|
||||
const mockIntersectionObserver = jest.fn();
|
||||
mockIntersectionObserver.mockReturnValue({
|
||||
observe: () => null,
|
||||
unobserve: () => null,
|
||||
disconnect: () => null
|
||||
});
|
||||
window.IntersectionObserver = mockIntersectionObserver;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Dependencies tab', () => {
|
||||
it('should render the dependencies if there are any', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependenciesList });
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findAllByText(/Tag/i)).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('renders no dependencies if there are not any', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 200,
|
||||
data: { data: { BaseImageList: { Results: [], Page: {} } } }
|
||||
});
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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(<RouterDependsWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should stop loading if the api response contains an error', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 500, data: { errors: ['test error'] } });
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
51
src/__tests__/TagPage/HistoryLayers.test.js
Normal file
@ -0,0 +1,51 @@
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import HistoryLayers from 'components/Tag/Tabs/HistoryLayers';
|
||||
import React from 'react';
|
||||
|
||||
const mockLayersList = [
|
||||
{
|
||||
Layer: { Size: '2806054', Digest: '213ec9aee27d8be045c6a92b7eac22c9a64b44558193775a1a7f626352392b49' },
|
||||
HistoryDescription: {
|
||||
Created: '2022-08-09T17:19:53.274069586Z',
|
||||
CreatedBy: '/bin/sh -c #(nop) ADD file:2a949686d9886ac7c10582a6c29116fd29d3077d02755e87e111870d63607725 in / ',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: false
|
||||
}
|
||||
},
|
||||
{
|
||||
Layer: null,
|
||||
HistoryDescription: {
|
||||
Created: '2022-08-09T17:19:53.47374331Z',
|
||||
CreatedBy: '/bin/sh -c #(nop) CMD ["/bin/sh"]',
|
||||
Author: '',
|
||||
Comment: '',
|
||||
EmptyLayer: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Layers page', () => {
|
||||
it('renders the layers if there are any', async () => {
|
||||
render(<HistoryLayers name="alpine:latest" history={mockLayersList} />);
|
||||
expect(await screen.findAllByTestId('layer-card-container')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders no layers if there are not any', async () => {
|
||||
render(<HistoryLayers name="alpine:latest" history={[]} />);
|
||||
await waitFor(() => expect(screen.getAllByText(/No Layer data available/i)).toHaveLength(1));
|
||||
});
|
||||
|
||||
it('opens dropdown and renders layer command and digest', async () => {
|
||||
render(<HistoryLayers name="alpine:latest" history={mockLayersList} />);
|
||||
expect(screen.queryAllByText(/DIGEST/i)).toHaveLength(0);
|
||||
const openDetails = await screen.findAllByText(/details/i);
|
||||
fireEvent.click(openDetails[0]);
|
||||
expect(await screen.findAllByText(/DIGEST/i)).toHaveLength(1);
|
||||
});
|
||||
});
|
118
src/__tests__/TagPage/IsDependentOn.test.js
Normal file
@ -0,0 +1,118 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { api } from 'api';
|
||||
import IsDependentOn from 'components/Tag/Tabs/IsDependentOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
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: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
RepoName: 'tag4',
|
||||
Tag: 'tag4',
|
||||
Manifests: [],
|
||||
Vulnerabilities: {
|
||||
MaxSeverity: 'HIGH',
|
||||
Count: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const RouterDependsWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<IsDependentOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
// IntersectionObserver isn't available in test environment
|
||||
const mockIntersectionObserver = jest.fn();
|
||||
mockIntersectionObserver.mockReturnValue({
|
||||
observe: () => null,
|
||||
unobserve: () => null,
|
||||
disconnect: () => null
|
||||
});
|
||||
window.IntersectionObserver = mockIntersectionObserver;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Dependents tab', () => {
|
||||
it('should render the dependents if there are any', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependentsList });
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findAllByText(/tag/i)).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('renders no dependents if there are not any', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 200,
|
||||
data: { data: { DerivedImageList: { Results: [], Page: {} } } }
|
||||
});
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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(<RouterDependsWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should stop loading if the api response contains an error', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 500, data: { errors: ['test error'] } });
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<RouterDependsWrapper />);
|
||||
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
86
src/__tests__/TagPage/ReferredBy.test.js
Normal file
@ -0,0 +1,86 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ReferredBy from 'components/Tag/Tabs/ReferredBy';
|
||||
import React from 'react';
|
||||
|
||||
const mockReferrersList = [
|
||||
{
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Referred by tab', () => {
|
||||
it('should render referrers if there are any', async () => {
|
||||
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 () => {
|
||||
render(<ReferredBy referrers={[]} />);
|
||||
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display the digest when clicking the dropdowns', async () => {
|
||||
render(<ReferredBy referrers={mockReferrersList} />);
|
||||
const firstDigest = (await screen.findAllByText(/digest/i))[0];
|
||||
expect(firstDigest).toBeInTheDocument();
|
||||
await userEvent.click(firstDigest);
|
||||
expect(
|
||||
await screen.findByText(/sha256:be7a3d01c35a2cf53c502e9dc50cdf36b15d9361c81c63bf319f1d5cbe44ab7c/i)
|
||||
).toBeInTheDocument();
|
||||
await userEvent.click(firstDigest);
|
||||
expect(
|
||||
await screen.findByText(/sha256:be7a3d01c35a2cf53c502e9dc50cdf36b15d9361c81c63bf319f1d5cbe44ab7c/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display the annotations when clicking the dropdown', async () => {
|
||||
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);
|
||||
});
|
||||
});
|
1070
src/__tests__/TagPage/TagDetails.test.js
Normal file
44
src/__tests__/TagPage/TagPage.test.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import TagPage from 'pages/TagPage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'components/Tag/TagDetails',
|
||||
() =>
|
||||
function TagDetails() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/ExploreHeader',
|
||||
() =>
|
||||
function ExploreHeader() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the tags page component', async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<TagPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
expect(screen.getByTestId('tag-container')).toBeInTheDocument();
|
||||
});
|
872
src/__tests__/TagPage/VulnerabilitiesDetails.test.js
Normal file
@ -0,0 +1,872 @@
|
||||
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 (
|
||||
<MockThemeProvider>
|
||||
<MemoryRouter>
|
||||
<VulnerabilitiesDetails name="mongo" />
|
||||
</MemoryRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const simpleMockCVEList = {
|
||||
CVEListForImage: {
|
||||
Tag: '',
|
||||
Page: { ItemCount: 2, TotalCount: 2 },
|
||||
Summary: {
|
||||
Count: 2,
|
||||
UnknownCount: 0,
|
||||
LowCount: 0,
|
||||
MediumCount: 1,
|
||||
HighCount: 0,
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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',
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '2.31-0ubuntu9.9',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'libc6',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
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',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '1:1.2.11.dfsg-2ubuntu1.3',
|
||||
FixedVersion: 'Not Specified'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
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: {
|
||||
Page: { TotalCount: 5, ItemCount: 3 },
|
||||
Results: [
|
||||
{
|
||||
Tag: '1.0.16'
|
||||
},
|
||||
{
|
||||
Tag: '0.4.33'
|
||||
},
|
||||
{
|
||||
Tag: '1.0.17'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
pageTwo: {
|
||||
ImageListWithCVEFixed: {
|
||||
Page: { TotalCount: 5, ItemCount: 2 },
|
||||
Results: [
|
||||
{
|
||||
Tag: 'slim'
|
||||
},
|
||||
{
|
||||
Tag: 'latest'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// IntersectionObserver isn't available in test environment
|
||||
const mockIntersectionObserver = jest.fn();
|
||||
mockIntersectionObserver.mockReturnValue({
|
||||
observe: () => null,
|
||||
unobserve: () => null,
|
||||
disconnect: () => null
|
||||
});
|
||||
window.IntersectionObserver = mockIntersectionObserver;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Vulnerabilties page', () => {
|
||||
it('renders the vulnerabilities if there are any', async () => {
|
||||
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('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: [], Summary: {} } } }
|
||||
});
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1));
|
||||
});
|
||||
|
||||
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 />);
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
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(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should find out which version fixes the CVEs', async () => {
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageOne } })
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon');
|
||||
fireEvent.click(expandListBtn[1]);
|
||||
await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getAllByText(/Load more/).length).toBe(1));
|
||||
const loadMoreBtn = screen.getAllByText(/Load more/)[0];
|
||||
await fireEvent.click(loadMoreBtn);
|
||||
await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument());
|
||||
expect(await screen.findByText('latest')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the list of vulnerable packages for the CVEs', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: simpleMockCVEList } })
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
const expandListBtn = await screen.findByTestId('expand-list-view-toggle');
|
||||
fireEvent.click(expandListBtn);
|
||||
const packageLists = await screen.findAllByTestId('cve-package-list');
|
||||
expect(packageLists.length).toEqual(2); // Data set has 2 CVEs, so 2 package lists
|
||||
|
||||
const expectedData = [
|
||||
{
|
||||
Name: 'perl-base',
|
||||
PackagePath: 'Not Specified',
|
||||
InstalledVersion: '5.30.0-9ubuntu0.2',
|
||||
FixedVersion: 'Not Specified'
|
||||
},
|
||||
{
|
||||
Name: 'org.springframework:spring-web',
|
||||
PackagePath: 'usr/local/tomcat/webapps/spring4shell.war/WEB-INF/lib/spring-web-5.3.15.jar',
|
||||
InstalledVersion: '5.3.15',
|
||||
FixedVersion: '6.0.0'
|
||||
}
|
||||
];
|
||||
|
||||
for (let index = 0; index < 2; index++) {
|
||||
const expectedPackageData = expectedData[index];
|
||||
const container = packageLists[index];
|
||||
const pkgName = await within(container).findAllByTestId('cve-info-pkg-name');
|
||||
expect(pkgName).toHaveLength(1);
|
||||
expect(pkgName[0]).toHaveTextContent(expectedPackageData.Name);
|
||||
|
||||
const pkgPath = await within(container).findAllByTestId('cve-info-pkg-path');
|
||||
expect(pkgPath).toHaveLength(1);
|
||||
expect(pkgPath[0]).toHaveTextContent(expectedPackageData.PackagePath);
|
||||
|
||||
const pkgInstalledVer = await within(container).findAllByTestId('cve-info-pkg-install-ver');
|
||||
expect(pkgInstalledVer).toHaveLength(1);
|
||||
expect(pkgInstalledVer[0]).toHaveTextContent(expectedPackageData.InstalledVersion);
|
||||
|
||||
const pkgFixedVer = await within(container).findAllByTestId('cve-info-pkg-fixed-ver');
|
||||
expect(pkgFixedVer).toHaveLength(1);
|
||||
expect(pkgFixedVer[0]).toHaveTextContent(expectedPackageData.FixedVersion);
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow export of vulnerabilities list', async () => {
|
||||
const xlsxMock = jest.createMockFromModule('xlsx');
|
||||
xlsxMock.writeFile = jest.fn();
|
||||
|
||||
jest.spyOn(api, 'get').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 () => {
|
||||
jest
|
||||
.spyOn(api, 'get')
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
|
||||
.mockRejectedValue({ status: 500, data: {} });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon');
|
||||
fireEvent.click(expandListBtn[1]);
|
||||
await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument());
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
});
|
19
src/__tests__/api.test.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { api } from '../api';
|
||||
|
||||
describe('api module', () => {
|
||||
it('should redirect to login if a 401 error is received', () => {
|
||||
const location = new URL('https://www.test.com');
|
||||
location.replace = jest.fn();
|
||||
delete window.location;
|
||||
window.location = location;
|
||||
const axiosInstance = api.getAxiosInstance();
|
||||
expect(
|
||||
axiosInstance.interceptors.response.handlers[0].rejected({
|
||||
response: { statusText: 'Unauthorized', status: 401 }
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
response: { statusText: 'Unauthorized', status: 401 }
|
||||
});
|
||||
expect(location.replace).toHaveBeenCalledWith('/login');
|
||||
});
|
||||
});
|
172
src/api.js
Normal file
@ -0,0 +1,172 @@
|
||||
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) {
|
||||
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'
|
||||
};
|
||||
// withCredentials option must be enabled on cross-origin
|
||||
return {
|
||||
headers: genericHeaders,
|
||||
withCredentials: host() !== window?.location?.origin && authConfig !== null
|
||||
};
|
||||
},
|
||||
|
||||
get(urli, abortSignal, cfg) {
|
||||
let config = isEmpty(cfg) ? this.getRequestCfg() : cfg;
|
||||
if (!isEmpty(abortSignal) && isEmpty(config.signal)) {
|
||||
config = { ...config, signal: abortSignal };
|
||||
}
|
||||
return axios.get(urli, config);
|
||||
},
|
||||
|
||||
post(urli, payload, abortSignal, cfg) {
|
||||
let config = isEmpty(cfg) ? this.getRequestCfg() : cfg;
|
||||
if (!isEmpty(abortSignal) && isEmpty(config.signal)) {
|
||||
config = { ...config, signal: abortSignal };
|
||||
}
|
||||
return axios.post(urli, payload, config);
|
||||
},
|
||||
|
||||
put(urli, payload, abortSignal, cfg) {
|
||||
let config = isEmpty(cfg) ? this.getRequestCfg() : cfg;
|
||||
if (!isEmpty(abortSignal) && isEmpty(config.signal)) {
|
||||
config = { ...config, signal: abortSignal };
|
||||
}
|
||||
return axios.put(urli, payload, config);
|
||||
},
|
||||
|
||||
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 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} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor IsDeletable } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||
detailedImageInfo: (name, tag) =>
|
||||
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
||||
vulnerabilitiesForRepo: (
|
||||
name,
|
||||
{ pageNumber = 1, pageSize = 15 },
|
||||
searchTerm = '',
|
||||
excludedTerm = '',
|
||||
severity = ''
|
||||
) => {
|
||||
let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
}}`;
|
||||
if (!isEmpty(searchTerm)) {
|
||||
query += `, searchedCVE: "${searchTerm}"`;
|
||||
}
|
||||
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
|
||||
}}${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 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 SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count}}}}`,
|
||||
globalSearch: ({
|
||||
searchQuery = '""',
|
||||
pageNumber = 1,
|
||||
pageSize = 15,
|
||||
sortBy = sortByCriteria.relevance.value,
|
||||
filter = {}
|
||||
}) => {
|
||||
const searchParam = !isEmpty(searchQuery) ? `query:"${searchQuery}"` : `query:""`;
|
||||
const paginationParam = `requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
} sortBy: ${sortBy}}`;
|
||||
let 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)}` : '""'}`;
|
||||
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 } 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:""`;
|
||||
const paginationParam = `requestedPage: {limit:${pageSize} offset:${(pageNumber - 1) * pageSize} sortBy:RELEVANCE}`;
|
||||
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}}}`,
|
||||
bookmarkToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleBookmark`,
|
||||
starToggle: (repo) => `/v2/_zot/ext/userprefs?repo=${repo}&action=toggleStar`
|
||||
};
|
||||
|
||||
export { api, endpoints };
|
401
src/assets/Alt_Linux_Team.svg
Normal file
@ -0,0 +1,401 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
||||
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
||||
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
||||
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
||||
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
||||
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
||||
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
||||
]>
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="220px" height="97px"
|
||||
viewBox="0 0 220 97" style="enable-background:new 0 0 220 97;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#FDC811;}
|
||||
.st2{fill-rule:evenodd;clip-rule:evenodd;}
|
||||
.st3{fill:#FFCA07;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="&ns_sfw;">
|
||||
<slices></slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="96.7914734" width="220" x="0" y="0.2085275"></sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<g>
|
||||
<path class="st0" d="M219.9046783,66.5411987c0-0.0004807,0-0.0004807-0.0004883-0.0004807l-0.1141663-0.3805542
|
||||
c-0.1006775-0.3381577-0.3820038-1.0241241-1.1329956-1.8189468c-0.3661041-0.389225-0.8290253-0.7047501-1.5294342-1.1821251
|
||||
c-2.2240753-1.5154724-2.5318909-1.9957428-2.5627136-2.0737801c-0.0192719-0.0703278-0.0419159-0.1401787-0.0679321-0.2095451
|
||||
c-0.465332-1.2375221-0.0703278-3.2380791,0.3126373-5.1764946c0.1690826-0.8574486,0.3439484-1.7442856,0.4484711-2.5704231
|
||||
c1.3285675-8.9984093,0.8213348-13.87817-0.8174591-21.4073524l-0.3270874-1.5911007
|
||||
c-0.7245026-3.5492649-1.3507233-6.6144085-2.5458527-9.9276352c-0.3275757-0.9142933-0.8256531-1.8305111-1.2245178-2.5641613
|
||||
l-0.3015594-0.5631237c-0.2601166-0.4952011-0.3265991-0.7649612-0.4701538-1.3478346
|
||||
c-0.1478729-0.6922226-0.1950836-1.4258718-0.2456665-2.2043209c-0.0761108-1.1883879-0.1546326-2.4172392-0.5746918-3.7087135
|
||||
c-0.5130157-1.6556492-1.9152832-3.5931015-3.3401947-4.6119261c-0.7866364-0.5443363-1.6267548-0.7957907-2.3021088-0.9976287
|
||||
c-0.2606049-0.0785193-0.6546478-0.197021-0.7668915-0.2663877c-0.0414276-0.0255308-0.0838165-0.0500982-0.1247559-0.0717754
|
||||
L201.08461,3.2608931c-1.4870605-0.7914555-3.0251617-1.6098869-4.4828339-2.528033
|
||||
c-0.868042-0.595398-2.7115631-1.1594846-4.2304077-0.2716864l-0.4007874,0.2345945
|
||||
c-0.6700592,0.3925966-1.0891571,1.0905997-1.1214294,1.8661585c-0.0327606,0.7760406,0.3270874,1.5058367,0.9629517,1.9528668
|
||||
l0.3781433,0.2663879c1.8016052,1.2697968,2.8006744,2.6739922,3.235672,4.5401506l0.1151276,0.5587883
|
||||
c-1.3719177,0.9205542-2.8353729,2.2626085-4.3209686,5.3104105c-0.5501251,1.1281738-0.9576569,2.0024834-1.3661499,2.8792019
|
||||
c-0.2663879,0.5722752-0.5202484,1.1156483-0.7996368,1.69804H20.0874577c-0.2726498,0-0.4932747,0.2206249-0.4932747,0.4932747
|
||||
v16.3561058c-0.0178242-0.0038567-0.0356464-0.0077095-0.0534706-0.0110817
|
||||
c-1.5858002-0.3246727-3.5699797-0.482193-6.0652561-0.482193c-1.5853195,0-3.0940466,0.1353607-4.4847536,0.40271
|
||||
c-1.5434103,0.297699-2.766963,0.7326889-3.7405062,1.3300133c-1.30159,0.7981987-2.3281217,1.777523-3.0521371,2.9114761
|
||||
c-0.7008934,1.0978241-1.2066927,2.4841957-1.5463009,4.2390785l-0.403676,2.087265
|
||||
c-0.0260125,0.1343994,0.0048171,0.2740974,0.0862267,0.3848915c0.0809279,0.1103134,0.2042466,0.1825676,0.3405715,0.1984673
|
||||
l3.2886589,0.3853683c-0.8357732,0.4894218-1.5164344,1.0597725-2.0588441,1.7226067
|
||||
C0.6406791,51.3267593,0,53.2622833,0,55.5383797c0,2.4832344,0.8733468,4.60952,2.5959547,6.319603
|
||||
c1.7182724,1.7038193,4.1422558,2.5680122,7.2040272,2.5680122c2.1498871,0,4.097456-0.3882599,5.7921238-1.1556282
|
||||
c0.2543449-0.1175385,0.5096531-0.2509727,0.763998-0.3983765c0.1806431,0.3559837,0.3945236,0.7779655,0.3945236,0.7779655
|
||||
c0.0838184,0.1657104,0.2538624,0.2702408,0.4398041,0.2702408h2.4037514v5.0753326
|
||||
c0,0.2726517,0.2206249,0.4932785,0.4932747,0.4932785h181.1667023c0.0476837,0,0.0939331-0.007225,0.1377716-0.0211945h16.3368378
|
||||
c0.7163086,0,1.397934-0.3424988,1.823288-0.9162216C219.9788513,67.9786377,220.1103668,67.2266769,219.9046783,66.5411987z"/>
|
||||
<rect x="22.0607033" y="22.2340794" class="st1" width="177.2202148" height="44.7882233"/>
|
||||
<path class="st1" d="M10.4436264,46.3384933L3.07324,45.4756279c0.2770877-1.432476,0.6785073-2.5584679,1.2031634-3.3794403
|
||||
c0.5240412-0.8209724,1.2788663-1.5331688,2.263741-2.1373215c0.7077856-0.4343758,1.6802859-0.7717323,2.9178729-1.010601
|
||||
c1.2384424-0.2381325,2.5775766-0.357933,4.0175247-0.357933c2.3113928,0,4.1679554,0.1447868,5.5702972,0.4321671
|
||||
c1.4023438,0.2873764,2.5702286,0.8878555,3.5051231,1.7999687c0.6570721,0.6313477,1.1752357,1.5265541,1.5537491,2.6841507
|
||||
c0.3792496,1.158329,0.5688763,2.2630043,0.5688763,3.316227v9.8730011c0,1.0532265,0.0595322,1.8778725,0.180069,2.4739418
|
||||
c0.1198025,0.5968056,0.3815765,1.3575096,0.7858143,2.2843208h-7.2374744
|
||||
c-0.2910519-0.5754929-0.4806767-1.0135384-0.5688744-1.3156128c-0.0881977-0.3020821-0.1771297-0.7754059-0.2653294-1.4214554
|
||||
c-1.0105972,1.0818901-2.0145807,1.8528862-3.0126848,2.3159256c-1.3641233,0.617382-2.9494772,0.9268074-4.7553253,0.9268074
|
||||
c-2.4004464,0-4.2224631-0.6181183-5.4667859-1.8528824c-1.2443225-1.2347679-1.8661156-2.7583847-1.8661156-4.5686417
|
||||
c0-1.6978073,0.448338-3.095005,1.3450146-4.1893921c0.8966765-1.0943832,2.5518527-1.9087448,4.9640594-2.4416008
|
||||
c2.8928833-0.6460495,4.7685547-1.0987968,5.6277466-1.3582458c0.8591928-0.2594528,1.768364-0.6004829,2.7282486-1.0216255
|
||||
c0-1.0517578-0.1955051-1.7889442-0.5872498-2.2100868c-0.3917446-0.4211464-1.0796862-0.6320839-2.0652952-0.6320839
|
||||
c-1.2634325,0-2.2108221,0.2249069-2.8421707,0.6739807C11.1447973,44.7105141,10.7471733,45.3697929,10.4436264,46.3384933z
|
||||
M17.1319504,50.8431931c-1.0605774,0.4211426-2.165988,0.7937775-3.3154974,1.116436
|
||||
c-1.5669785,0.4630356-2.5577326,0.9187279-2.9752016,1.3678017c-0.4292297,0.463768-0.6437235,0.9900131-0.6437235,1.5794716
|
||||
c0,0.6732407,0.2108192,1.2244759,0.6341677,1.6522331c0.4233494,0.4284973,1.0451431,0.6423759,1.8668518,0.6423759
|
||||
c0.8584576,0,1.6581163-0.2315178,2.3967714-0.6945572c0.738656-0.4637756,1.2626982-1.027504,1.5728598-1.6948662
|
||||
c0.3094254-0.666626,0.4637718-1.5331688,0.4637718-2.6003609V50.8431931z"/>
|
||||
<g>
|
||||
<path d="M10.4436264,46.3384933L3.07324,45.4756279c0.2770877-1.432476,0.6785073-2.5584679,1.2031634-3.3794403
|
||||
c0.5240412-0.8209724,1.2788663-1.5331688,2.263741-2.1373215c0.7077856-0.4343758,1.6802859-0.7717323,2.9178729-1.010601
|
||||
c1.2384424-0.2381325,2.5775766-0.357933,4.0175247-0.357933c2.3113928,0,4.1679554,0.1447868,5.5702972,0.4321671
|
||||
c1.4023438,0.2873764,2.5702286,0.8878555,3.5051231,1.7999687c0.6570721,0.6313477,1.1752357,1.5265541,1.5537491,2.6841507
|
||||
c0.3792496,1.158329,0.5688763,2.2630043,0.5688763,3.316227v9.8730011c0,1.0532265,0.0595322,1.8778725,0.180069,2.4739418
|
||||
c0.1198025,0.5968056,0.3815765,1.3575096,0.7858143,2.2843208h-7.2374744
|
||||
c-0.2910519-0.5754929-0.4806767-1.0135384-0.5688744-1.3156128c-0.0881977-0.3020821-0.1771297-0.7754059-0.2653294-1.4214554
|
||||
c-1.0105972,1.0818901-2.0145807,1.8528862-3.0126848,2.3159256c-1.3641233,0.617382-2.9494772,0.9268074-4.7553253,0.9268074
|
||||
c-2.4004464,0-4.2224631-0.6181183-5.4667859-1.8528824c-1.2443225-1.2347679-1.8661156-2.7583847-1.8661156-4.5686417
|
||||
c0-1.6978073,0.448338-3.095005,1.3450146-4.1893921c0.8966765-1.0943832,2.5518527-1.9087448,4.9640594-2.4416008
|
||||
c2.8928833-0.6460495,4.7685547-1.0987968,5.6277466-1.3582458c0.8591928-0.2594528,1.768364-0.6004829,2.7282486-1.0216255
|
||||
c0-1.0517578-0.1955051-1.7889442-0.5872498-2.2100868c-0.3917446-0.4211464-1.0796862-0.6320839-2.0652952-0.6320839
|
||||
c-1.2634325,0-2.2108221,0.2249069-2.8421707,0.6739807C11.1447973,44.7105141,10.7471733,45.3697929,10.4436264,46.3384933z
|
||||
M17.1319504,50.8431931c-1.0605774,0.4211426-2.165988,0.7937775-3.3154974,1.116436
|
||||
c-1.5669785,0.4630356-2.5577326,0.9187279-2.9752016,1.3678017c-0.4292297,0.463768-0.6437235,0.9900131-0.6437235,1.5794716
|
||||
c0,0.6732407,0.2108192,1.2244759,0.6341677,1.6522331c0.4233494,0.4284973,1.0451431,0.6423759,1.8668518,0.6423759
|
||||
c0.8584576,0,1.6581163-0.2315178,2.3967714-0.6945572c0.738656-0.4637756,1.2626982-1.027504,1.5728598-1.6948662
|
||||
c0.3094254-0.666626,0.4637718-1.5331688,0.4637718-2.6003609V50.8431931z"/>
|
||||
<g>
|
||||
<path d="M28.7858028,30.5908012h7.7305279v30.863308h-7.7305279V30.5908012z"/>
|
||||
<path d="M49.97015,30.5908012v8.5051918h4.2445145v6.2737999H49.97015v7.920887
|
||||
c0,0.9525337,0.0815849,1.583149,0.2462196,1.8911018c0.2528343,0.4762688,0.6945572,0.7144051,1.3259048,0.7144051
|
||||
c0.5688744,0,1.3649826-0.1815414,2.3879547-0.5468292l0.5681381,5.9151268
|
||||
c-1.9071541,0.4630356-3.6881332,0.6952896-5.3433113,0.6952896c-1.9197655,0-3.3346062-0.2734146-4.2437744-0.8202362
|
||||
c-0.9099083-0.5468254-1.5824165-1.3773537-2.018261-2.4915848c-0.4358406-1.1142311-0.6533966-2.919342-0.6533966-5.413868
|
||||
v-7.8642921h-2.8421707V39.095993h2.8421707v-4.1055984L49.97015,30.5908012z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M65.8155975,30.5908012h7.7305298v30.863308h-7.7305298V30.5908012z"/>
|
||||
<path d="M77.9810028,30.5908012h7.7114105v5.831337h-7.7114105V30.5908012z M77.9810028,39.095993h7.7114105v22.3581161h-7.7114105
|
||||
V39.095993z"/>
|
||||
<path d="M89.8428497,39.095993h7.1807632v3.6418304c1.0738068-1.4875984,2.1608429-2.5503807,3.2596436-3.1890793
|
||||
c1.098793-0.6386986,2.4379272-0.9584122,4.0166626-0.9584122c2.1343842,0,3.8057327,0.7055779,5.0118408,2.1160088
|
||||
c1.2060928,1.4111595,1.8095169,3.5896416,1.8095169,6.5369186v14.2108498h-7.7496338v-12.294754
|
||||
c0-1.4030838-0.2337189-2.3960419-0.7011719-2.9788818c-0.467453-0.5820999-1.124527-0.8738899-1.9704895-0.8738899
|
||||
c-0.9348907,0-1.6926575,0.3932152-2.274025,1.1789093c-0.5806351,0.7864265-0.8716812,2.1975937-0.8716812,4.2312813v10.7373352
|
||||
h-7.7114258V39.095993z"/>
|
||||
<path d="M136.1517639,61.4541092h-7.200592V57.832859c-1.0730743,1.4883385-2.156311,2.5474434-3.2493515,3.178791
|
||||
c-1.0929184,0.6320839-2.4342575,0.9481239-4.0262222,0.9481239c-2.1218872,0-3.7866211-0.7055817-4.9927216-2.1160088
|
||||
s-1.8093948-3.5822945-1.8093948-6.5163345V39.095993h7.7495041v12.2954979c0,1.4030762,0.2337341,2.3997078,0.7011719,2.98843
|
||||
c0.467453,0.5901909,1.124527,0.884182,1.9704895,0.884182c0.9216614,0,1.6766129-0.3924789,2.264595-1.1781731
|
||||
c0.5871201-0.7856979,0.8811264-2.1968613,0.8811264-4.2320213V39.095993h7.7113953V61.4541092z"/>
|
||||
<path d="M138.3317413,39.095993h9.1512299l3.1927643,6.2311707l3.7234039-6.2311707h8.5074158l-6.8639984,10.652813
|
||||
l7.3564301,11.7053032h-8.9998474l-3.7234039-7.1998711l-4.3671417,7.1998711h-8.3561096l7.3086395-11.7053032
|
||||
L138.3317413,39.095993z"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M56.8743248,77.8356705H42.8329544c-1.4275665,0-2.589386,1.1615219-2.5898552,2.588913h-0.4812546
|
||||
c-0.0004692-1.4273911-1.1622887-2.588913-2.5898552-2.588913h-4.6130028c-1.4278011,0-2.5898552,1.162056-2.5898552,2.5898514
|
||||
v3.8786774l-2.0189228-4.8702927c-0.4023762-0.9709625-1.3409405-1.5982361-2.3917198-1.5982361h-5.0088043
|
||||
c-1.0507793,0-1.9893436,0.6272736-2.3917198,1.5982361l-5.6032133,13.5145721
|
||||
c-0.3324184,0.8005219-0.2432098,1.7090378,0.2385139,2.4297485c0.4812555,0.7211761,1.2860069,1.1517258,2.1532049,1.1517258
|
||||
h4.7458763c1.1160412,0,2.1029663-0.711319,2.4565125-1.7700806l0.1356888-0.407074h1.473814l0.1347523,0.4047241
|
||||
c0.353075,1.060173,1.3400002,1.7724304,2.4569817,1.7724304h4.8106689c0.2206726,0,0.4436932-0.0347443,0.6953545-0.1079865
|
||||
c0.248373,0.0723038,0.4789066,0.1079865,0.7028675,0.1079865h11.799427c0.5892448,0,1.152195-0.1990738,1.6104431-0.5653
|
||||
c0.4577789,0.3662262,1.0207291,0.5653,1.6099739,0.5653h4.5477371c1.427803,0,2.5898552-1.162056,2.5898552-2.589859v-7.562973
|
||||
h2.1579018c1.4277992,0,2.5898552-1.162056,2.5898552-2.589859v-3.3617401
|
||||
C59.46418,78.9977264,58.302124,77.8356705,56.8743248,77.8356705z"/>
|
||||
<path class="st0" d="M134.7516785,86.9945374l3.5993042-5.0698471c0.5624847-0.7934799,0.6347961-1.8231354,0.1887512-2.6870422
|
||||
c-0.4465027-0.8648529-1.3282623-1.4019775-2.3006287-1.4019775h-5.076889c-0.9211884,0-1.7804108,0.4958115-2.2419434,1.2935181
|
||||
l-0.2835846,0.4915848l-0.355423-0.5681152c-0.4760971-0.7620239-1.2968063-1.2169876-2.1954651-1.2169876h-5.0740662
|
||||
c-0.2244339,0-0.4502716,0.0333328-0.6878433,0.1004791c-0.2403946-0.0671463-0.4620056-0.1004791-0.6756363-0.1004791
|
||||
h-4.6144104c-1.2522049,0-2.2992249,0.893486-2.538681,2.0762024c-0.2389832-1.1827164-1.2860031-2.0762024-2.538208-2.0762024
|
||||
h-4.6125336c-0.5282059,0-1.0324631,0.1619797-1.470993,0.4704514c-0.4380569-0.3084717-0.9418488-0.4704514-1.4705276-0.4704514
|
||||
h-4.3463135c-1.3362503,0-2.4396133,1.0174408-2.5753021,2.3184738l-0.952179-1.276619
|
||||
c-0.4864197-0.6521606-1.2625351-1.0418549-2.075737-1.0418549h-4.2843399c-0.6620178,0-1.2935181,0.2587051-1.7747726,0.7178879
|
||||
c-0.4803162-0.4591827-1.1122894-0.7178879-1.7743073-0.7178879h-4.6158142c-1.427803,0-2.589859,1.162056-2.589859,2.5898514
|
||||
v7.6287155h-3.8021469v-7.6287155c0-1.4277954-1.162056-2.5898514-2.5898514-2.5898514h-4.614418
|
||||
c-1.4277954,0-2.5898514,1.162056-2.5898514,2.5898514V93.940094c0,1.427803,1.162056,2.589859,2.5898514,2.589859h11.7327576
|
||||
c0.3183365,0,0.6315002-0.0596313,0.9319916-0.1765366c0.3004913,0.1169052,0.6131897,0.1765366,0.9315262,0.1765366h4.6158142
|
||||
c0.6620178,0,1.2939911-0.2587051,1.7743073-0.7178955c0.4812546,0.4591904,1.1127548,0.7178955,1.7747726,0.7178955h4.2843399
|
||||
c1.3296738,0,2.4283447-1.0075836,2.5734253-2.2992249l0.9672012,1.2747345
|
||||
c0.4868927,0.6413651,1.2578354,1.0244904,2.0625916,1.0244904h4.3463135c1.3371811,0,2.4410172-1.0188522,2.5757675-2.3212891
|
||||
c0.6704712,0.7277451,1.4432983,1.2911682,2.2917175,1.6728821c0.8502884,0.4169312,1.8949661,0.6840897,3.0218048,0.7718887
|
||||
l0.2319412,0.0178452c0.8117981,0.0615005,1.5789871,0.1201935,2.3335037,0.1201935
|
||||
c1.6541061,0,2.9898834-0.1981354,4.0725861-0.6005096c0.5117722-0.1638641,1.016037-0.4061356,1.5334396-0.7380829
|
||||
c0.4845428,0.6704712,1.2606583,1.0770721,2.0870056,1.0770721h5.1388626c0.8601532,0,1.6616211-0.4258499,2.1442795-1.1381073
|
||||
l0.7573318-1.1183929l0.7545166,1.1165085c0.48172,0.7136688,1.2836609,1.1399918,2.1452179,1.1399918h5.1421509
|
||||
c0.9808197,0,1.86586-0.5437012,2.3100281-1.4193497c0.443222-0.8751831,0.3582306-1.9104614-0.2216187-2.7015991
|
||||
L134.7516785,86.9945374z"/>
|
||||
<path class="st0" d="M208.9085693,77.8356705h-5.9943237c-1.0061646,0-1.917038,0.5925293-2.3400726,1.4822617
|
||||
c-0.4225616-0.8902054-1.331543-1.4822617-2.3400726-1.4822617h-6.0666199c-1.4278107,0-2.589859,1.162056-2.589859,2.5898514
|
||||
v4.5585403l-2.3011017-5.5501556c-0.4023743-0.9709625-1.3409424-1.5982361-2.3917236-1.5982361h-5.0078583
|
||||
c-1.0507813,0-1.9893494,0.6272736-2.3917236,1.5982361l-2.8053589,6.7657394v-0.6991119
|
||||
c0-0.1788864-0.0187836-0.3573074-0.0554047-0.5324326c0.3784332-0.4620056,0.585495-1.0390396,0.585495-1.6395569v-2.9030228
|
||||
c0-1.4277954-1.1620636-2.5898514-2.589859-2.5898514H160.293396c-0.3629456,0-0.7211914,0.0812225-1.067688,0.24086
|
||||
c-0.3455658-0.1596375-0.7042694-0.24086-1.067215-0.24086h-14.0366669c-1.4278107,0-2.589859,1.162056-2.589859,2.5898514
|
||||
v3.3617401c0,1.427803,1.1620483,2.589859,2.589859,2.589859h2.0902863v7.562973
|
||||
c0,1.427803,1.1620483,2.589859,2.589859,2.589859h4.6129913c1.4278107,0,2.589859-1.162056,2.589859-2.589859v-7.562973
|
||||
h1.6987152v7.562973c0,1.427803,1.1620483,2.589859,2.589859,2.589859h12.5891418
|
||||
c0.2220917,0,0.4516754-0.0352173,0.6986542-0.1065826c0.2511902,0.0723038,0.4727936,0.1065826,0.6920624,0.1065826h4.7486877
|
||||
c1.1183929,0,2.1062622-0.7131958,2.4579315-1.7747726l0.1333313-0.4023819h1.4705353l0.1352234,0.4061356
|
||||
c0.3535461,1.059227,1.3409271,1.771019,2.4569702,1.771019h4.812088c0.2601013,0,0.5286713-0.0493011,0.8193054-0.1507187
|
||||
c0.2817078,0.1000061,0.5704651,0.1507187,0.8601532,0.1507187h3.756134c0.5164642,0,1.0240173-0.1568222,1.4503326-0.4446335
|
||||
c0.4263306,0.2878113,0.9338684,0.4446335,1.4498749,0.4446335h3.4307556c0.5160065,0,1.0230713-0.1568222,1.4494019-0.4441681
|
||||
c0.4258423,0.2878189,0.9329224,0.4441681,1.4494019,0.4441681h3.755188c1.4277954,0,2.589859-1.162056,2.589859-2.589859
|
||||
V80.4255219C211.4984283,78.9977264,210.3363647,77.8356705,208.9085693,77.8356705z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st2" d="M20.5494957,80.4256134L14.9462709,93.939949h4.7459002l0.7266064-2.1771622h5.2069492l0.7242508,2.1771622
|
||||
h4.8109646l-5.6026363-13.5143356H20.5494957z M21.4068203,88.7963791l1.6487007-4.8101578l1.6478157,4.8101578H21.4068203z"/>
|
||||
<polygon class="st2" points="37.1717606,80.4256134 32.5589333,80.4256134 32.5589333,93.939949 44.3586197,93.939949
|
||||
44.3586197,90.6439514 37.1717606,90.6439514 "/>
|
||||
<polygon class="st2" points="42.8327599,83.7874222 47.5786629,83.7874222 47.5786629,93.939949 52.1267204,93.939949
|
||||
52.1267204,83.7874222 56.8740921,83.7874222 56.8740921,80.4256134 42.8327599,80.4256134 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st2" points="71.0224304,80.4256134 66.4078369,80.4256134 66.4078369,93.939949 78.1403961,93.939949
|
||||
78.1403961,90.6439514 71.0224304,90.6439514 "/>
|
||||
<rect x="80.0041428" y="80.4256134" class="st2" width="4.6157718" height="13.5143366"/>
|
||||
<polygon class="st2" points="98.0564499,87.9391251 92.4532242,80.4256134 88.1692505,80.4256134 88.1692505,93.939949
|
||||
92.4532242,93.939949 92.4532242,86.555687 98.0564499,93.939949 102.4031296,93.939949 102.4031296,80.4256134
|
||||
98.0564499,80.4256134 "/>
|
||||
<path class="st2" d="M115.0338593,88.7336655c0,0.7249908-0.1987228,1.3160934-0.6577072,1.709938
|
||||
c-0.4634018,0.3968658-1.0551682,0.5941925-1.8450775,0.5941925c-0.7922592,0-1.4529114-0.1973267-1.9127808-0.5941925
|
||||
c-0.4634094-0.3938446-0.6612473-0.9849472-0.6612473-1.709938v-8.3080521h-4.6128311v8.1077042
|
||||
c0,0.6569061,0.132782,1.3833618,0.4610443,2.2391434c0.1330795,0.5292053,0.4610519,1.0575943,0.9895172,1.5837097
|
||||
c0.4619293,0.5269241,0.9880447,0.9230499,1.5821609,1.1838226c0.5270004,0.2653427,1.2518387,0.4642181,2.1115189,0.5314865
|
||||
c0.8549652,0.0649948,1.646637,0.129982,2.3691254,0.129982c1.2533035,0,2.3735352-0.129982,3.2332153-0.4611206
|
||||
c0.6577148-0.2003479,1.2488937-0.5941925,1.8406525-1.1180267c0.5941238-0.5314789,1.0575256-1.1233978,1.3183746-1.8498535
|
||||
c0.2655563-0.722702,0.3986359-1.4491653,0.3986359-2.2391434v-8.1077042h-4.6146011V88.7336655z"/>
|
||||
<polygon class="st2" points="136.2389221,80.4256134 131.1621094,80.4256134 128.7232056,84.6446686 126.0855789,80.4256134
|
||||
121.0117035,80.4256134 125.6895981,86.9510803 120.5506592,93.939949 125.6895981,93.939949 128.5924835,89.6520844
|
||||
131.4909668,93.939949 136.632843,93.939949 131.55867,87.0183563 "/>
|
||||
</g>
|
||||
<polygon class="st2" points="144.121933,83.7874222 148.8021851,83.7874222 148.8021851,93.939949 153.4150085,93.939949
|
||||
153.4150085,83.7874222 158.1585541,83.7874222 158.1585541,80.4256134 144.121933,80.4256134 "/>
|
||||
<polygon class="st2" points="164.9077606,88.2702637 172.09021,88.2702637 172.09021,85.5003738 164.9077606,85.5003738
|
||||
164.9077606,83.3285828 172.6201477,83.3285828 172.6201477,80.4256134 160.293457,80.4256134 160.293457,93.939949
|
||||
172.8827515,93.939949 172.8827515,90.9070053 164.9077606,90.9070053 "/>
|
||||
<path class="st2" d="M179.876709,80.4256134l-5.6032257,13.5143356h4.7482452l0.7219086-2.1771622h5.2069397l0.7251434,2.1771622
|
||||
h4.8124237l-5.6032257-13.5143356H179.876709z M180.733139,88.7963791l1.6495819-4.8101578l1.6457672,4.8101578H180.733139z"/>
|
||||
<polygon class="st2" points="202.9142761,80.4256134 200.5380859,88.6663971 198.234024,80.4256134 192.1674042,80.4256134
|
||||
192.1674042,93.939949 195.9237823,93.939949 195.9237823,83.6550827 198.8237305,93.939949 202.2547913,93.939949
|
||||
205.1532593,83.6550827 205.1532593,93.939949 208.9087677,93.939949 208.9087677,80.4256134 "/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st2" d="M184.3468018,66.8016739h10.7164917l-1.0384216-2.0768509c0,0,0.3323059-1.0799561,0.3323059-1.204567
|
||||
s0.0830688-1.9937706,0.0830688-1.9937706l-1.4953308-3.0737267c0,0,0.2492218-1.5368652,0.2492218-1.6614761
|
||||
s0.1302032-1.4689789,0.1302032-1.4689789l-1.2516785-1.2724533c0,0,2.076828-0.8307381,2.2014465-0.955349
|
||||
c0.1246033-0.1246109,0.8722687-0.4984436,0.8722687-0.4984436l-1.5784149-2.450676c0,0-2.2014465-9.262722-2.2014465-9.0965767
|
||||
c0,0.1661491-1.9937592,7.3935661-2.0353088,7.5597115c-0.0415344,0.1661453-2.6998901,8.7227478-2.6998901,8.8473549
|
||||
c0,0.1246109-0.3738403,4.9428864-0.3738403,4.9428864l0.1246185,1.4953346l0.166153,1.3707161L184.3468018,66.8016739z"/>
|
||||
<g>
|
||||
<path d="M217.7933044,66.8032532c-0.0118866-0.0402832-0.1311951-0.4059448-0.6125336-0.9156876
|
||||
c-0.1967621-0.2091675-0.5996094-0.4834061-1.1573944-0.8635254c-1.2596436-0.8588715-3.1638336-2.1567421-3.4907684-3.3735237
|
||||
c-0.6951447-1.8489342-0.2349854-4.1802406,0.2097015-6.435112c0.1709442-0.8650742,0.332077-1.6826324,0.4322815-2.474369
|
||||
c1.2813416-8.6760368,0.7891541-13.3959808-0.7979431-20.6904831l-0.328476-1.5989666
|
||||
c-0.7101288-3.4757862-1.3236847-6.4779778-2.4671173-9.6485348c-0.2685699-0.7478371-0.7049866-1.5529995-1.0902557-2.262619
|
||||
c-0.1105347-0.2045174-0.2169189-0.400774-0.3134918-0.5846348c-0.390976-0.7447357-0.5113068-1.2343426-0.663147-1.8546124
|
||||
l-0.0206604-0.0836678c-0.1828308-0.8573256-0.2380829-1.7131023-0.2917938-2.5404739
|
||||
c-0.070755-1.1031628-0.1384125-2.144865-0.478775-3.1922493c-0.3651123-1.1770163-1.4724121-2.7134891-2.4717712-3.4282722
|
||||
c-0.4896088-0.3387985-1.0876617-0.518527-1.6661072-0.6920581c-0.4870148-0.1461587-0.9477081-0.2845707-1.316452-0.5128465
|
||||
l-1.125885-0.6021943c-1.5344086-0.8165259-3.1204681-1.6614568-4.7235718-2.6732054
|
||||
c-0.2778473-0.1905742-1.2203979-0.5257578-1.8019409-0.1854095l-0.4023132,0.2355065l0.3811493,0.2685599
|
||||
c2.2683105,1.5979323,3.5935364,3.4990273,4.1693878,5.972362c0.0351257,0.1786957,0.0702362,0.3480949,0.1043396,0.5087147
|
||||
c0.2489166,1.1852798,0.3026276,1.614459-0.3295135,1.9971571c-1.3655243,0.8258219-2.7129669,1.8163948-4.1822968,4.8299475
|
||||
c-0.5562439,1.1413822-0.965271,2.0203991-1.3526154,2.851902c-0.636795,1.3686237-1.1904602,2.5580349-2.2249146,4.5531254
|
||||
c-3.9938049-1.7947044-7.3781738-1.9103928-10.3333588-0.3615227c-6.8007813,3.2552567-8.4921875,11.1839771-8.7132263,17.2622128
|
||||
c-0.1187897,3.1700401,0.4611969,6.665966,1.0215454,10.0462074c0.641449,3.8688164,1.3050995,7.8693275,0.8831482,11.2640305
|
||||
c-0.3883667,2.5533829-1.8117371,4.0185852-4.4141846,4.6032181c-0.3785553,0-0.6352539,0.1001968-0.7628174,0.2964478
|
||||
c-0.0542297,0.0831604-0.1286011,0.2582397-0.0237579,0.4932251l0.0795288,0.1776581h15.6095428
|
||||
c0.6729431,0,0.9766235-0.3191681,1.3278198-0.6884384c0.1518555-0.1590652,0.3269196-0.3439636,0.5861969-0.5582962
|
||||
c0.6734619-0.6533203,0.5236816-1.4171677,0.3346558-2.3845024c-0.114151-0.5835991-0.2432556-1.2451897-0.2432556-2.0606804
|
||||
c0-2.7945747,0.9647522-5.5540314,1.8974762-8.2220688l0.3548126-1.0194931
|
||||
c1.1501617-3.3203316,1.9527435-6.0384712,2.5379028-8.6145821c0.0469971,0.2158813,0.0944977,0.4369278,0.1430511,0.6621017
|
||||
c0.231369,1.078373,0.4942474,2.3003235,0.8046417,3.4732056c0.1570129,0.5913467,0.350174,1.2803078,0.6125336,1.9971581
|
||||
c-0.6249237,0.2561646-1.2865143,0.519043-1.8411865,0.731823c-0.287674,0.1131096-0.4157562,0.387867-0.3134918,0.6977425
|
||||
c0.0676422,0.1745644,0.2246552,0.3140068,0.3894043,0.365139c0.0666199,0.0340881,0.122406,0.059906,0.1740417,0.0836678
|
||||
c0.1528931,0.070755,0.2375793,0.1094894,0.489624,0.336216c0.3248444,0.2918015,0.8051605,1.0918007,0.8051605,1.8830185
|
||||
c0,0.4560356-0.1038208,0.7788239-0.20401,1.0907631c-0.130661,0.4059448-0.265976,0.8253098-0.1378937,1.4305992
|
||||
c0.1409912,0.7519722,0.5123291,1.501358,0.905365,2.2951584c0.4927063,0.995739,1.0024414,2.0255623,1.0024414,3.0280151
|
||||
c0,0.4198799-0.3109131,0.8702354-0.619751,1.1300163c-0.1704407,0.1430588-0.3563538,0.2649422-0.5422821,0.3873482
|
||||
c-0.212265,0.1394463-0.4322815,0.2840538-0.6357574,0.4606819c-0.4348755,0.377533-0.6843262,0.9306641-0.7411346,1.6433792
|
||||
c-0.0056763,0.0759201-0.0165253,0.1559677-0.0273743,0.2375717c-0.0557709,0.4209137-0.1487427,1.125885,0.5732727,1.2983856
|
||||
c0.4989014,0.1218796,0.8031158-0.3098755,1.0267334-0.6228561c0.0738525-0.1038055,0.2050323-0.2881851,0.2773438-0.333107
|
||||
c0.22052,0.0883102,0.351181,0.3052216,0.4999237,0.5531235c0.1926422,0.3202057,0.4322815,0.7189102,0.9363556,0.7189102
|
||||
h24.4100342L217.7933044,66.8032532z"/>
|
||||
<path class="st0" d="M194.9966888,38.5344658c-0.1146698-0.7964439-0.9970245-0.542347-0.7979431,0.0836067
|
||||
c0.4245453,1.9584198,1.0504761,4.6263962,0.4245453,6.298893
|
||||
C195.5304413,42.9569931,195.6148834,41.0582848,194.9966888,38.5344658z"/>
|
||||
<path class="st0" d="M200.2460022,7.8746614c-1.5274506-2.3263369-3.4675293-4.1429906-5.3639832-5.0278177
|
||||
c1.5036926,0.8848276,3.4548798,3.8626807,4.4560394,5.3981848C199.8741455,8.9776945,200.5001068,8.357295,200.2460022,7.8746614
|
||||
z"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M194.6893768,48.9474983c0.2262115-0.0038757,0.58078,0.0217552,0.7979431,0.0876656
|
||||
c0.2881927,0.0872192,0.557785,0.2287292,0.7994843,0.4071655c0.5017395,0.370369,0.8849487,0.8952866,1.11763,1.4715958
|
||||
c0.108963,0.270237,0.1817932,0.5515175,0.2230988,0.8398933c0.2122803,1.4921875-0.2016754,3.0194931,0.5941925,4.5467987
|
||||
c0.8320313,1.4230461,1.7727509,2.6776581,2.3318329,3.8874092c0.7607422,1.5282707,1.4241333,3.2951546,0.9340057,5.5588608
|
||||
c-0.0046387,0.0262222-0.0167847,0.0560379-0.0224609,0.0827103c1.2929688-0.1495819,2.5211182-0.4603653,3.5744324-1.0690155
|
||||
c3.1537781-1.874752,4.6845703-5.6450539,2.6148376-10.1344604c-0.6499481-1.4479027-1.3675842-2.2984467-2.272171-3.8299484
|
||||
c-1.7019958-2.4163971-1.3611298-6.7584839-0.5639648-9.5952797c0.6481476-2.6664276,2.1861877-5.7629356,2.2985077-5.7629356
|
||||
c0.1670685-0.3703041-0.2587433-0.6251755-0.4803009-0.2540359c-0.8888397,1.419239-1.4827728,2.7237587-1.9909668,4.1389885
|
||||
c-0.8242798,2.3032265-1.1669464,4.7147789-1.0504913,7.1287842c0.0550079,1.4446754,0.0410614,3.5284004-0.1712036,4.2027054
|
||||
s-0.7021179,1.1333122-1.3180084,0.6309242c-0.8552551-0.6976089-1.7830811-2.0375671-2.335434-3.4676514
|
||||
c-0.9125824-2.3635216-1.127182-5.1390533-1.0517883-7.3853378c0-3.7145195,1.413559-7.6934052,1.9194336-11.4087009
|
||||
c0.2881927-1.1652012-0.25177-4.4886322-0.171463-6.4725533c0.0490723-1.3293743,0.0046539-3.1167183-0.0748901-4.6101322
|
||||
c0.0258331-0.5437679,0.1694031-1.5550652,0.2463531-2.4612608c0.1208496-1.3021288,1.0799255-1.6486101,1.9326019-1.080761
|
||||
c0.3982086,0.2899284,0.5138855,0.6554537,1.0541077,0.9971581c1.5909576,0.8513231,2.7436981-0.5565519,2.497345-2.0436373
|
||||
c-0.2465973-1.4894114-1.2043915-2.766427-2.4746246-2.1139431c-0.6832733,0.3509359-0.5861816,1.3501606-1.2431183,1.856679
|
||||
c-0.9120789,0.5448036-1.8770905,0.143384-2.696701-0.4276295c-0.6848297-0.4754677-0.6848297-0.9095535-1.6229858-0.4754677
|
||||
c-1.5238342,0.6880569-3.1127167,2.635891-4.0307312,4.7936049c-1.0711365,2.5176849-2.2698517,4.7100639-3.4587402,7.1837215
|
||||
c1.6444092,1.0210438,3.0628815,2.1821785,4.5099945,3.4875355c2.3852844,2.132019,3.693222,5.5104523,2.9546814,8.6889496
|
||||
c-0.2548676,1.3921242-1.2650757,2.7194939-1.6741028,3.8029022c-0.4090424,1.0834084-0.3561096,2.2922478-0.5812836,3.1249123
|
||||
c-0.2249146,0.8326645-0.382431,1.2377663-0.8325348,1.8453865c-0.4501038,0.6076126-0.8883057,0.168232-0.6011658-0.2021332
|
||||
c0.2525635-0.6227875,0.2411957-1.6882477,0-3.0614548c-0.2411804-1.3732071-1.5602264-3.6093559-2.8160095-5.4275627
|
||||
c-0.4508667-0.6801796-0.6793976-1.2440262-0.8506165-1.8692017c-0.0048981-0.0169144-0.0064392-0.0369263-0.0110931-0.0541649
|
||||
c0.2109833,0.1408005,0.5458984-0.5922508,0.611496-0.9143295c0.0542145-0.3671417,0.4286652-1.159584,1.3014832-1.2200737
|
||||
c0.7932739,0,0.6306,0.6530647,1.8468628,0.9094849c0.7359467,0.1983261,1.1620331-0.0565529,1.3363495-0.5407333
|
||||
c0.4253082-1.0504818-0.2587585-3.1793365-1.0535889-3.8005123c-0.8482819-0.7988377-2.7468109-0.9087124-2.7468109,0.7342815
|
||||
c0.0787659,0.4284687,0,0.6506767,0,0.6506767c-0.2835236,0.1457062-0.6610565-0.1123314-0.9128418-0.6506767
|
||||
c-0.6267242-0.9883785-2.812912-2.5230446-4.0839081-2.4402161c0.0144501,0.0040646,0.0260773,0.0098095,0.0405426,0.0140057
|
||||
c-0.8707581-0.1128445-1.8029785,0.0057468-2.6848297,0.3570709c-2.2130432,0.8761101-3.6315002,2.7826939-4.2329254,4.8828831
|
||||
c-0.9053497,3.0606194,0.3695374,7.0387306,0,10.4705505c-0.2510071,2.5285988-1.0995483,6.8754616-2.8637695,8.8609314
|
||||
c-0.4402924,0.6376381-1.0029755,0.750164-1.2759247,0.2548752s-0.4552612-1.6764984-0.4785004-2.6996117
|
||||
c-0.0361481-1.5928307,0.1384125-3.295475-0.0351105-4.2260742c-0.0852203-0.3711433-0.3687744-0.2301483-0.3687744,0
|
||||
c-0.4810791,2.4418297-0.1107788,6.8078041,0.8823853,10.1041832c1.0241547,3.237442,1.930542,5.620327,1.930542,7.6345329
|
||||
c-0.0591431,1.9081917-2.1296387,2.9252357-1.7084656,2.9833336c2.0196228,0.0302811,4.4315033,0.1131058,6.6169128,0.0302811
|
||||
c0.6241302,0,0.5430603-0.1131058,1.1690063-0.7438354c0.616394-0.7335052,0.225174-1.5307274,0.111557-2.1814003
|
||||
c-0.5417633-3.9757233,1.4174194-8.6276245,2.5786896-12.2369804c1.1142578-3.3203316,2.1598511-6.7823639,2.8957977-10.8727417
|
||||
c0.0271149-0.1664276,0.1998749-0.2540359,0.34552-0.2038727c0.107666,0.0374451,0.1673279,0.179985,0.1967773,0.2922516
|
||||
c0.4836578,1.8772087,1.105484,4.6805611,1.7314301,7.1216164c0.2003937,0.7774086,0.4498444,1.6651382,0.7695313,2.5287895
|
||||
c0.3904419-0.2019997,0.7669373-0.4209175,1.2049103-0.5260124
|
||||
C194.1406555,48.9995308,194.3570404,48.9531746,194.6893768,48.9474983z"/>
|
||||
<g>
|
||||
<path class="st3" d="M191.9888,29.672699c0.0787659,0.4284687,0,0.6506767,0,0.6506767
|
||||
c-0.2835236,0.1457062-0.6610565-0.1123314-0.9128418-0.6506767c-0.6267242-0.9883785-2.812912-2.5230446-4.0839081-2.4402161
|
||||
c1.9256287,0.5423489,2.7220001,1.7059345,3.173645,3.236599c0.3718567,1.0226574,0.2541046,2.1870193,0.3718567,3.5512543
|
||||
c0.111557,1.1635857,0.6794128,0,0.7669525-0.4300842c0.0542145-0.3671417,0.4286652-1.159584,1.3014832-1.2200737
|
||||
c0.7932739,0,0.6306,0.6530647,1.8468628,0.9094849c0.7359467,0.1983261,1.1620331-0.0565529,1.3363495-0.5407333
|
||||
c0.4253082-1.0504818-0.2587585-3.1793365-1.0535889-3.8005123C193.8873291,28.1395798,191.9888,28.029705,191.9888,29.672699z
|
||||
"/>
|
||||
<path class="st3" d="M194.1987457,38.6180725c0.4245453,1.9584198,1.0504761,4.6263962,0.4245453,6.298893
|
||||
c0.9071503-1.9599724,0.9915924-3.8586807,0.3733978-6.3824997
|
||||
C194.882019,37.7380219,193.9996643,37.9921188,194.1987457,38.6180725z"/>
|
||||
<path class="st3" d="M200.2460022,7.8746614c-1.5274506-2.3263369-3.4675293-4.1429906-5.3639832-5.0278177
|
||||
c1.5036926,0.8848276,3.4548798,3.8626807,4.4560394,5.3981848
|
||||
C199.8741455,8.9776945,200.5001068,8.357295,200.2460022,7.8746614z"/>
|
||||
<path class="st3" d="M206.1220398,13.3524475c-0.2465973-1.4894114-1.2043915-2.766427-2.4746246-2.1139431
|
||||
c-0.6832733,0.3509359-0.5861816,1.3501606-1.2431183,1.856679c-0.9120789,0.5448036-1.8770905,0.143384-2.696701-0.4276295
|
||||
c-0.6848297-0.4754677-0.6848297-0.9095535-1.6229858-0.4754677c-0.3444824,0.1555843-0.691803,0.3831501-1.0344696,0.6554546
|
||||
c0.6171722,0.7480307,1.8210449,0.4854088,2.42659,1.6931543c0.5430603,1.0823746,0.8242645,2.4737225,0.9094849,3.5544186
|
||||
c0-0.0081348,0.0103302-0.0126534,0.0113678-0.020401c-0.0023346-0.0433826-0.0036163-0.0908337-0.0059509-0.1337643
|
||||
c0.0258331-0.5437679,0.1694031-1.5550652,0.2463531-2.4612608c0.1208496-1.3021288,1.0799255-1.6486101,1.9326019-1.080761
|
||||
c0.3982086,0.2899284,0.5138855,0.6554537,1.0541077,0.9971581
|
||||
C205.2156525,16.2474079,206.3683929,14.8395329,206.1220398,13.3524475z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st0" d="M197.8086853,62.3406677c-1.0478973-1.7299156-1.87883-4.827343-1.87883-6.7657013
|
||||
c0-1.1123238-0.7958679-0.7344437-1.387085-0.7344437c-0.6272278,0-1.2516022,0.0350571-1.7376556-0.1684914
|
||||
c-0.382782-0.1733856-0.3439331-0.4217796,0.0748444-0.4899178c1.2449799-0.3107681,1.9773712,0.4899178,2.842392-1.2097549
|
||||
c0.2747803-0.561058,0.3486786-1.189312,0.1724548-1.7396278c-0.2008667-0.6584892-0.8981934-1.0052643-1.6940765-1.0412636
|
||||
c-0.5608978,0-1.5661621,0.1383247-1.6372223,0.8328209c-0.0410461,0.3827744,0.1705627,0.9019089,0.7276611,0.9749413
|
||||
c0,0.2386856,0.0701141,0.5503998-0.2027588,0.6565132c-0.4235229,0.1412506-1.2857056-0.2064667-1.1540222,0
|
||||
c0.5880737,0.8318787,0.3126831,1.8069,0.0729675,2.6747017c-0.2706604,0.6604614,0.0761108,1.6646957,0.5561523,2.6348228
|
||||
c0.4481659,0.9418602,1.0043335,1.8447952,1.1777191,2.4263039c0.3173828,1.0509758,0.0672607,2.1584854,0.2823334,3.0263634
|
||||
c0.0644226,0.4539146,0.5192108,0.6282539,0.6218567,1.1484108c0.1408539,0.3467636,0.6600647,0.2746811,0.7974396,0.2746811
|
||||
c0.263092,0,0.5169983-0.2793427,0.680603-0.452652c0.3960419-0.419487,0.9427338-0.4178314,1.4891052-0.3925667
|
||||
c0.5179443,0.0240059,1.0962067,0.1494713,1.4411011,0.5705376c0.2832794,0,0.4547882,0,0.4547882-0.1744156
|
||||
C199.5084534,64.0091553,198.4343262,63.3867455,197.8086853,62.3406677z M194.2601166,50.7846603
|
||||
c0.2826538,0,0.736496,0.4480705,0.5261536,0.6584053c-0.1408539,0.1061974-0.2434998-0.2103348-0.698288-0.2103348
|
||||
c-0.2794952,0-0.5883636,0.2103348-0.5883636,0C193.4996185,50.991127,193.8084869,50.7846603,194.2601166,50.7846603z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st3" d="M192.5630341,66.4706802c-0.1486969-0.2479019-0.2792969-0.4648209-0.4999695-0.5531769
|
||||
c-0.0722046,0.0449142-0.2035217,0.2293167-0.2771454,0.3331528c-0.2237701,0.3129654-0.5278625,0.7447205-1.026886,0.6228256
|
||||
c-0.7220764-0.1724777-0.6289063-0.8774567-0.5731354-1.298378c0.0107269-0.0816193,0.0216827-0.1616287,0.0274048-0.237587
|
||||
c0.0567169-0.7127228,0.3062286-1.2658463,0.7411499-1.6433868c0.2035217-0.1765862,0.4234619-0.3212395,0.6355743-0.4606514
|
||||
c0.1861115-0.1224289,0.371994-0.2442665,0.5423889-0.3873749c0.3088379-0.2597542,0.6198425-0.7101021,0.6198425-1.1300049
|
||||
c0-1.002449-0.5099792-2.0322418-1.0025787-3.0280228c-0.3929749-0.7938042-0.7642517-1.5431709-0.9053345-2.2951622
|
||||
c-0.1282043-0.6052437,0.0071564-1.0246124,0.1377411-1.4305725c0.1003265-0.3119469,0.20401-0.6347351,0.20401-1.0908012
|
||||
c0-0.7911873-0.4801941-1.5911942-0.8050232-1.8830032c-0.2521362-0.2266922-0.336731-0.2654152-0.4894867-0.3361931
|
||||
c-0.051712-0.023777-0.1074829-0.0495682-0.1742096-0.0836525c-0.1646729-0.0511742-0.3217163-0.1905823-0.389389-0.3651466
|
||||
c-0.1022491-0.309864,0.0259705-0.5846329,0.3133698-0.697773c0.5547791-0.2127495,1.2163391-0.475666,1.8411865-0.7317886
|
||||
c-0.262146-0.7168961-0.4554138-1.4058495-0.6124573-1.9971542c-0.3102875-1.1729012-0.5731354-2.3948326-0.8045349-3.4732437
|
||||
c-0.0486145-0.2251434-0.0960388-0.4461746-0.1429749-0.6620827c-0.5852966,2.5761261-1.3879242,5.2942276-2.5380096,8.6145973
|
||||
l-0.3548431,1.0194893c-0.9327393,2.6679955-1.8974152,5.4275055-1.8974152,8.2220421
|
||||
c0,0.8154945,0.1291504,1.4771042,0.2433014,2.0606651c0.1889954,0.9673615,0.3386536,1.7312012-0.3348083,2.3845215
|
||||
c-0.2592926,0.2143631-0.4342041,0.399231-0.5860138,0.5583038c-0.3512726,0.3692627-0.6548767,0.6884155-1.3278503,0.6884155
|
||||
h3.1204224h2.2958679h4.9563751C192.9953308,67.189537,192.7558289,66.7908478,192.5630341,66.4706802z"/>
|
||||
<path class="st0" d="M219.4322205,66.6828918l-0.1151123-0.3818283c-0.0891113-0.2998581-0.3398132-0.902832-1.0187531-1.6218185
|
||||
c-0.3281555-0.3483505-0.7676086-0.648201-1.4487-1.1126099c-0.6303253-0.4299088-2.5477753-1.737278-2.7612915-2.3524742
|
||||
c-0.0145416-0.055645-0.0326538-0.1103401-0.0529022-0.1645546c-0.5147552-1.3690872-0.1060486-3.43964,0.2895508-5.4418602
|
||||
c0.1677704-0.8505249,0.3412628-1.7302475,0.4451599-2.5509872c1.3171387-8.9211807,0.8126373-13.7676277-0.8128815-21.2362537
|
||||
l-0.3267212-1.5916119c-0.7213593-3.5310955-1.3440704-6.5804462-2.5270386-9.8597679
|
||||
c-0.3148041-0.8783493-0.8047791-1.7797565-1.1946411-2.4972458l-0.3040924-0.5671787
|
||||
c-0.2890625-0.5501976-0.366745-0.8674469-0.5161591-1.4746599c-0.1551514-0.7279167-0.2037659-1.4799032-0.2550049-2.2752028
|
||||
c-0.074585-1.159317-0.1517944-2.3581934-0.5535889-3.594306c-0.4770966-1.5378723-1.82164-3.4024076-3.1499786-4.3523693
|
||||
c-0.7206573-0.4983063-1.5197144-0.7382836-2.1617126-0.9306593c-0.3531647-0.1060476-0.7185059-0.2159085-0.8862762-0.319633
|
||||
c-0.0321655-0.0197797-0.0645752-0.0386658-0.097702-0.0560622l-1.1252899-0.60203
|
||||
c-1.4968414-0.7967885-3.0451202-1.6203868-4.5362396-2.5599825c-0.7354126-0.5044421-2.3897705-1.0202039-3.7021332-0.2531444
|
||||
l-0.4008331,0.2347947c-0.5219116,0.30593-0.8529205,0.8566043-0.8779449,1.4609576
|
||||
c-0.0252533,0.6048896,0.2588043,1.1810033,0.7540283,1.5289354l0.3784332,0.2668476
|
||||
c1.9078979,1.3440671,2.9712372,2.8554869,3.4349823,4.8435855l0.1079559,0.5261288
|
||||
c0.0178833,0.0862675,0.0471954,0.2244272,0.0736389,0.3621111c-1.4115143,0.901823-2.9040375,2.1710014-4.4277954,5.2966719
|
||||
c-0.5540771,1.1361418-0.9627686,2.0140142-1.3621674,2.8710966c-0.4563751,0.9805832-0.8674469,1.8640575-1.4486847,3.0266533
|
||||
c-3.8479919-1.386488-7.2172089-1.2799625-10.221344,0.2960987c-5.9996796,2.8720493-9.3512878,9.3530006-9.6925354,18.7408524
|
||||
c-0.1248779,3.3406906,0.4677887,6.913723,1.0404663,10.3689766c0.6186523,3.7305603,1.258255,7.5874252,0.8764954,10.6664467
|
||||
c-0.2688141,1.7678986-1.0804901,2.6433868-2.8783112,3.083725c-1.1743774,0.0759048-1.7615814,0.7095108-2.0130005,1.0956268
|
||||
c-0.4217987,0.6477356-0.480423,1.4662018-0.1570435,2.1879807l0.079361,0.1772537
|
||||
c0.2862091,0.6406937,0.9220123,1.0531998,1.6240692,1.0531998h10.1906128h5.3653107h10.3369141h13.4566345h10.8688202
|
||||
c0.5624237,0,1.0924072-0.2658844,1.4279633-0.7175446C219.4925232,67.8053894,219.5942841,67.2217712,219.4322205,66.6828918z
|
||||
M206.8599548,67.189537h-13.3603516h-4.9563751h-2.2958679h-3.1204224h-5.4258423h-10.1839294l-0.079361-0.1776581
|
||||
c-0.1048431-0.2349701-0.0305023-0.4100723,0.0235901-0.493187c0.1277466-0.1962433,0.3843994-0.2964554,0.7630768-0.2964554
|
||||
c2.6023407-0.5846252,4.0257568-2.0498199,4.4139709-4.6032486c0.4220428-3.3946609-0.2416534-7.3951645-0.8831787-11.2640076
|
||||
c-0.5602722-3.3802452-1.1400757-6.8761864-1.0213928-10.0461807c0.2211456-6.07827,1.9124298-14.0069523,8.7130737-17.2622643
|
||||
c2.9552765-1.5488338,6.3397522-1.4331341,10.3336029,0.3615742c1.0342407-1.9951267,1.588089-3.1845303,2.2248535-4.5531425
|
||||
c0.3872528-0.831522,0.7964172-1.7105274,1.3526459-2.8519135c1.469162-3.013546,2.8168182-4.0041389,4.1823273-4.8299417
|
||||
c0.6319885-0.3827257,0.5783691-0.811862,0.3293457-1.9971542c-0.0340881-0.1606207-0.0691071-0.3299999-0.1043854-0.5087328
|
||||
c-0.5757599-2.4732971-1.9009857-4.3744125-4.1692352-5.9723387l-0.3812866-0.2685752l0.4024963-0.2355096
|
||||
c0.5814972-0.3403656,1.5240021-0.0051832,1.8018646,0.185405c1.6031189,1.011744,3.1890717,1.8566711,4.7235413,2.6732395
|
||||
l1.1257782,0.602149c0.3688965,0.2283006,0.8295593,0.3666987,1.3164215,0.5128427
|
||||
c0.5786285,0.1735487,1.1767731,0.3532939,1.6662598,0.6921105c0.9994812,0.7147508,2.1066742,2.2512527,2.4717407,3.4282641
|
||||
c0.3403168,1.0473719,0.4080048,2.0890827,0.478775,3.1922188c0.0536194,0.8274117,0.108902,1.6831808,0.2919312,2.5405016
|
||||
l0.0204926,0.0836468c0.1518097,0.6202583,0.2721405,1.1098671,0.6632233,1.8546448
|
||||
c0.0965118,0.1838551,0.2028046,0.3801041,0.3133698,0.5846329c0.3853455,0.709568,0.821701,1.5147552,1.090271,2.2625732
|
||||
c1.1436462,3.1705894,1.7570648,6.1727562,2.4672089,9.6485634l0.3283997,1.5989399
|
||||
c1.5871429,7.2945404,2.0792542,12.0144424,0.7978668,20.6905251c-0.1000977,0.7917252-0.2612,1.6092491-0.4320679,2.4743729
|
||||
c-0.4446869,2.2548256-0.9048615,4.5861511-0.2097168,6.4350777c0.3267212,1.2168083,2.2310638,2.5146446,3.4907684,3.3735085
|
||||
c0.5576477,0.3801651,0.9606323,0.6544037,1.1574707,0.8635788c0.4811401,0.5097427,0.6005402,0.8753738,0.6124573,0.9156418
|
||||
l0.1160583,0.3862991H206.8599548z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 39 KiB |
3
src/assets/GhIcon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.0001 0.296387C5.3735 0.296387 0 5.66889 0 12.2965C0 17.5984 3.43839 22.0966 8.2064 23.6833C8.80613 23.7944 9.0263 23.423 9.0263 23.1061C9.0263 22.8199 9.01518 21.8746 9.01001 20.8719C5.67157 21.5978 4.96712 19.456 4.96712 19.456C4.42125 18.069 3.63473 17.7002 3.63473 17.7002C2.54596 16.9554 3.7168 16.9707 3.7168 16.9707C4.92181 17.0554 5.55632 18.2073 5.55632 18.2073C6.6266 20.0419 8.36359 19.5115 9.04836 19.2049C9.15607 18.4293 9.46706 17.8999 9.81024 17.6002C7.14486 17.2968 4.34295 16.2678 4.34295 11.6697C4.34295 10.3596 4.81172 9.28911 5.57937 8.44874C5.45477 8.14649 5.04402 6.92597 5.69562 5.27305C5.69562 5.27305 6.70331 4.95053 8.9965 6.5031C9.95372 6.23722 10.9803 6.10388 12.0001 6.09931C13.0199 6.10388 14.0473 6.23722 15.0063 6.5031C17.2967 4.95053 18.303 5.27305 18.303 5.27305C18.9562 6.92597 18.5452 8.14649 18.4206 8.44874C19.1901 9.28911 19.6557 10.3596 19.6557 11.6697C19.6557 16.2788 16.8484 17.2936 14.1762 17.5907C14.6067 17.9631 14.9902 18.6934 14.9902 19.8129C14.9902 21.4186 14.9763 22.7108 14.9763 23.1061C14.9763 23.4254 15.1923 23.7996 15.8006 23.6818C20.566 22.0932 24 17.5967 24 12.2965C24 5.66889 18.6273 0.296387 12.0001 0.296387ZM4.49443 17.3908C4.468 17.4504 4.3742 17.4683 4.28876 17.4273C4.20172 17.3882 4.15283 17.3069 4.18105 17.2471C4.20688 17.1857 4.30088 17.1686 4.38772 17.2098C4.47495 17.2489 4.52463 17.331 4.49443 17.3908ZM5.0847 17.9175C5.02747 17.9705 4.91559 17.9459 4.83968 17.862C4.76119 17.7784 4.74648 17.6665 4.80451 17.6126C4.86353 17.5596 4.97203 17.5844 5.05072 17.6681C5.12921 17.7527 5.14451 17.8638 5.0847 17.9175ZM5.48965 18.5914C5.41612 18.6424 5.2959 18.5945 5.22158 18.4878C5.14805 18.3811 5.14805 18.2531 5.22317 18.2019C5.29769 18.1506 5.41612 18.1967 5.49144 18.3026C5.56476 18.4111 5.56476 18.5391 5.48965 18.5914ZM6.1745 19.3718C6.10873 19.4443 5.96863 19.4249 5.86609 19.3259C5.76117 19.2291 5.73196 19.0918 5.79793 19.0193C5.8645 18.9466 6.00539 18.967 6.10873 19.0652C6.21285 19.1618 6.24465 19.3001 6.1745 19.3718ZM7.05961 19.6353C7.0306 19.7293 6.89567 19.772 6.75975 19.7321C6.62402 19.6909 6.5352 19.5808 6.56262 19.4858C6.59084 19.3913 6.72636 19.3467 6.86328 19.3895C6.9988 19.4304 7.08783 19.5397 7.05961 19.6353ZM8.0669 19.747C8.07028 19.846 7.95502 19.9281 7.81235 19.9299C7.66887 19.933 7.55282 19.853 7.55123 19.7556C7.55123 19.6556 7.6639 19.5744 7.80738 19.572C7.95006 19.5692 8.0669 19.6487 8.0669 19.747ZM9.05645 19.7091C9.07354 19.8057 8.97438 19.9048 8.8327 19.9313C8.6934 19.9567 8.56443 19.8971 8.54674 19.8013C8.52945 19.7024 8.6304 19.6032 8.7695 19.5776C8.91139 19.5529 9.03837 19.6109 9.05645 19.7091Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
19
src/assets/Profile_button_placeholder.svg
Normal file
@ -0,0 +1,19 @@
|
||||
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_729_15832)">
|
||||
<path d="M10 28C10 15.2975 20.2975 5 33 5C45.7025 5 56 15.2975 56 28C56 40.7025 45.7025 51 33 51C20.2975 51 10 40.7025 10 28Z" fill="black" fill-opacity="0.38" shape-rendering="crispEdges"/>
|
||||
<path d="M37.1064 33.0342H28.8447L27.1162 38H23.2637L31.3203 16.6719H34.6455L42.7168 38H38.8496L37.1064 33.0342ZM29.8848 30.0459H36.0664L32.9756 21.1982L29.8848 30.0459Z" fill="white"/>
|
||||
<path d="M33 50C20.8497 50 11 40.1503 11 28H9C9 41.2548 19.7452 52 33 52V50ZM55 28C55 40.1503 45.1503 50 33 50V52C46.2548 52 57 41.2548 57 28H55ZM33 6C45.1503 6 55 15.8497 55 28H57C57 14.7452 46.2548 4 33 4V6ZM33 4C19.7452 4 9 14.7452 9 28H11C11 15.8497 20.8497 6 33 6V4Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_729_15832" x="0" y="0" width="66" height="66" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="5"/>
|
||||
<feGaussianBlur stdDeviation="5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.5125 0 0 0 0 0.5125 0 0 0 0 0.5125 0 0 0 0.08 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_729_15832"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_729_15832" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/altlinux-logo.gif
Normal file
After Width: | Height: | Size: 1.4 KiB |
4
src/assets/failedScan.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3155 6.75V5.877C16.3155 4.71615 15.8544 3.60284 15.0335 2.78199C14.2127 1.96115 13.0994 1.5 11.9385 1.5C10.7776 1.5 9.66434 1.96115 8.84349 2.78199C8.02265 3.60284 7.5615 4.71615 7.5615 5.877V6.75H6.312L3.8175 4.2435L2.928 5.1285L5.3445 7.557L5.316 7.6305C4.81106 8.98908 4.55546 10.4276 4.5615 11.877C4.5615 12.171 4.572 12.462 4.5915 12.747L4.596 12.8145H1.5V14.0685H4.7535L4.7625 14.1195C5.0205 15.531 5.5185 16.8225 6.1935 17.916L6.2445 17.9985L3.3 20.943L4.188 21.831L6.9945 19.023L7.0815 19.122C8.3835 20.61 10.0845 21.5055 11.9385 21.5055C13.7655 21.5055 15.4425 20.637 16.737 19.1895L16.8225 19.0935L19.6875 21.9735L20.577 21.087L17.583 18.078L17.6355 17.994C18.336 16.884 18.8505 15.5655 19.1145 14.1195L19.1235 14.0685H22.38V12.8145H19.2825L19.287 12.7485C19.307 12.4589 19.317 12.1688 19.317 11.8785C19.323 10.4102 19.0602 8.95316 18.5415 7.5795L18.513 7.5045L20.868 5.1495L19.98 4.2645L17.493 6.75H16.3155ZM8.8155 6.75V5.877C8.8155 5.04873 9.14453 4.25438 9.73021 3.66871C10.3159 3.08303 11.1102 2.754 11.9385 2.754C12.7668 2.754 13.5611 3.08303 14.1468 3.66871C14.7325 4.25438 15.0615 5.04873 15.0615 5.877V6.75H8.817H8.8155ZM17.361 8.0055L17.391 8.085C17.8155 9.2145 18.0615 10.5 18.0615 11.877C18.0615 14.292 17.307 16.428 16.1505 17.9325C14.9955 19.434 13.494 20.25 11.9385 20.25C10.3845 20.25 8.883 19.434 7.728 17.9325C6.57 16.428 5.8155 14.292 5.8155 11.877C5.8155 10.5 6.0615 9.2145 6.4875 8.085L6.5175 8.0055H17.361Z" fill="#F6F7F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9997 9.33337C12.1765 9.33337 12.3461 9.40361 12.4711 9.52864C12.5961 9.65366 12.6663 9.82323 12.6663 10V16C12.6663 16.1769 12.5961 16.3464 12.4711 16.4714C12.3461 16.5965 12.1765 16.6667 11.9997 16.6667C11.8229 16.6667 11.6533 16.5965 11.5283 16.4714C11.4032 16.3464 11.333 16.1769 11.333 16V10C11.333 9.82323 11.4032 9.65366 11.5283 9.52864C11.6533 9.40361 11.8229 9.33337 11.9997 9.33337ZM11.9997 18.6667C12.1765 18.6667 12.3461 18.5965 12.4711 18.4714C12.5961 18.3464 12.6663 18.1769 12.6663 18C12.6663 17.8232 12.5961 17.6537 12.4711 17.5286C12.3461 17.4036 12.1765 17.3334 11.9997 17.3334C11.8229 17.3334 11.6533 17.4036 11.5283 17.5286C11.4032 17.6537 11.333 17.8232 11.333 18C11.333 18.1769 11.4032 18.3464 11.5283 18.4714C11.6533 18.5965 11.8229 18.6667 11.9997 18.6667Z" fill="#F6F7F9"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
93
src/assets/fonts/LICENSE
Normal file
@ -0,0 +1,93 @@
|
||||
Copyright © 2009 ParaType Ltd. All rights reserved.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
src/assets/fonts/pt-sans-cyrillic-400-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-400-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-400-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-400-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-700-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-700-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-700-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-700-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-400-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-400-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-400-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-400-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-700-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-700-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-700-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-cyrillic-ext-700-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-400-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-400-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-400-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-400-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-700-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-700-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-700-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-700-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-400-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-400-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-400-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-400-normal.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-700-italic.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-700-italic.woff2
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-700-normal.woff
Normal file
BIN
src/assets/fonts/pt-sans-latin-ext-700-normal.woff2
Normal file
29
src/assets/login-drawing.svg
Normal file
@ -0,0 +1,29 @@
|
||||
<svg width="464" height="298" viewBox="0 0 464 298" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_334_10474)">
|
||||
<path d="M452.647 298.667H11.709C8.69903 298.663 5.81332 297.467 3.68494 295.341C1.55657 293.215 0.359359 290.333 0.355957 287.326V15.7352C0.35841 13.5324 1.23547 11.4206 2.79471 9.86306C4.35395 8.30548 6.46805 7.4293 8.67319 7.42676H455.538C457.782 7.4293 459.932 8.32061 461.519 9.90513C463.105 11.4896 463.997 13.638 464 15.8788V287.326C463.996 290.333 462.799 293.215 460.671 295.341C458.542 297.467 455.657 298.663 452.647 298.667V298.667Z" fill="#F2F2F2"/>
|
||||
<path d="M438.391 284.349H27.4482C21.4242 284.349 16.5234 279.764 16.5234 274.13V37.6247C16.5234 33.4458 20.1551 30.0459 24.619 30.0459H441.086C445.624 30.0459 449.315 33.5019 449.315 37.7495V274.13C449.315 279.764 444.414 284.349 438.391 284.349Z" fill="white"/>
|
||||
<path d="M463.842 17.1484H0.197266V11.3747C0.20163 8.3581 1.40373 5.46636 3.53984 3.33392C5.67594 1.20149 8.57161 0.00250136 11.5916 0H452.448C455.468 0.00247592 458.364 1.20145 460.5 3.33389C462.636 5.46633 463.838 8.35809 463.842 11.3747V17.1484Z" fill="#3F3D56"/>
|
||||
<path d="M20.9043 11.5355C22.532 11.5355 23.8516 10.2174 23.8516 8.59147C23.8516 6.96554 22.532 5.64746 20.9043 5.64746C19.2766 5.64746 17.957 6.96554 17.957 8.59147C17.957 10.2174 19.2766 11.5355 20.9043 11.5355Z" fill="white"/>
|
||||
<path d="M32.0918 11.5355C33.7195 11.5355 35.0391 10.2174 35.0391 8.59147C35.0391 6.96554 33.7195 5.64746 32.0918 5.64746C30.4641 5.64746 29.1445 6.96554 29.1445 8.59147C29.1445 10.2174 30.4641 11.5355 32.0918 11.5355Z" fill="white"/>
|
||||
<path d="M43.2788 11.5355C44.9065 11.5355 46.2261 10.2174 46.2261 8.59147C46.2261 6.96554 44.9065 5.64746 43.2788 5.64746C41.6511 5.64746 40.3315 6.96554 40.3315 8.59147C40.3315 10.2174 41.6511 11.5355 43.2788 11.5355Z" fill="white"/>
|
||||
<path d="M223.17 81.7578H142.799C141.6 81.7578 140.451 81.2823 139.604 80.436C138.757 79.5897 138.281 78.4418 138.281 77.2449C138.281 76.048 138.757 74.9001 139.604 74.0537C140.451 73.2074 141.6 72.7319 142.799 72.7319H223.17C224.368 72.7319 225.517 73.2074 226.365 74.0537C227.212 74.9001 227.688 76.048 227.688 77.2449C227.688 78.4418 227.212 79.5897 226.365 80.436C225.517 81.2823 224.368 81.7578 223.17 81.7578V81.7578Z" fill="#CCCCCC"/>
|
||||
<path d="M323.04 81.7578H242.668C241.47 81.7578 240.321 81.2823 239.474 80.436C238.626 79.5897 238.15 78.4418 238.15 77.2449C238.15 76.048 238.626 74.9001 239.474 74.0537C240.321 73.2074 241.47 72.7319 242.668 72.7319H323.04C324.238 72.7319 325.387 73.2074 326.234 74.0537C327.082 74.9001 327.558 76.048 327.558 77.2449C327.558 78.4418 327.082 79.5897 326.234 80.436C325.387 81.2823 324.238 81.7578 323.04 81.7578V81.7578Z" fill="#CCCCCC"/>
|
||||
<path d="M323.04 98.8594H183.222C182.024 98.8594 180.875 98.3839 180.027 97.5375C179.18 96.6912 178.704 95.5433 178.704 94.3464C178.704 93.1495 179.18 92.0016 180.027 91.1553C180.875 90.309 182.024 89.8335 183.222 89.8335H323.04C324.238 89.8335 325.387 90.309 326.234 91.1553C327.082 92.0016 327.558 93.1495 327.558 94.3464C327.558 95.5433 327.082 96.6912 326.234 97.5375C325.387 98.3839 324.238 98.8594 323.04 98.8594Z" fill="#CCCCCC"/>
|
||||
<path d="M223.17 64.4189H142.798C141.601 64.4175 140.454 63.9413 139.607 63.0952C138.761 62.249 138.286 61.1019 138.286 59.906C138.286 58.7101 138.761 57.563 139.607 56.7168C140.454 55.8707 141.601 55.3945 142.798 55.3931H223.17C223.764 55.3923 224.352 55.5085 224.901 55.735C225.449 55.9615 225.948 56.2938 226.368 56.7129C226.788 57.132 227.122 57.6298 227.349 58.1777C227.576 58.7256 227.693 59.3129 227.693 59.906C227.693 60.4991 227.576 61.0864 227.349 61.6343C227.122 62.1822 226.788 62.68 226.368 63.0991C225.948 63.5182 225.449 63.8505 224.901 64.077C224.352 64.3035 223.764 64.4197 223.17 64.4189V64.4189Z" fill="#CCCCCC"/>
|
||||
<path d="M267.398 64.6562H244.571C243.372 64.6562 242.223 64.1808 241.376 63.3344C240.529 62.4881 240.053 61.3402 240.053 60.1433C240.053 58.9464 240.529 57.7985 241.376 56.9522C242.223 56.1058 243.372 55.6304 244.571 55.6304H267.398C268.596 55.6304 269.745 56.1058 270.593 56.9522C271.44 57.7985 271.916 58.9464 271.916 60.1433C271.916 61.3402 271.44 62.4881 270.593 63.3344C269.745 64.1808 268.596 64.6562 267.398 64.6562Z" fill="#CCCCCC"/>
|
||||
<path d="M165.626 98.8594H142.798C141.601 98.8579 140.454 98.3818 139.607 97.5356C138.761 96.6894 138.286 95.5424 138.286 94.3464C138.286 93.1505 138.761 92.0034 139.607 91.1573C140.454 90.3111 141.601 89.835 142.798 89.8335H165.626C166.22 89.8328 166.808 89.949 167.357 90.1754C167.905 90.4019 168.404 90.7342 168.824 91.1533C169.244 91.5725 169.578 92.0702 169.805 92.6181C170.032 93.166 170.149 93.7533 170.149 94.3464C170.149 94.9395 170.032 95.5268 169.805 96.0747C169.578 96.6226 169.244 97.1204 168.824 97.5395C168.404 97.9587 167.905 98.291 167.357 98.5174C166.808 98.7439 166.22 98.8601 165.626 98.8594Z" fill="#CCCCCC"/>
|
||||
<path d="M223.17 241.9H142.799C141.6 241.9 140.451 241.424 139.604 240.578C138.757 239.732 138.281 238.584 138.281 237.387C138.281 236.19 138.757 235.042 139.604 234.196C140.451 233.349 141.6 232.874 142.799 232.874H223.17C223.763 232.874 224.351 232.991 224.899 233.218C225.447 233.444 225.945 233.777 226.365 234.196C226.784 234.615 227.117 235.112 227.344 235.66C227.571 236.207 227.688 236.794 227.688 237.387C227.688 237.98 227.571 238.566 227.344 239.114C227.117 239.662 226.784 240.159 226.365 240.578C225.945 240.997 225.447 241.33 224.899 241.556C224.351 241.783 223.763 241.9 223.17 241.9V241.9Z" fill="#CCCCCC"/>
|
||||
<path d="M323.04 241.9H242.668C241.47 241.9 240.321 241.424 239.474 240.578C238.626 239.732 238.15 238.584 238.15 237.387C238.15 236.19 238.626 235.042 239.474 234.196C240.321 233.349 241.47 232.874 242.668 232.874H323.04C323.633 232.874 324.221 232.991 324.769 233.218C325.317 233.444 325.815 233.777 326.234 234.196C326.654 234.615 326.987 235.112 327.214 235.66C327.441 236.207 327.558 236.794 327.558 237.387C327.558 237.98 327.441 238.566 327.214 239.114C326.987 239.662 326.654 240.159 326.234 240.578C325.815 240.997 325.317 241.33 324.769 241.556C324.221 241.783 323.633 241.9 323.04 241.9V241.9Z" fill="#CCCCCC"/>
|
||||
<path d="M323.04 259.001H183.222C182.024 259.001 180.875 258.526 180.027 257.68C179.18 256.833 178.704 255.685 178.704 254.488C178.704 253.292 179.18 252.144 180.027 251.297C180.875 250.451 182.024 249.976 183.222 249.976H323.04C323.634 249.975 324.222 250.091 324.77 250.318C325.319 250.544 325.818 250.876 326.238 251.295C326.658 251.715 326.991 252.212 327.219 252.76C327.446 253.308 327.563 253.895 327.563 254.488C327.563 255.082 327.446 255.669 327.219 256.217C326.991 256.765 326.658 257.262 326.238 257.682C325.818 258.101 325.319 258.433 324.77 258.66C324.222 258.886 323.634 259.002 323.04 259.001Z" fill="#CCCCCC"/>
|
||||
<path d="M223.17 224.561H142.799C141.6 224.561 140.451 224.085 139.604 223.239C138.757 222.392 138.281 221.245 138.281 220.048C138.281 218.851 138.757 217.703 139.604 216.856C140.451 216.01 141.6 215.535 142.799 215.535H223.17C224.368 215.535 225.517 216.01 226.365 216.856C227.212 217.703 227.688 218.851 227.688 220.048C227.688 221.245 227.212 222.392 226.365 223.239C225.517 224.085 224.368 224.561 223.17 224.561Z" fill="#CCCCCC"/>
|
||||
<path d="M267.398 224.798H244.571C243.372 224.798 242.223 224.323 241.376 223.477C240.529 222.63 240.053 221.482 240.053 220.285C240.053 219.089 240.529 217.941 241.376 217.094C242.223 216.248 243.372 215.772 244.571 215.772H267.398C268.596 215.772 269.745 216.248 270.593 217.094C271.44 217.941 271.916 219.089 271.916 220.285C271.916 221.482 271.44 222.63 270.593 223.477C269.745 224.323 268.596 224.798 267.398 224.798V224.798Z" fill="#CCCCCC"/>
|
||||
<path d="M165.626 259.001H142.799C141.6 259.001 140.451 258.526 139.604 257.68C138.757 256.833 138.281 255.685 138.281 254.488C138.281 253.292 138.757 252.144 139.604 251.297C140.451 250.451 141.6 249.976 142.799 249.976H165.626C166.22 249.975 166.808 250.091 167.357 250.318C167.905 250.544 168.404 250.876 168.824 251.295C169.244 251.715 169.578 252.212 169.805 252.76C170.033 253.308 170.15 253.895 170.15 254.488C170.15 255.082 170.033 255.669 169.805 256.217C169.578 256.765 169.244 257.262 168.824 257.682C168.404 258.101 167.905 258.433 167.357 258.66C166.808 258.886 166.22 259.002 165.626 259.001Z" fill="#CCCCCC"/>
|
||||
<path d="M202.18 193.45C200.474 193.454 198.836 192.784 197.622 191.587L167.41 161.786C166.817 161.201 166.346 160.504 166.025 159.736C165.704 158.968 165.538 158.143 165.538 157.311C165.538 156.478 165.704 155.654 166.025 154.885C166.346 154.117 166.817 153.42 167.41 152.835L197.5 123.154C198.754 121.924 200.443 121.237 202.2 121.242C203.958 121.248 205.642 121.945 206.888 123.184C208.135 124.432 208.835 126.124 208.835 127.887C208.835 129.651 208.135 131.342 206.888 132.591L184.529 154.926C183.832 155.623 183.441 156.568 183.441 157.554C183.441 158.539 183.832 159.484 184.529 160.181L206.767 182.395C207.675 183.3 208.294 184.455 208.544 185.711C208.795 186.968 208.667 188.271 208.175 189.455C207.684 190.639 206.852 191.651 205.785 192.362C204.717 193.073 203.463 193.452 202.18 193.45V193.45Z" fill="#865685"/>
|
||||
<path d="M250.32 193.45C249.037 193.452 247.783 193.073 246.715 192.362C245.648 191.651 244.816 190.639 244.325 189.455C243.833 188.271 243.705 186.968 243.956 185.711C244.207 184.455 244.825 183.3 245.733 182.395L267.971 160.181C268.668 159.484 269.059 158.539 269.059 157.554C269.059 156.568 268.668 155.623 267.971 154.926L245.612 132.591C244.994 131.973 244.503 131.24 244.168 130.433C243.834 129.626 243.662 128.761 243.662 127.887C243.662 127.014 243.834 126.149 244.169 125.342C244.503 124.535 244.994 123.801 245.612 123.184V123.184C246.858 121.945 248.542 121.248 250.3 121.242C252.057 121.237 253.746 121.924 255 123.154L285.09 152.835C285.683 153.42 286.154 154.117 286.475 154.885C286.796 155.654 286.962 156.478 286.962 157.311C286.962 158.143 286.796 158.968 286.475 159.736C286.154 160.504 285.683 161.201 285.09 161.786L254.878 191.587C253.664 192.784 252.026 193.454 250.32 193.45Z" fill="#865685"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_334_10474">
|
||||
<rect width="464" height="298" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 9.9 KiB |
15
src/assets/noData.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="235" height="240" viewBox="0 0 235 240" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="34.8711" y="16.2017" width="36" height="249.944" rx="18" transform="rotate(-26.7465 34.8711 16.2017)" fill="#F15527" fill-opacity="0.2"/>
|
||||
<rect x="156.871" y="36.2017" width="36" height="100.235" rx="18" transform="rotate(-26.7465 156.871 36.2017)" fill="#F15527" fill-opacity="0.2"/>
|
||||
<rect x="0.871094" y="138.037" width="26.745" height="74.4663" rx="13.3725" transform="rotate(-26.7465 0.871094 138.037)" fill="#F15527" fill-opacity="0.2"/>
|
||||
<g clip-path="url(#clip0_2865_33046)">
|
||||
<path d="M117.5 199C161.225 199 197 163.225 197 119.5C197 75.775 161.225 40 117.5 40C73.775 40 38 75.775 38 119.5C38 163.225 73.775 199 117.5 199ZM119.885 82.0555C121.475 80.6245 123.383 79.75 125.45 79.75C127.596 79.75 129.425 80.6245 131.094 82.0555C132.605 83.725 133.4 85.633 133.4 87.7C133.4 89.8465 132.605 91.675 131.094 93.3445C129.425 94.855 127.596 95.65 125.45 95.65C123.383 95.65 121.475 94.855 119.885 93.3445C118.375 91.675 117.5 89.8465 117.5 87.7C117.5 85.633 118.375 83.725 119.885 82.0555ZM100.01 119.261C100.01 119.261 117.261 105.588 123.542 105.031C129.425 104.554 128.232 111.311 127.676 114.809L127.597 115.286C126.484 119.5 125.132 124.588 123.78 129.438C120.759 140.488 117.818 151.3 118.534 153.287C119.328 155.991 124.257 152.572 127.835 150.187C128.312 149.869 128.709 149.551 129.107 149.312C129.107 149.312 129.743 148.676 130.379 149.551C130.538 149.789 130.697 150.028 130.856 150.187C131.572 151.3 131.969 151.697 131.015 152.333L130.697 152.492C128.948 153.685 121.475 158.932 118.454 160.84C115.194 162.987 102.713 170.142 104.621 156.229C106.291 146.451 108.517 138.023 110.266 131.425C113.525 119.5 114.956 114.094 107.642 118.785C104.701 120.534 102.951 121.646 101.918 122.362C101.043 122.998 100.964 122.998 100.408 121.965L100.169 121.487L99.7715 120.852C99.215 120.057 99.215 119.977 100.01 119.261Z" fill="#0F2139"/>
|
||||
<path d="M119.885 82.0555C121.475 80.6245 123.383 79.75 125.45 79.75C127.596 79.75 129.425 80.6245 131.094 82.0555C132.605 83.725 133.4 85.633 133.4 87.7C133.4 89.8465 132.605 91.675 131.094 93.3445C129.425 94.855 127.596 95.65 125.45 95.65C123.383 95.65 121.475 94.855 119.885 93.3445C118.375 91.675 117.5 89.8465 117.5 87.7C117.5 85.633 118.375 83.725 119.885 82.0555Z" fill="white"/>
|
||||
<path d="M100.01 119.261C100.01 119.261 117.261 105.588 123.542 105.031C129.425 104.554 128.232 111.311 127.676 114.809L127.597 115.286C126.484 119.5 125.132 124.588 123.78 129.438C120.759 140.488 117.818 151.3 118.534 153.287C119.328 155.991 124.257 152.572 127.835 150.187C128.312 149.869 128.709 149.551 129.107 149.312C129.107 149.312 129.743 148.676 130.379 149.551C130.538 149.789 130.697 150.028 130.856 150.187C131.572 151.3 131.969 151.697 131.015 152.333L130.697 152.492C128.948 153.685 121.475 158.932 118.454 160.84C115.194 162.987 102.713 170.142 104.621 156.229C106.291 146.451 108.517 138.023 110.266 131.425C113.525 119.5 114.956 114.094 107.642 118.785C104.701 120.534 102.951 121.646 101.918 122.362C101.043 122.998 100.964 122.998 100.408 121.965L100.169 121.487L99.7715 120.852C99.215 120.057 99.215 119.977 100.01 119.261Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2865_33046">
|
||||
<rect width="159" height="159" fill="white" transform="translate(38 40)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/repocube-1.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src/assets/repocube-2.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
src/assets/repocube-3.png
Normal file
After Width: | Height: | Size: 28 KiB |