Compare commits

...

No commits in common. "master" and "patches" have entirely different histories.

175 changed files with 34883 additions and 177 deletions

10
.eslintignore Normal file
View File

@ -0,0 +1,10 @@
**/.git
**/.svn
**/.hg
**/node_modules
**/.github
README.md
LICENSE
Makefile
**/coverage
**/build

54
.eslintrc.json Normal file
View 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" }
]
}
}

View File

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

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

@ -0,0 +1,142 @@
on:
push:
branches:
- main
pull_request:
branches:
- main
release:
types:
- published
name: end-to-end-test
permissions:
contents: read
jobs:
build-and-test:
name: Test zui/zot integration
env:
CI: ""
REGISTRY_HOST: "localhost"
REGISTRY_PORT: "8080"
runs-on: ubuntu-latest
steps:
- name: Cleanup disk space
run: |
# To free up ~15 GB of disk space
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
- name: Checkout zui repository
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set up Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- name: Build zui
run: |
cd $GITHUB_WORKSPACE
make install
make build
- name: Install container image tooling
run: |
cd $GITHUB_WORKSPACE
sudo apt-get update
sudo apt-get install libgpgme-dev libassuan-dev libbtrfs-dev libdevmapper-dev pkg-config rpm snapd jq
git clone https://github.com/containers/skopeo -b v1.9.0 $GITHUB_WORKSPACE/src/github.com/containers/skopeo
cd $GITHUB_WORKSPACE/src/github.com/containers/skopeo && make bin/skopeo
chmod +x bin/skopeo
sudo mv bin/skopeo /usr/local/bin/skopeo
which skopeo
skopeo -v
curl -L https://github.com/regclient/regclient/releases/download/v0.4.7/regctl-linux-amd64 -o regctl
chmod +x regctl
sudo mv regctl /usr/local/bin/regctl
which regctl
regctl version
curl -L https://github.com/sigstore/cosign/releases/download/v1.13.0/cosign-linux-amd64 -o cosign
chmod +x cosign
sudo mv cosign /usr/local/bin/cosign
which cosign
cosign version
pushd $(mktemp -d)
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.38.3/trivy_0.38.3_Linux-64bit.tar.gz -o trivy.tar.gz
tar -xzvf trivy.tar.gz
sudo mv trivy /usr/local/bin/trivy
popd
which trivy
trivy version
cd $GITHUB_WORKSPACE
- name: Install go
uses: actions/setup-go@v3
with:
go-version: 1.21.x
- name: Checkout zot repo
uses: actions/checkout@v3
with:
fetch-depth: 2
repository: project-zot/zot
ref: main
path: zot
- name: Build zot
run: |
cd $GITHUB_WORKSPACE/zot
make binary ZUI_BUILD_PATH=$GITHUB_WORKSPACE/build
ls -l bin/
- name: Bringup zot server
run: |
cd $GITHUB_WORKSPACE/zot
mkdir /tmp/zot
./bin/zot-linux-amd64 serve examples/config-ui.json &
while true; do x=0; curl -f http://$REGISTRY_HOST:$REGISTRY_PORT/v2/ || x=1; if [ $x -eq 0 ]; then break; fi; sleep 1; done
- name: Load image test data from cache into a local folder
id: restore-cache
uses: actions/cache@v3
with:
path: tests/data/images
key: image-config-${{ hashFiles('**/tests/data/config.yaml') }}
restore-keys: |
image-config-
- name: Load image test data into zot server
run: |
cd $GITHUB_WORKSPACE
regctl registry set --tls disabled $REGISTRY_HOST:$REGISTRY_PORT
make test-data REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT
- name: Install playwright dependencies
run: |
cd $GITHUB_WORKSPACE
make playwright-browsers
- name: Trigger CVE scanning
run: |
# trigger CVE scanning for all images before running the tests
curl -X POST -H "Content-Type: application/json" -m 600 --data '{ "query": "{ ImageListForCVE (id:\"CVE-2021-43616\") { Results { RepoName Tag } } }" }' http://$REGISTRY_HOST:$REGISTRY_PORT/v2/_zot/ext/search
- name: Run integration tests
run: |
cd $GITHUB_WORKSPACE
make integration-tests REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT
- name: Upload playwright report
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
retention-days: 30

24
.github/workflows/stale.yaml vendored Normal file
View 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
View 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
View File

@ -0,0 +1,10 @@
**/.git
**/.svn
**/.hg
**/node_modules
**/.github
README.md
LICENSE
Makefile
**/coverage
**/build

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": true,
"tabWidth": 2,
"printWidth": 120,
"singleQuote": true,
"trailingComma": "none",
"endOfLine": "auto"
}

View File

@ -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
View 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
View 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
View 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/).

View File

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

@ -0,0 +1,7 @@
{
"compilerOptions": {
"baseUrl": "./src",
"checkJs": true,
"jsx": "react"
}
}

19454
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

71
package.json Normal file
View 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
View File

@ -0,0 +1,114 @@
// @ts-check
const { devices } = require('@playwright/test');
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* @see https://playwright.dev/docs/test-configuration
* @type {import('@playwright/test').PlaywrightTestConfig}
*/
const config = {
testDir: './tests',
/* Maximum time one test can run for. */
timeout: 50 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 15000
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: 2,
/* Opt out of parallel tests on CI. */
workers: 2,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [['html', { open: 'never' }]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure'
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
ignoreHTTPSErrors: true
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
ignoreHTTPSErrors: true
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
ignoreHTTPSErrors: true
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
module.exports = config;

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

54
public/index.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View 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
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

86
src/App.css Normal file
View 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
View 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
View 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();
});

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

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

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

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

View File

@ -0,0 +1,40 @@
import { render, screen, fireEvent } from '@testing-library/react';
import UserAccountMenu from 'components/Header/UserAccountMenu';
import React from 'react';
const mockIsApiKeyEnabled = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => {}
}));
jest.mock('../../utilities/authUtilities', () => ({
isApiKeyEnabled: () => {
return mockIsApiKeyEnabled();
},
getLoggedInUser: () => {
return 'jest-user';
},
logoutUser: () => {}
}));
describe('Account Menu', () => {
it('displays Api Keys menu item with its divider when the API Keys config is enabled', async () => {
mockIsApiKeyEnabled.mockReturnValue(true);
render(<UserAccountMenu />);
const userIconButton = await screen.getByTestId('user-icon-header-button');
fireEvent.click(userIconButton);
expect(await screen.queryByTestId('api-keys-menu-item')).toBeInTheDocument();
expect(await screen.queryByTestId('api-keys-menu-item-divider')).toBeInTheDocument();
});
it('does not display Api Keys menu item and divider when the API Keys config is disabled', async () => {
mockIsApiKeyEnabled.mockReturnValue(false);
render(<UserAccountMenu />);
const userIconButton = await screen.getByTestId('user-icon-header-button');
fireEvent.click(userIconButton);
expect(await screen.queryByTestId('api-keys-menu-item')).not.toBeInTheDocument();
expect(await screen.queryByTestId('api-keys-menu-item-divider')).not.toBeInTheDocument();
});
});

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

@ -0,0 +1,19 @@
import { api } from '../api';
describe('api module', () => {
it('should redirect to login if a 401 error is received', () => {
const location = new URL('https://www.test.com');
location.replace = jest.fn();
delete window.location;
window.location = location;
const axiosInstance = api.getAxiosInstance();
expect(
axiosInstance.interceptors.response.handlers[0].rejected({
response: { statusText: 'Unauthorized', status: 401 }
})
).rejects.toMatchObject({
response: { statusText: 'Unauthorized', status: 401 }
});
expect(location.replace).toHaveBeenCalledWith('/login');
});
});

172
src/api.js Normal file
View 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 };

View File

@ -0,0 +1,401 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
]>
<svg version="1.1" id="Layer_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="220px" height="97px"
viewBox="0 0 220 97" style="enable-background:new 0 0 220 97;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#FDC811;}
.st2{fill-rule:evenodd;clip-rule:evenodd;}
.st3{fill:#FFCA07;}
</style>
<metadata>
<sfw xmlns="&ns_sfw;">
<slices></slices>
<sliceSourceBounds bottomLeftOrigin="true" height="96.7914734" width="220" x="0" y="0.2085275"></sliceSourceBounds>
</sfw>
</metadata>
<g>
<path class="st0" d="M219.9046783,66.5411987c0-0.0004807,0-0.0004807-0.0004883-0.0004807l-0.1141663-0.3805542
c-0.1006775-0.3381577-0.3820038-1.0241241-1.1329956-1.8189468c-0.3661041-0.389225-0.8290253-0.7047501-1.5294342-1.1821251
c-2.2240753-1.5154724-2.5318909-1.9957428-2.5627136-2.0737801c-0.0192719-0.0703278-0.0419159-0.1401787-0.0679321-0.2095451
c-0.465332-1.2375221-0.0703278-3.2380791,0.3126373-5.1764946c0.1690826-0.8574486,0.3439484-1.7442856,0.4484711-2.5704231
c1.3285675-8.9984093,0.8213348-13.87817-0.8174591-21.4073524l-0.3270874-1.5911007
c-0.7245026-3.5492649-1.3507233-6.6144085-2.5458527-9.9276352c-0.3275757-0.9142933-0.8256531-1.8305111-1.2245178-2.5641613
l-0.3015594-0.5631237c-0.2601166-0.4952011-0.3265991-0.7649612-0.4701538-1.3478346
c-0.1478729-0.6922226-0.1950836-1.4258718-0.2456665-2.2043209c-0.0761108-1.1883879-0.1546326-2.4172392-0.5746918-3.7087135
c-0.5130157-1.6556492-1.9152832-3.5931015-3.3401947-4.6119261c-0.7866364-0.5443363-1.6267548-0.7957907-2.3021088-0.9976287
c-0.2606049-0.0785193-0.6546478-0.197021-0.7668915-0.2663877c-0.0414276-0.0255308-0.0838165-0.0500982-0.1247559-0.0717754
L201.08461,3.2608931c-1.4870605-0.7914555-3.0251617-1.6098869-4.4828339-2.528033
c-0.868042-0.595398-2.7115631-1.1594846-4.2304077-0.2716864l-0.4007874,0.2345945
c-0.6700592,0.3925966-1.0891571,1.0905997-1.1214294,1.8661585c-0.0327606,0.7760406,0.3270874,1.5058367,0.9629517,1.9528668
l0.3781433,0.2663879c1.8016052,1.2697968,2.8006744,2.6739922,3.235672,4.5401506l0.1151276,0.5587883
c-1.3719177,0.9205542-2.8353729,2.2626085-4.3209686,5.3104105c-0.5501251,1.1281738-0.9576569,2.0024834-1.3661499,2.8792019
c-0.2663879,0.5722752-0.5202484,1.1156483-0.7996368,1.69804H20.0874577c-0.2726498,0-0.4932747,0.2206249-0.4932747,0.4932747
v16.3561058c-0.0178242-0.0038567-0.0356464-0.0077095-0.0534706-0.0110817
c-1.5858002-0.3246727-3.5699797-0.482193-6.0652561-0.482193c-1.5853195,0-3.0940466,0.1353607-4.4847536,0.40271
c-1.5434103,0.297699-2.766963,0.7326889-3.7405062,1.3300133c-1.30159,0.7981987-2.3281217,1.777523-3.0521371,2.9114761
c-0.7008934,1.0978241-1.2066927,2.4841957-1.5463009,4.2390785l-0.403676,2.087265
c-0.0260125,0.1343994,0.0048171,0.2740974,0.0862267,0.3848915c0.0809279,0.1103134,0.2042466,0.1825676,0.3405715,0.1984673
l3.2886589,0.3853683c-0.8357732,0.4894218-1.5164344,1.0597725-2.0588441,1.7226067
C0.6406791,51.3267593,0,53.2622833,0,55.5383797c0,2.4832344,0.8733468,4.60952,2.5959547,6.319603
c1.7182724,1.7038193,4.1422558,2.5680122,7.2040272,2.5680122c2.1498871,0,4.097456-0.3882599,5.7921238-1.1556282
c0.2543449-0.1175385,0.5096531-0.2509727,0.763998-0.3983765c0.1806431,0.3559837,0.3945236,0.7779655,0.3945236,0.7779655
c0.0838184,0.1657104,0.2538624,0.2702408,0.4398041,0.2702408h2.4037514v5.0753326
c0,0.2726517,0.2206249,0.4932785,0.4932747,0.4932785h181.1667023c0.0476837,0,0.0939331-0.007225,0.1377716-0.0211945h16.3368378
c0.7163086,0,1.397934-0.3424988,1.823288-0.9162216C219.9788513,67.9786377,220.1103668,67.2266769,219.9046783,66.5411987z"/>
<rect x="22.0607033" y="22.2340794" class="st1" width="177.2202148" height="44.7882233"/>
<path class="st1" d="M10.4436264,46.3384933L3.07324,45.4756279c0.2770877-1.432476,0.6785073-2.5584679,1.2031634-3.3794403
c0.5240412-0.8209724,1.2788663-1.5331688,2.263741-2.1373215c0.7077856-0.4343758,1.6802859-0.7717323,2.9178729-1.010601
c1.2384424-0.2381325,2.5775766-0.357933,4.0175247-0.357933c2.3113928,0,4.1679554,0.1447868,5.5702972,0.4321671
c1.4023438,0.2873764,2.5702286,0.8878555,3.5051231,1.7999687c0.6570721,0.6313477,1.1752357,1.5265541,1.5537491,2.6841507
c0.3792496,1.158329,0.5688763,2.2630043,0.5688763,3.316227v9.8730011c0,1.0532265,0.0595322,1.8778725,0.180069,2.4739418
c0.1198025,0.5968056,0.3815765,1.3575096,0.7858143,2.2843208h-7.2374744
c-0.2910519-0.5754929-0.4806767-1.0135384-0.5688744-1.3156128c-0.0881977-0.3020821-0.1771297-0.7754059-0.2653294-1.4214554
c-1.0105972,1.0818901-2.0145807,1.8528862-3.0126848,2.3159256c-1.3641233,0.617382-2.9494772,0.9268074-4.7553253,0.9268074
c-2.4004464,0-4.2224631-0.6181183-5.4667859-1.8528824c-1.2443225-1.2347679-1.8661156-2.7583847-1.8661156-4.5686417
c0-1.6978073,0.448338-3.095005,1.3450146-4.1893921c0.8966765-1.0943832,2.5518527-1.9087448,4.9640594-2.4416008
c2.8928833-0.6460495,4.7685547-1.0987968,5.6277466-1.3582458c0.8591928-0.2594528,1.768364-0.6004829,2.7282486-1.0216255
c0-1.0517578-0.1955051-1.7889442-0.5872498-2.2100868c-0.3917446-0.4211464-1.0796862-0.6320839-2.0652952-0.6320839
c-1.2634325,0-2.2108221,0.2249069-2.8421707,0.6739807C11.1447973,44.7105141,10.7471733,45.3697929,10.4436264,46.3384933z
M17.1319504,50.8431931c-1.0605774,0.4211426-2.165988,0.7937775-3.3154974,1.116436
c-1.5669785,0.4630356-2.5577326,0.9187279-2.9752016,1.3678017c-0.4292297,0.463768-0.6437235,0.9900131-0.6437235,1.5794716
c0,0.6732407,0.2108192,1.2244759,0.6341677,1.6522331c0.4233494,0.4284973,1.0451431,0.6423759,1.8668518,0.6423759
c0.8584576,0,1.6581163-0.2315178,2.3967714-0.6945572c0.738656-0.4637756,1.2626982-1.027504,1.5728598-1.6948662
c0.3094254-0.666626,0.4637718-1.5331688,0.4637718-2.6003609V50.8431931z"/>
<g>
<path d="M10.4436264,46.3384933L3.07324,45.4756279c0.2770877-1.432476,0.6785073-2.5584679,1.2031634-3.3794403
c0.5240412-0.8209724,1.2788663-1.5331688,2.263741-2.1373215c0.7077856-0.4343758,1.6802859-0.7717323,2.9178729-1.010601
c1.2384424-0.2381325,2.5775766-0.357933,4.0175247-0.357933c2.3113928,0,4.1679554,0.1447868,5.5702972,0.4321671
c1.4023438,0.2873764,2.5702286,0.8878555,3.5051231,1.7999687c0.6570721,0.6313477,1.1752357,1.5265541,1.5537491,2.6841507
c0.3792496,1.158329,0.5688763,2.2630043,0.5688763,3.316227v9.8730011c0,1.0532265,0.0595322,1.8778725,0.180069,2.4739418
c0.1198025,0.5968056,0.3815765,1.3575096,0.7858143,2.2843208h-7.2374744
c-0.2910519-0.5754929-0.4806767-1.0135384-0.5688744-1.3156128c-0.0881977-0.3020821-0.1771297-0.7754059-0.2653294-1.4214554
c-1.0105972,1.0818901-2.0145807,1.8528862-3.0126848,2.3159256c-1.3641233,0.617382-2.9494772,0.9268074-4.7553253,0.9268074
c-2.4004464,0-4.2224631-0.6181183-5.4667859-1.8528824c-1.2443225-1.2347679-1.8661156-2.7583847-1.8661156-4.5686417
c0-1.6978073,0.448338-3.095005,1.3450146-4.1893921c0.8966765-1.0943832,2.5518527-1.9087448,4.9640594-2.4416008
c2.8928833-0.6460495,4.7685547-1.0987968,5.6277466-1.3582458c0.8591928-0.2594528,1.768364-0.6004829,2.7282486-1.0216255
c0-1.0517578-0.1955051-1.7889442-0.5872498-2.2100868c-0.3917446-0.4211464-1.0796862-0.6320839-2.0652952-0.6320839
c-1.2634325,0-2.2108221,0.2249069-2.8421707,0.6739807C11.1447973,44.7105141,10.7471733,45.3697929,10.4436264,46.3384933z
M17.1319504,50.8431931c-1.0605774,0.4211426-2.165988,0.7937775-3.3154974,1.116436
c-1.5669785,0.4630356-2.5577326,0.9187279-2.9752016,1.3678017c-0.4292297,0.463768-0.6437235,0.9900131-0.6437235,1.5794716
c0,0.6732407,0.2108192,1.2244759,0.6341677,1.6522331c0.4233494,0.4284973,1.0451431,0.6423759,1.8668518,0.6423759
c0.8584576,0,1.6581163-0.2315178,2.3967714-0.6945572c0.738656-0.4637756,1.2626982-1.027504,1.5728598-1.6948662
c0.3094254-0.666626,0.4637718-1.5331688,0.4637718-2.6003609V50.8431931z"/>
<g>
<path d="M28.7858028,30.5908012h7.7305279v30.863308h-7.7305279V30.5908012z"/>
<path d="M49.97015,30.5908012v8.5051918h4.2445145v6.2737999H49.97015v7.920887
c0,0.9525337,0.0815849,1.583149,0.2462196,1.8911018c0.2528343,0.4762688,0.6945572,0.7144051,1.3259048,0.7144051
c0.5688744,0,1.3649826-0.1815414,2.3879547-0.5468292l0.5681381,5.9151268
c-1.9071541,0.4630356-3.6881332,0.6952896-5.3433113,0.6952896c-1.9197655,0-3.3346062-0.2734146-4.2437744-0.8202362
c-0.9099083-0.5468254-1.5824165-1.3773537-2.018261-2.4915848c-0.4358406-1.1142311-0.6533966-2.919342-0.6533966-5.413868
v-7.8642921h-2.8421707V39.095993h2.8421707v-4.1055984L49.97015,30.5908012z"/>
</g>
</g>
<path d="M65.8155975,30.5908012h7.7305298v30.863308h-7.7305298V30.5908012z"/>
<path d="M77.9810028,30.5908012h7.7114105v5.831337h-7.7114105V30.5908012z M77.9810028,39.095993h7.7114105v22.3581161h-7.7114105
V39.095993z"/>
<path d="M89.8428497,39.095993h7.1807632v3.6418304c1.0738068-1.4875984,2.1608429-2.5503807,3.2596436-3.1890793
c1.098793-0.6386986,2.4379272-0.9584122,4.0166626-0.9584122c2.1343842,0,3.8057327,0.7055779,5.0118408,2.1160088
c1.2060928,1.4111595,1.8095169,3.5896416,1.8095169,6.5369186v14.2108498h-7.7496338v-12.294754
c0-1.4030838-0.2337189-2.3960419-0.7011719-2.9788818c-0.467453-0.5820999-1.124527-0.8738899-1.9704895-0.8738899
c-0.9348907,0-1.6926575,0.3932152-2.274025,1.1789093c-0.5806351,0.7864265-0.8716812,2.1975937-0.8716812,4.2312813v10.7373352
h-7.7114258V39.095993z"/>
<path d="M136.1517639,61.4541092h-7.200592V57.832859c-1.0730743,1.4883385-2.156311,2.5474434-3.2493515,3.178791
c-1.0929184,0.6320839-2.4342575,0.9481239-4.0262222,0.9481239c-2.1218872,0-3.7866211-0.7055817-4.9927216-2.1160088
s-1.8093948-3.5822945-1.8093948-6.5163345V39.095993h7.7495041v12.2954979c0,1.4030762,0.2337341,2.3997078,0.7011719,2.98843
c0.467453,0.5901909,1.124527,0.884182,1.9704895,0.884182c0.9216614,0,1.6766129-0.3924789,2.264595-1.1781731
c0.5871201-0.7856979,0.8811264-2.1968613,0.8811264-4.2320213V39.095993h7.7113953V61.4541092z"/>
<path d="M138.3317413,39.095993h9.1512299l3.1927643,6.2311707l3.7234039-6.2311707h8.5074158l-6.8639984,10.652813
l7.3564301,11.7053032h-8.9998474l-3.7234039-7.1998711l-4.3671417,7.1998711h-8.3561096l7.3086395-11.7053032
L138.3317413,39.095993z"/>
<g>
<g>
<path class="st0" d="M56.8743248,77.8356705H42.8329544c-1.4275665,0-2.589386,1.1615219-2.5898552,2.588913h-0.4812546
c-0.0004692-1.4273911-1.1622887-2.588913-2.5898552-2.588913h-4.6130028c-1.4278011,0-2.5898552,1.162056-2.5898552,2.5898514
v3.8786774l-2.0189228-4.8702927c-0.4023762-0.9709625-1.3409405-1.5982361-2.3917198-1.5982361h-5.0088043
c-1.0507793,0-1.9893436,0.6272736-2.3917198,1.5982361l-5.6032133,13.5145721
c-0.3324184,0.8005219-0.2432098,1.7090378,0.2385139,2.4297485c0.4812555,0.7211761,1.2860069,1.1517258,2.1532049,1.1517258
h4.7458763c1.1160412,0,2.1029663-0.711319,2.4565125-1.7700806l0.1356888-0.407074h1.473814l0.1347523,0.4047241
c0.353075,1.060173,1.3400002,1.7724304,2.4569817,1.7724304h4.8106689c0.2206726,0,0.4436932-0.0347443,0.6953545-0.1079865
c0.248373,0.0723038,0.4789066,0.1079865,0.7028675,0.1079865h11.799427c0.5892448,0,1.152195-0.1990738,1.6104431-0.5653
c0.4577789,0.3662262,1.0207291,0.5653,1.6099739,0.5653h4.5477371c1.427803,0,2.5898552-1.162056,2.5898552-2.589859v-7.562973
h2.1579018c1.4277992,0,2.5898552-1.162056,2.5898552-2.589859v-3.3617401
C59.46418,78.9977264,58.302124,77.8356705,56.8743248,77.8356705z"/>
<path class="st0" d="M134.7516785,86.9945374l3.5993042-5.0698471c0.5624847-0.7934799,0.6347961-1.8231354,0.1887512-2.6870422
c-0.4465027-0.8648529-1.3282623-1.4019775-2.3006287-1.4019775h-5.076889c-0.9211884,0-1.7804108,0.4958115-2.2419434,1.2935181
l-0.2835846,0.4915848l-0.355423-0.5681152c-0.4760971-0.7620239-1.2968063-1.2169876-2.1954651-1.2169876h-5.0740662
c-0.2244339,0-0.4502716,0.0333328-0.6878433,0.1004791c-0.2403946-0.0671463-0.4620056-0.1004791-0.6756363-0.1004791
h-4.6144104c-1.2522049,0-2.2992249,0.893486-2.538681,2.0762024c-0.2389832-1.1827164-1.2860031-2.0762024-2.538208-2.0762024
h-4.6125336c-0.5282059,0-1.0324631,0.1619797-1.470993,0.4704514c-0.4380569-0.3084717-0.9418488-0.4704514-1.4705276-0.4704514
h-4.3463135c-1.3362503,0-2.4396133,1.0174408-2.5753021,2.3184738l-0.952179-1.276619
c-0.4864197-0.6521606-1.2625351-1.0418549-2.075737-1.0418549h-4.2843399c-0.6620178,0-1.2935181,0.2587051-1.7747726,0.7178879
c-0.4803162-0.4591827-1.1122894-0.7178879-1.7743073-0.7178879h-4.6158142c-1.427803,0-2.589859,1.162056-2.589859,2.5898514
v7.6287155h-3.8021469v-7.6287155c0-1.4277954-1.162056-2.5898514-2.5898514-2.5898514h-4.614418
c-1.4277954,0-2.5898514,1.162056-2.5898514,2.5898514V93.940094c0,1.427803,1.162056,2.589859,2.5898514,2.589859h11.7327576
c0.3183365,0,0.6315002-0.0596313,0.9319916-0.1765366c0.3004913,0.1169052,0.6131897,0.1765366,0.9315262,0.1765366h4.6158142
c0.6620178,0,1.2939911-0.2587051,1.7743073-0.7178955c0.4812546,0.4591904,1.1127548,0.7178955,1.7747726,0.7178955h4.2843399
c1.3296738,0,2.4283447-1.0075836,2.5734253-2.2992249l0.9672012,1.2747345
c0.4868927,0.6413651,1.2578354,1.0244904,2.0625916,1.0244904h4.3463135c1.3371811,0,2.4410172-1.0188522,2.5757675-2.3212891
c0.6704712,0.7277451,1.4432983,1.2911682,2.2917175,1.6728821c0.8502884,0.4169312,1.8949661,0.6840897,3.0218048,0.7718887
l0.2319412,0.0178452c0.8117981,0.0615005,1.5789871,0.1201935,2.3335037,0.1201935
c1.6541061,0,2.9898834-0.1981354,4.0725861-0.6005096c0.5117722-0.1638641,1.016037-0.4061356,1.5334396-0.7380829
c0.4845428,0.6704712,1.2606583,1.0770721,2.0870056,1.0770721h5.1388626c0.8601532,0,1.6616211-0.4258499,2.1442795-1.1381073
l0.7573318-1.1183929l0.7545166,1.1165085c0.48172,0.7136688,1.2836609,1.1399918,2.1452179,1.1399918h5.1421509
c0.9808197,0,1.86586-0.5437012,2.3100281-1.4193497c0.443222-0.8751831,0.3582306-1.9104614-0.2216187-2.7015991
L134.7516785,86.9945374z"/>
<path class="st0" d="M208.9085693,77.8356705h-5.9943237c-1.0061646,0-1.917038,0.5925293-2.3400726,1.4822617
c-0.4225616-0.8902054-1.331543-1.4822617-2.3400726-1.4822617h-6.0666199c-1.4278107,0-2.589859,1.162056-2.589859,2.5898514
v4.5585403l-2.3011017-5.5501556c-0.4023743-0.9709625-1.3409424-1.5982361-2.3917236-1.5982361h-5.0078583
c-1.0507813,0-1.9893494,0.6272736-2.3917236,1.5982361l-2.8053589,6.7657394v-0.6991119
c0-0.1788864-0.0187836-0.3573074-0.0554047-0.5324326c0.3784332-0.4620056,0.585495-1.0390396,0.585495-1.6395569v-2.9030228
c0-1.4277954-1.1620636-2.5898514-2.589859-2.5898514H160.293396c-0.3629456,0-0.7211914,0.0812225-1.067688,0.24086
c-0.3455658-0.1596375-0.7042694-0.24086-1.067215-0.24086h-14.0366669c-1.4278107,0-2.589859,1.162056-2.589859,2.5898514
v3.3617401c0,1.427803,1.1620483,2.589859,2.589859,2.589859h2.0902863v7.562973
c0,1.427803,1.1620483,2.589859,2.589859,2.589859h4.6129913c1.4278107,0,2.589859-1.162056,2.589859-2.589859v-7.562973
h1.6987152v7.562973c0,1.427803,1.1620483,2.589859,2.589859,2.589859h12.5891418
c0.2220917,0,0.4516754-0.0352173,0.6986542-0.1065826c0.2511902,0.0723038,0.4727936,0.1065826,0.6920624,0.1065826h4.7486877
c1.1183929,0,2.1062622-0.7131958,2.4579315-1.7747726l0.1333313-0.4023819h1.4705353l0.1352234,0.4061356
c0.3535461,1.059227,1.3409271,1.771019,2.4569702,1.771019h4.812088c0.2601013,0,0.5286713-0.0493011,0.8193054-0.1507187
c0.2817078,0.1000061,0.5704651,0.1507187,0.8601532,0.1507187h3.756134c0.5164642,0,1.0240173-0.1568222,1.4503326-0.4446335
c0.4263306,0.2878113,0.9338684,0.4446335,1.4498749,0.4446335h3.4307556c0.5160065,0,1.0230713-0.1568222,1.4494019-0.4441681
c0.4258423,0.2878189,0.9329224,0.4441681,1.4494019,0.4441681h3.755188c1.4277954,0,2.589859-1.162056,2.589859-2.589859
V80.4255219C211.4984283,78.9977264,210.3363647,77.8356705,208.9085693,77.8356705z"/>
</g>
<g>
<g>
<path class="st2" d="M20.5494957,80.4256134L14.9462709,93.939949h4.7459002l0.7266064-2.1771622h5.2069492l0.7242508,2.1771622
h4.8109646l-5.6026363-13.5143356H20.5494957z M21.4068203,88.7963791l1.6487007-4.8101578l1.6478157,4.8101578H21.4068203z"/>
<polygon class="st2" points="37.1717606,80.4256134 32.5589333,80.4256134 32.5589333,93.939949 44.3586197,93.939949
44.3586197,90.6439514 37.1717606,90.6439514 "/>
<polygon class="st2" points="42.8327599,83.7874222 47.5786629,83.7874222 47.5786629,93.939949 52.1267204,93.939949
52.1267204,83.7874222 56.8740921,83.7874222 56.8740921,80.4256134 42.8327599,80.4256134 "/>
</g>
<g>
<polygon class="st2" points="71.0224304,80.4256134 66.4078369,80.4256134 66.4078369,93.939949 78.1403961,93.939949
78.1403961,90.6439514 71.0224304,90.6439514 "/>
<rect x="80.0041428" y="80.4256134" class="st2" width="4.6157718" height="13.5143366"/>
<polygon class="st2" points="98.0564499,87.9391251 92.4532242,80.4256134 88.1692505,80.4256134 88.1692505,93.939949
92.4532242,93.939949 92.4532242,86.555687 98.0564499,93.939949 102.4031296,93.939949 102.4031296,80.4256134
98.0564499,80.4256134 "/>
<path class="st2" d="M115.0338593,88.7336655c0,0.7249908-0.1987228,1.3160934-0.6577072,1.709938
c-0.4634018,0.3968658-1.0551682,0.5941925-1.8450775,0.5941925c-0.7922592,0-1.4529114-0.1973267-1.9127808-0.5941925
c-0.4634094-0.3938446-0.6612473-0.9849472-0.6612473-1.709938v-8.3080521h-4.6128311v8.1077042
c0,0.6569061,0.132782,1.3833618,0.4610443,2.2391434c0.1330795,0.5292053,0.4610519,1.0575943,0.9895172,1.5837097
c0.4619293,0.5269241,0.9880447,0.9230499,1.5821609,1.1838226c0.5270004,0.2653427,1.2518387,0.4642181,2.1115189,0.5314865
c0.8549652,0.0649948,1.646637,0.129982,2.3691254,0.129982c1.2533035,0,2.3735352-0.129982,3.2332153-0.4611206
c0.6577148-0.2003479,1.2488937-0.5941925,1.8406525-1.1180267c0.5941238-0.5314789,1.0575256-1.1233978,1.3183746-1.8498535
c0.2655563-0.722702,0.3986359-1.4491653,0.3986359-2.2391434v-8.1077042h-4.6146011V88.7336655z"/>
<polygon class="st2" points="136.2389221,80.4256134 131.1621094,80.4256134 128.7232056,84.6446686 126.0855789,80.4256134
121.0117035,80.4256134 125.6895981,86.9510803 120.5506592,93.939949 125.6895981,93.939949 128.5924835,89.6520844
131.4909668,93.939949 136.632843,93.939949 131.55867,87.0183563 "/>
</g>
<polygon class="st2" points="144.121933,83.7874222 148.8021851,83.7874222 148.8021851,93.939949 153.4150085,93.939949
153.4150085,83.7874222 158.1585541,83.7874222 158.1585541,80.4256134 144.121933,80.4256134 "/>
<polygon class="st2" points="164.9077606,88.2702637 172.09021,88.2702637 172.09021,85.5003738 164.9077606,85.5003738
164.9077606,83.3285828 172.6201477,83.3285828 172.6201477,80.4256134 160.293457,80.4256134 160.293457,93.939949
172.8827515,93.939949 172.8827515,90.9070053 164.9077606,90.9070053 "/>
<path class="st2" d="M179.876709,80.4256134l-5.6032257,13.5143356h4.7482452l0.7219086-2.1771622h5.2069397l0.7251434,2.1771622
h4.8124237l-5.6032257-13.5143356H179.876709z M180.733139,88.7963791l1.6495819-4.8101578l1.6457672,4.8101578H180.733139z"/>
<polygon class="st2" points="202.9142761,80.4256134 200.5380859,88.6663971 198.234024,80.4256134 192.1674042,80.4256134
192.1674042,93.939949 195.9237823,93.939949 195.9237823,83.6550827 198.8237305,93.939949 202.2547913,93.939949
205.1532593,83.6550827 205.1532593,93.939949 208.9087677,93.939949 208.9087677,80.4256134 "/>
</g>
</g>
<path class="st2" d="M184.3468018,66.8016739h10.7164917l-1.0384216-2.0768509c0,0,0.3323059-1.0799561,0.3323059-1.204567
s0.0830688-1.9937706,0.0830688-1.9937706l-1.4953308-3.0737267c0,0,0.2492218-1.5368652,0.2492218-1.6614761
s0.1302032-1.4689789,0.1302032-1.4689789l-1.2516785-1.2724533c0,0,2.076828-0.8307381,2.2014465-0.955349
c0.1246033-0.1246109,0.8722687-0.4984436,0.8722687-0.4984436l-1.5784149-2.450676c0,0-2.2014465-9.262722-2.2014465-9.0965767
c0,0.1661491-1.9937592,7.3935661-2.0353088,7.5597115c-0.0415344,0.1661453-2.6998901,8.7227478-2.6998901,8.8473549
c0,0.1246109-0.3738403,4.9428864-0.3738403,4.9428864l0.1246185,1.4953346l0.166153,1.3707161L184.3468018,66.8016739z"/>
<g>
<path d="M217.7933044,66.8032532c-0.0118866-0.0402832-0.1311951-0.4059448-0.6125336-0.9156876
c-0.1967621-0.2091675-0.5996094-0.4834061-1.1573944-0.8635254c-1.2596436-0.8588715-3.1638336-2.1567421-3.4907684-3.3735237
c-0.6951447-1.8489342-0.2349854-4.1802406,0.2097015-6.435112c0.1709442-0.8650742,0.332077-1.6826324,0.4322815-2.474369
c1.2813416-8.6760368,0.7891541-13.3959808-0.7979431-20.6904831l-0.328476-1.5989666
c-0.7101288-3.4757862-1.3236847-6.4779778-2.4671173-9.6485348c-0.2685699-0.7478371-0.7049866-1.5529995-1.0902557-2.262619
c-0.1105347-0.2045174-0.2169189-0.400774-0.3134918-0.5846348c-0.390976-0.7447357-0.5113068-1.2343426-0.663147-1.8546124
l-0.0206604-0.0836678c-0.1828308-0.8573256-0.2380829-1.7131023-0.2917938-2.5404739
c-0.070755-1.1031628-0.1384125-2.144865-0.478775-3.1922493c-0.3651123-1.1770163-1.4724121-2.7134891-2.4717712-3.4282722
c-0.4896088-0.3387985-1.0876617-0.518527-1.6661072-0.6920581c-0.4870148-0.1461587-0.9477081-0.2845707-1.316452-0.5128465
l-1.125885-0.6021943c-1.5344086-0.8165259-3.1204681-1.6614568-4.7235718-2.6732054
c-0.2778473-0.1905742-1.2203979-0.5257578-1.8019409-0.1854095l-0.4023132,0.2355065l0.3811493,0.2685599
c2.2683105,1.5979323,3.5935364,3.4990273,4.1693878,5.972362c0.0351257,0.1786957,0.0702362,0.3480949,0.1043396,0.5087147
c0.2489166,1.1852798,0.3026276,1.614459-0.3295135,1.9971571c-1.3655243,0.8258219-2.7129669,1.8163948-4.1822968,4.8299475
c-0.5562439,1.1413822-0.965271,2.0203991-1.3526154,2.851902c-0.636795,1.3686237-1.1904602,2.5580349-2.2249146,4.5531254
c-3.9938049-1.7947044-7.3781738-1.9103928-10.3333588-0.3615227c-6.8007813,3.2552567-8.4921875,11.1839771-8.7132263,17.2622128
c-0.1187897,3.1700401,0.4611969,6.665966,1.0215454,10.0462074c0.641449,3.8688164,1.3050995,7.8693275,0.8831482,11.2640305
c-0.3883667,2.5533829-1.8117371,4.0185852-4.4141846,4.6032181c-0.3785553,0-0.6352539,0.1001968-0.7628174,0.2964478
c-0.0542297,0.0831604-0.1286011,0.2582397-0.0237579,0.4932251l0.0795288,0.1776581h15.6095428
c0.6729431,0,0.9766235-0.3191681,1.3278198-0.6884384c0.1518555-0.1590652,0.3269196-0.3439636,0.5861969-0.5582962
c0.6734619-0.6533203,0.5236816-1.4171677,0.3346558-2.3845024c-0.114151-0.5835991-0.2432556-1.2451897-0.2432556-2.0606804
c0-2.7945747,0.9647522-5.5540314,1.8974762-8.2220688l0.3548126-1.0194931
c1.1501617-3.3203316,1.9527435-6.0384712,2.5379028-8.6145821c0.0469971,0.2158813,0.0944977,0.4369278,0.1430511,0.6621017
c0.231369,1.078373,0.4942474,2.3003235,0.8046417,3.4732056c0.1570129,0.5913467,0.350174,1.2803078,0.6125336,1.9971581
c-0.6249237,0.2561646-1.2865143,0.519043-1.8411865,0.731823c-0.287674,0.1131096-0.4157562,0.387867-0.3134918,0.6977425
c0.0676422,0.1745644,0.2246552,0.3140068,0.3894043,0.365139c0.0666199,0.0340881,0.122406,0.059906,0.1740417,0.0836678
c0.1528931,0.070755,0.2375793,0.1094894,0.489624,0.336216c0.3248444,0.2918015,0.8051605,1.0918007,0.8051605,1.8830185
c0,0.4560356-0.1038208,0.7788239-0.20401,1.0907631c-0.130661,0.4059448-0.265976,0.8253098-0.1378937,1.4305992
c0.1409912,0.7519722,0.5123291,1.501358,0.905365,2.2951584c0.4927063,0.995739,1.0024414,2.0255623,1.0024414,3.0280151
c0,0.4198799-0.3109131,0.8702354-0.619751,1.1300163c-0.1704407,0.1430588-0.3563538,0.2649422-0.5422821,0.3873482
c-0.212265,0.1394463-0.4322815,0.2840538-0.6357574,0.4606819c-0.4348755,0.377533-0.6843262,0.9306641-0.7411346,1.6433792
c-0.0056763,0.0759201-0.0165253,0.1559677-0.0273743,0.2375717c-0.0557709,0.4209137-0.1487427,1.125885,0.5732727,1.2983856
c0.4989014,0.1218796,0.8031158-0.3098755,1.0267334-0.6228561c0.0738525-0.1038055,0.2050323-0.2881851,0.2773438-0.333107
c0.22052,0.0883102,0.351181,0.3052216,0.4999237,0.5531235c0.1926422,0.3202057,0.4322815,0.7189102,0.9363556,0.7189102
h24.4100342L217.7933044,66.8032532z"/>
<path class="st0" d="M194.9966888,38.5344658c-0.1146698-0.7964439-0.9970245-0.542347-0.7979431,0.0836067
c0.4245453,1.9584198,1.0504761,4.6263962,0.4245453,6.298893
C195.5304413,42.9569931,195.6148834,41.0582848,194.9966888,38.5344658z"/>
<path class="st0" d="M200.2460022,7.8746614c-1.5274506-2.3263369-3.4675293-4.1429906-5.3639832-5.0278177
c1.5036926,0.8848276,3.4548798,3.8626807,4.4560394,5.3981848C199.8741455,8.9776945,200.5001068,8.357295,200.2460022,7.8746614
z"/>
<g>
<g>
<path class="st0" d="M194.6893768,48.9474983c0.2262115-0.0038757,0.58078,0.0217552,0.7979431,0.0876656
c0.2881927,0.0872192,0.557785,0.2287292,0.7994843,0.4071655c0.5017395,0.370369,0.8849487,0.8952866,1.11763,1.4715958
c0.108963,0.270237,0.1817932,0.5515175,0.2230988,0.8398933c0.2122803,1.4921875-0.2016754,3.0194931,0.5941925,4.5467987
c0.8320313,1.4230461,1.7727509,2.6776581,2.3318329,3.8874092c0.7607422,1.5282707,1.4241333,3.2951546,0.9340057,5.5588608
c-0.0046387,0.0262222-0.0167847,0.0560379-0.0224609,0.0827103c1.2929688-0.1495819,2.5211182-0.4603653,3.5744324-1.0690155
c3.1537781-1.874752,4.6845703-5.6450539,2.6148376-10.1344604c-0.6499481-1.4479027-1.3675842-2.2984467-2.272171-3.8299484
c-1.7019958-2.4163971-1.3611298-6.7584839-0.5639648-9.5952797c0.6481476-2.6664276,2.1861877-5.7629356,2.2985077-5.7629356
c0.1670685-0.3703041-0.2587433-0.6251755-0.4803009-0.2540359c-0.8888397,1.419239-1.4827728,2.7237587-1.9909668,4.1389885
c-0.8242798,2.3032265-1.1669464,4.7147789-1.0504913,7.1287842c0.0550079,1.4446754,0.0410614,3.5284004-0.1712036,4.2027054
s-0.7021179,1.1333122-1.3180084,0.6309242c-0.8552551-0.6976089-1.7830811-2.0375671-2.335434-3.4676514
c-0.9125824-2.3635216-1.127182-5.1390533-1.0517883-7.3853378c0-3.7145195,1.413559-7.6934052,1.9194336-11.4087009
c0.2881927-1.1652012-0.25177-4.4886322-0.171463-6.4725533c0.0490723-1.3293743,0.0046539-3.1167183-0.0748901-4.6101322
c0.0258331-0.5437679,0.1694031-1.5550652,0.2463531-2.4612608c0.1208496-1.3021288,1.0799255-1.6486101,1.9326019-1.080761
c0.3982086,0.2899284,0.5138855,0.6554537,1.0541077,0.9971581c1.5909576,0.8513231,2.7436981-0.5565519,2.497345-2.0436373
c-0.2465973-1.4894114-1.2043915-2.766427-2.4746246-2.1139431c-0.6832733,0.3509359-0.5861816,1.3501606-1.2431183,1.856679
c-0.9120789,0.5448036-1.8770905,0.143384-2.696701-0.4276295c-0.6848297-0.4754677-0.6848297-0.9095535-1.6229858-0.4754677
c-1.5238342,0.6880569-3.1127167,2.635891-4.0307312,4.7936049c-1.0711365,2.5176849-2.2698517,4.7100639-3.4587402,7.1837215
c1.6444092,1.0210438,3.0628815,2.1821785,4.5099945,3.4875355c2.3852844,2.132019,3.693222,5.5104523,2.9546814,8.6889496
c-0.2548676,1.3921242-1.2650757,2.7194939-1.6741028,3.8029022c-0.4090424,1.0834084-0.3561096,2.2922478-0.5812836,3.1249123
c-0.2249146,0.8326645-0.382431,1.2377663-0.8325348,1.8453865c-0.4501038,0.6076126-0.8883057,0.168232-0.6011658-0.2021332
c0.2525635-0.6227875,0.2411957-1.6882477,0-3.0614548c-0.2411804-1.3732071-1.5602264-3.6093559-2.8160095-5.4275627
c-0.4508667-0.6801796-0.6793976-1.2440262-0.8506165-1.8692017c-0.0048981-0.0169144-0.0064392-0.0369263-0.0110931-0.0541649
c0.2109833,0.1408005,0.5458984-0.5922508,0.611496-0.9143295c0.0542145-0.3671417,0.4286652-1.159584,1.3014832-1.2200737
c0.7932739,0,0.6306,0.6530647,1.8468628,0.9094849c0.7359467,0.1983261,1.1620331-0.0565529,1.3363495-0.5407333
c0.4253082-1.0504818-0.2587585-3.1793365-1.0535889-3.8005123c-0.8482819-0.7988377-2.7468109-0.9087124-2.7468109,0.7342815
c0.0787659,0.4284687,0,0.6506767,0,0.6506767c-0.2835236,0.1457062-0.6610565-0.1123314-0.9128418-0.6506767
c-0.6267242-0.9883785-2.812912-2.5230446-4.0839081-2.4402161c0.0144501,0.0040646,0.0260773,0.0098095,0.0405426,0.0140057
c-0.8707581-0.1128445-1.8029785,0.0057468-2.6848297,0.3570709c-2.2130432,0.8761101-3.6315002,2.7826939-4.2329254,4.8828831
c-0.9053497,3.0606194,0.3695374,7.0387306,0,10.4705505c-0.2510071,2.5285988-1.0995483,6.8754616-2.8637695,8.8609314
c-0.4402924,0.6376381-1.0029755,0.750164-1.2759247,0.2548752s-0.4552612-1.6764984-0.4785004-2.6996117
c-0.0361481-1.5928307,0.1384125-3.295475-0.0351105-4.2260742c-0.0852203-0.3711433-0.3687744-0.2301483-0.3687744,0
c-0.4810791,2.4418297-0.1107788,6.8078041,0.8823853,10.1041832c1.0241547,3.237442,1.930542,5.620327,1.930542,7.6345329
c-0.0591431,1.9081917-2.1296387,2.9252357-1.7084656,2.9833336c2.0196228,0.0302811,4.4315033,0.1131058,6.6169128,0.0302811
c0.6241302,0,0.5430603-0.1131058,1.1690063-0.7438354c0.616394-0.7335052,0.225174-1.5307274,0.111557-2.1814003
c-0.5417633-3.9757233,1.4174194-8.6276245,2.5786896-12.2369804c1.1142578-3.3203316,2.1598511-6.7823639,2.8957977-10.8727417
c0.0271149-0.1664276,0.1998749-0.2540359,0.34552-0.2038727c0.107666,0.0374451,0.1673279,0.179985,0.1967773,0.2922516
c0.4836578,1.8772087,1.105484,4.6805611,1.7314301,7.1216164c0.2003937,0.7774086,0.4498444,1.6651382,0.7695313,2.5287895
c0.3904419-0.2019997,0.7669373-0.4209175,1.2049103-0.5260124
C194.1406555,48.9995308,194.3570404,48.9531746,194.6893768,48.9474983z"/>
<g>
<path class="st3" d="M191.9888,29.672699c0.0787659,0.4284687,0,0.6506767,0,0.6506767
c-0.2835236,0.1457062-0.6610565-0.1123314-0.9128418-0.6506767c-0.6267242-0.9883785-2.812912-2.5230446-4.0839081-2.4402161
c1.9256287,0.5423489,2.7220001,1.7059345,3.173645,3.236599c0.3718567,1.0226574,0.2541046,2.1870193,0.3718567,3.5512543
c0.111557,1.1635857,0.6794128,0,0.7669525-0.4300842c0.0542145-0.3671417,0.4286652-1.159584,1.3014832-1.2200737
c0.7932739,0,0.6306,0.6530647,1.8468628,0.9094849c0.7359467,0.1983261,1.1620331-0.0565529,1.3363495-0.5407333
c0.4253082-1.0504818-0.2587585-3.1793365-1.0535889-3.8005123C193.8873291,28.1395798,191.9888,28.029705,191.9888,29.672699z
"/>
<path class="st3" d="M194.1987457,38.6180725c0.4245453,1.9584198,1.0504761,4.6263962,0.4245453,6.298893
c0.9071503-1.9599724,0.9915924-3.8586807,0.3733978-6.3824997
C194.882019,37.7380219,193.9996643,37.9921188,194.1987457,38.6180725z"/>
<path class="st3" d="M200.2460022,7.8746614c-1.5274506-2.3263369-3.4675293-4.1429906-5.3639832-5.0278177
c1.5036926,0.8848276,3.4548798,3.8626807,4.4560394,5.3981848
C199.8741455,8.9776945,200.5001068,8.357295,200.2460022,7.8746614z"/>
<path class="st3" d="M206.1220398,13.3524475c-0.2465973-1.4894114-1.2043915-2.766427-2.4746246-2.1139431
c-0.6832733,0.3509359-0.5861816,1.3501606-1.2431183,1.856679c-0.9120789,0.5448036-1.8770905,0.143384-2.696701-0.4276295
c-0.6848297-0.4754677-0.6848297-0.9095535-1.6229858-0.4754677c-0.3444824,0.1555843-0.691803,0.3831501-1.0344696,0.6554546
c0.6171722,0.7480307,1.8210449,0.4854088,2.42659,1.6931543c0.5430603,1.0823746,0.8242645,2.4737225,0.9094849,3.5544186
c0-0.0081348,0.0103302-0.0126534,0.0113678-0.020401c-0.0023346-0.0433826-0.0036163-0.0908337-0.0059509-0.1337643
c0.0258331-0.5437679,0.1694031-1.5550652,0.2463531-2.4612608c0.1208496-1.3021288,1.0799255-1.6486101,1.9326019-1.080761
c0.3982086,0.2899284,0.5138855,0.6554537,1.0541077,0.9971581
C205.2156525,16.2474079,206.3683929,14.8395329,206.1220398,13.3524475z"/>
</g>
</g>
<path class="st0" d="M197.8086853,62.3406677c-1.0478973-1.7299156-1.87883-4.827343-1.87883-6.7657013
c0-1.1123238-0.7958679-0.7344437-1.387085-0.7344437c-0.6272278,0-1.2516022,0.0350571-1.7376556-0.1684914
c-0.382782-0.1733856-0.3439331-0.4217796,0.0748444-0.4899178c1.2449799-0.3107681,1.9773712,0.4899178,2.842392-1.2097549
c0.2747803-0.561058,0.3486786-1.189312,0.1724548-1.7396278c-0.2008667-0.6584892-0.8981934-1.0052643-1.6940765-1.0412636
c-0.5608978,0-1.5661621,0.1383247-1.6372223,0.8328209c-0.0410461,0.3827744,0.1705627,0.9019089,0.7276611,0.9749413
c0,0.2386856,0.0701141,0.5503998-0.2027588,0.6565132c-0.4235229,0.1412506-1.2857056-0.2064667-1.1540222,0
c0.5880737,0.8318787,0.3126831,1.8069,0.0729675,2.6747017c-0.2706604,0.6604614,0.0761108,1.6646957,0.5561523,2.6348228
c0.4481659,0.9418602,1.0043335,1.8447952,1.1777191,2.4263039c0.3173828,1.0509758,0.0672607,2.1584854,0.2823334,3.0263634
c0.0644226,0.4539146,0.5192108,0.6282539,0.6218567,1.1484108c0.1408539,0.3467636,0.6600647,0.2746811,0.7974396,0.2746811
c0.263092,0,0.5169983-0.2793427,0.680603-0.452652c0.3960419-0.419487,0.9427338-0.4178314,1.4891052-0.3925667
c0.5179443,0.0240059,1.0962067,0.1494713,1.4411011,0.5705376c0.2832794,0,0.4547882,0,0.4547882-0.1744156
C199.5084534,64.0091553,198.4343262,63.3867455,197.8086853,62.3406677z M194.2601166,50.7846603
c0.2826538,0,0.736496,0.4480705,0.5261536,0.6584053c-0.1408539,0.1061974-0.2434998-0.2103348-0.698288-0.2103348
c-0.2794952,0-0.5883636,0.2103348-0.5883636,0C193.4996185,50.991127,193.8084869,50.7846603,194.2601166,50.7846603z"/>
</g>
</g>
<path class="st3" d="M192.5630341,66.4706802c-0.1486969-0.2479019-0.2792969-0.4648209-0.4999695-0.5531769
c-0.0722046,0.0449142-0.2035217,0.2293167-0.2771454,0.3331528c-0.2237701,0.3129654-0.5278625,0.7447205-1.026886,0.6228256
c-0.7220764-0.1724777-0.6289063-0.8774567-0.5731354-1.298378c0.0107269-0.0816193,0.0216827-0.1616287,0.0274048-0.237587
c0.0567169-0.7127228,0.3062286-1.2658463,0.7411499-1.6433868c0.2035217-0.1765862,0.4234619-0.3212395,0.6355743-0.4606514
c0.1861115-0.1224289,0.371994-0.2442665,0.5423889-0.3873749c0.3088379-0.2597542,0.6198425-0.7101021,0.6198425-1.1300049
c0-1.002449-0.5099792-2.0322418-1.0025787-3.0280228c-0.3929749-0.7938042-0.7642517-1.5431709-0.9053345-2.2951622
c-0.1282043-0.6052437,0.0071564-1.0246124,0.1377411-1.4305725c0.1003265-0.3119469,0.20401-0.6347351,0.20401-1.0908012
c0-0.7911873-0.4801941-1.5911942-0.8050232-1.8830032c-0.2521362-0.2266922-0.336731-0.2654152-0.4894867-0.3361931
c-0.051712-0.023777-0.1074829-0.0495682-0.1742096-0.0836525c-0.1646729-0.0511742-0.3217163-0.1905823-0.389389-0.3651466
c-0.1022491-0.309864,0.0259705-0.5846329,0.3133698-0.697773c0.5547791-0.2127495,1.2163391-0.475666,1.8411865-0.7317886
c-0.262146-0.7168961-0.4554138-1.4058495-0.6124573-1.9971542c-0.3102875-1.1729012-0.5731354-2.3948326-0.8045349-3.4732437
c-0.0486145-0.2251434-0.0960388-0.4461746-0.1429749-0.6620827c-0.5852966,2.5761261-1.3879242,5.2942276-2.5380096,8.6145973
l-0.3548431,1.0194893c-0.9327393,2.6679955-1.8974152,5.4275055-1.8974152,8.2220421
c0,0.8154945,0.1291504,1.4771042,0.2433014,2.0606651c0.1889954,0.9673615,0.3386536,1.7312012-0.3348083,2.3845215
c-0.2592926,0.2143631-0.4342041,0.399231-0.5860138,0.5583038c-0.3512726,0.3692627-0.6548767,0.6884155-1.3278503,0.6884155
h3.1204224h2.2958679h4.9563751C192.9953308,67.189537,192.7558289,66.7908478,192.5630341,66.4706802z"/>
<path class="st0" d="M219.4322205,66.6828918l-0.1151123-0.3818283c-0.0891113-0.2998581-0.3398132-0.902832-1.0187531-1.6218185
c-0.3281555-0.3483505-0.7676086-0.648201-1.4487-1.1126099c-0.6303253-0.4299088-2.5477753-1.737278-2.7612915-2.3524742
c-0.0145416-0.055645-0.0326538-0.1103401-0.0529022-0.1645546c-0.5147552-1.3690872-0.1060486-3.43964,0.2895508-5.4418602
c0.1677704-0.8505249,0.3412628-1.7302475,0.4451599-2.5509872c1.3171387-8.9211807,0.8126373-13.7676277-0.8128815-21.2362537
l-0.3267212-1.5916119c-0.7213593-3.5310955-1.3440704-6.5804462-2.5270386-9.8597679
c-0.3148041-0.8783493-0.8047791-1.7797565-1.1946411-2.4972458l-0.3040924-0.5671787
c-0.2890625-0.5501976-0.366745-0.8674469-0.5161591-1.4746599c-0.1551514-0.7279167-0.2037659-1.4799032-0.2550049-2.2752028
c-0.074585-1.159317-0.1517944-2.3581934-0.5535889-3.594306c-0.4770966-1.5378723-1.82164-3.4024076-3.1499786-4.3523693
c-0.7206573-0.4983063-1.5197144-0.7382836-2.1617126-0.9306593c-0.3531647-0.1060476-0.7185059-0.2159085-0.8862762-0.319633
c-0.0321655-0.0197797-0.0645752-0.0386658-0.097702-0.0560622l-1.1252899-0.60203
c-1.4968414-0.7967885-3.0451202-1.6203868-4.5362396-2.5599825c-0.7354126-0.5044421-2.3897705-1.0202039-3.7021332-0.2531444
l-0.4008331,0.2347947c-0.5219116,0.30593-0.8529205,0.8566043-0.8779449,1.4609576
c-0.0252533,0.6048896,0.2588043,1.1810033,0.7540283,1.5289354l0.3784332,0.2668476
c1.9078979,1.3440671,2.9712372,2.8554869,3.4349823,4.8435855l0.1079559,0.5261288
c0.0178833,0.0862675,0.0471954,0.2244272,0.0736389,0.3621111c-1.4115143,0.901823-2.9040375,2.1710014-4.4277954,5.2966719
c-0.5540771,1.1361418-0.9627686,2.0140142-1.3621674,2.8710966c-0.4563751,0.9805832-0.8674469,1.8640575-1.4486847,3.0266533
c-3.8479919-1.386488-7.2172089-1.2799625-10.221344,0.2960987c-5.9996796,2.8720493-9.3512878,9.3530006-9.6925354,18.7408524
c-0.1248779,3.3406906,0.4677887,6.913723,1.0404663,10.3689766c0.6186523,3.7305603,1.258255,7.5874252,0.8764954,10.6664467
c-0.2688141,1.7678986-1.0804901,2.6433868-2.8783112,3.083725c-1.1743774,0.0759048-1.7615814,0.7095108-2.0130005,1.0956268
c-0.4217987,0.6477356-0.480423,1.4662018-0.1570435,2.1879807l0.079361,0.1772537
c0.2862091,0.6406937,0.9220123,1.0531998,1.6240692,1.0531998h10.1906128h5.3653107h10.3369141h13.4566345h10.8688202
c0.5624237,0,1.0924072-0.2658844,1.4279633-0.7175446C219.4925232,67.8053894,219.5942841,67.2217712,219.4322205,66.6828918z
M206.8599548,67.189537h-13.3603516h-4.9563751h-2.2958679h-3.1204224h-5.4258423h-10.1839294l-0.079361-0.1776581
c-0.1048431-0.2349701-0.0305023-0.4100723,0.0235901-0.493187c0.1277466-0.1962433,0.3843994-0.2964554,0.7630768-0.2964554
c2.6023407-0.5846252,4.0257568-2.0498199,4.4139709-4.6032486c0.4220428-3.3946609-0.2416534-7.3951645-0.8831787-11.2640076
c-0.5602722-3.3802452-1.1400757-6.8761864-1.0213928-10.0461807c0.2211456-6.07827,1.9124298-14.0069523,8.7130737-17.2622643
c2.9552765-1.5488338,6.3397522-1.4331341,10.3336029,0.3615742c1.0342407-1.9951267,1.588089-3.1845303,2.2248535-4.5531425
c0.3872528-0.831522,0.7964172-1.7105274,1.3526459-2.8519135c1.469162-3.013546,2.8168182-4.0041389,4.1823273-4.8299417
c0.6319885-0.3827257,0.5783691-0.811862,0.3293457-1.9971542c-0.0340881-0.1606207-0.0691071-0.3299999-0.1043854-0.5087328
c-0.5757599-2.4732971-1.9009857-4.3744125-4.1692352-5.9723387l-0.3812866-0.2685752l0.4024963-0.2355096
c0.5814972-0.3403656,1.5240021-0.0051832,1.8018646,0.185405c1.6031189,1.011744,3.1890717,1.8566711,4.7235413,2.6732395
l1.1257782,0.602149c0.3688965,0.2283006,0.8295593,0.3666987,1.3164215,0.5128427
c0.5786285,0.1735487,1.1767731,0.3532939,1.6662598,0.6921105c0.9994812,0.7147508,2.1066742,2.2512527,2.4717407,3.4282641
c0.3403168,1.0473719,0.4080048,2.0890827,0.478775,3.1922188c0.0536194,0.8274117,0.108902,1.6831808,0.2919312,2.5405016
l0.0204926,0.0836468c0.1518097,0.6202583,0.2721405,1.1098671,0.6632233,1.8546448
c0.0965118,0.1838551,0.2028046,0.3801041,0.3133698,0.5846329c0.3853455,0.709568,0.821701,1.5147552,1.090271,2.2625732
c1.1436462,3.1705894,1.7570648,6.1727562,2.4672089,9.6485634l0.3283997,1.5989399
c1.5871429,7.2945404,2.0792542,12.0144424,0.7978668,20.6905251c-0.1000977,0.7917252-0.2612,1.6092491-0.4320679,2.4743729
c-0.4446869,2.2548256-0.9048615,4.5861511-0.2097168,6.4350777c0.3267212,1.2168083,2.2310638,2.5146446,3.4907684,3.3735085
c0.5576477,0.3801651,0.9606323,0.6544037,1.1574707,0.8635788c0.4811401,0.5097427,0.6005402,0.8753738,0.6124573,0.9156418
l0.1160583,0.3862991H206.8599548z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 39 KiB

3
src/assets/GhIcon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0001 0.296387C5.3735 0.296387 0 5.66889 0 12.2965C0 17.5984 3.43839 22.0966 8.2064 23.6833C8.80613 23.7944 9.0263 23.423 9.0263 23.1061C9.0263 22.8199 9.01518 21.8746 9.01001 20.8719C5.67157 21.5978 4.96712 19.456 4.96712 19.456C4.42125 18.069 3.63473 17.7002 3.63473 17.7002C2.54596 16.9554 3.7168 16.9707 3.7168 16.9707C4.92181 17.0554 5.55632 18.2073 5.55632 18.2073C6.6266 20.0419 8.36359 19.5115 9.04836 19.2049C9.15607 18.4293 9.46706 17.8999 9.81024 17.6002C7.14486 17.2968 4.34295 16.2678 4.34295 11.6697C4.34295 10.3596 4.81172 9.28911 5.57937 8.44874C5.45477 8.14649 5.04402 6.92597 5.69562 5.27305C5.69562 5.27305 6.70331 4.95053 8.9965 6.5031C9.95372 6.23722 10.9803 6.10388 12.0001 6.09931C13.0199 6.10388 14.0473 6.23722 15.0063 6.5031C17.2967 4.95053 18.303 5.27305 18.303 5.27305C18.9562 6.92597 18.5452 8.14649 18.4206 8.44874C19.1901 9.28911 19.6557 10.3596 19.6557 11.6697C19.6557 16.2788 16.8484 17.2936 14.1762 17.5907C14.6067 17.9631 14.9902 18.6934 14.9902 19.8129C14.9902 21.4186 14.9763 22.7108 14.9763 23.1061C14.9763 23.4254 15.1923 23.7996 15.8006 23.6818C20.566 22.0932 24 17.5967 24 12.2965C24 5.66889 18.6273 0.296387 12.0001 0.296387ZM4.49443 17.3908C4.468 17.4504 4.3742 17.4683 4.28876 17.4273C4.20172 17.3882 4.15283 17.3069 4.18105 17.2471C4.20688 17.1857 4.30088 17.1686 4.38772 17.2098C4.47495 17.2489 4.52463 17.331 4.49443 17.3908ZM5.0847 17.9175C5.02747 17.9705 4.91559 17.9459 4.83968 17.862C4.76119 17.7784 4.74648 17.6665 4.80451 17.6126C4.86353 17.5596 4.97203 17.5844 5.05072 17.6681C5.12921 17.7527 5.14451 17.8638 5.0847 17.9175ZM5.48965 18.5914C5.41612 18.6424 5.2959 18.5945 5.22158 18.4878C5.14805 18.3811 5.14805 18.2531 5.22317 18.2019C5.29769 18.1506 5.41612 18.1967 5.49144 18.3026C5.56476 18.4111 5.56476 18.5391 5.48965 18.5914ZM6.1745 19.3718C6.10873 19.4443 5.96863 19.4249 5.86609 19.3259C5.76117 19.2291 5.73196 19.0918 5.79793 19.0193C5.8645 18.9466 6.00539 18.967 6.10873 19.0652C6.21285 19.1618 6.24465 19.3001 6.1745 19.3718ZM7.05961 19.6353C7.0306 19.7293 6.89567 19.772 6.75975 19.7321C6.62402 19.6909 6.5352 19.5808 6.56262 19.4858C6.59084 19.3913 6.72636 19.3467 6.86328 19.3895C6.9988 19.4304 7.08783 19.5397 7.05961 19.6353ZM8.0669 19.747C8.07028 19.846 7.95502 19.9281 7.81235 19.9299C7.66887 19.933 7.55282 19.853 7.55123 19.7556C7.55123 19.6556 7.6639 19.5744 7.80738 19.572C7.95006 19.5692 8.0669 19.6487 8.0669 19.747ZM9.05645 19.7091C9.07354 19.8057 8.97438 19.9048 8.8327 19.9313C8.6934 19.9567 8.56443 19.8971 8.54674 19.8013C8.52945 19.7024 8.6304 19.6032 8.7695 19.5776C8.91139 19.5529 9.03837 19.6109 9.05645 19.7091Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

@ -0,0 +1,93 @@
Copyright © 2009 ParaType Ltd. All rights reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

@ -0,0 +1,15 @@
<svg width="235" height="240" viewBox="0 0 235 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="34.8711" y="16.2017" width="36" height="249.944" rx="18" transform="rotate(-26.7465 34.8711 16.2017)" fill="#F15527" fill-opacity="0.2"/>
<rect x="156.871" y="36.2017" width="36" height="100.235" rx="18" transform="rotate(-26.7465 156.871 36.2017)" fill="#F15527" fill-opacity="0.2"/>
<rect x="0.871094" y="138.037" width="26.745" height="74.4663" rx="13.3725" transform="rotate(-26.7465 0.871094 138.037)" fill="#F15527" fill-opacity="0.2"/>
<g clip-path="url(#clip0_2865_33046)">
<path d="M117.5 199C161.225 199 197 163.225 197 119.5C197 75.775 161.225 40 117.5 40C73.775 40 38 75.775 38 119.5C38 163.225 73.775 199 117.5 199ZM119.885 82.0555C121.475 80.6245 123.383 79.75 125.45 79.75C127.596 79.75 129.425 80.6245 131.094 82.0555C132.605 83.725 133.4 85.633 133.4 87.7C133.4 89.8465 132.605 91.675 131.094 93.3445C129.425 94.855 127.596 95.65 125.45 95.65C123.383 95.65 121.475 94.855 119.885 93.3445C118.375 91.675 117.5 89.8465 117.5 87.7C117.5 85.633 118.375 83.725 119.885 82.0555ZM100.01 119.261C100.01 119.261 117.261 105.588 123.542 105.031C129.425 104.554 128.232 111.311 127.676 114.809L127.597 115.286C126.484 119.5 125.132 124.588 123.78 129.438C120.759 140.488 117.818 151.3 118.534 153.287C119.328 155.991 124.257 152.572 127.835 150.187C128.312 149.869 128.709 149.551 129.107 149.312C129.107 149.312 129.743 148.676 130.379 149.551C130.538 149.789 130.697 150.028 130.856 150.187C131.572 151.3 131.969 151.697 131.015 152.333L130.697 152.492C128.948 153.685 121.475 158.932 118.454 160.84C115.194 162.987 102.713 170.142 104.621 156.229C106.291 146.451 108.517 138.023 110.266 131.425C113.525 119.5 114.956 114.094 107.642 118.785C104.701 120.534 102.951 121.646 101.918 122.362C101.043 122.998 100.964 122.998 100.408 121.965L100.169 121.487L99.7715 120.852C99.215 120.057 99.215 119.977 100.01 119.261Z" fill="#0F2139"/>
<path d="M119.885 82.0555C121.475 80.6245 123.383 79.75 125.45 79.75C127.596 79.75 129.425 80.6245 131.094 82.0555C132.605 83.725 133.4 85.633 133.4 87.7C133.4 89.8465 132.605 91.675 131.094 93.3445C129.425 94.855 127.596 95.65 125.45 95.65C123.383 95.65 121.475 94.855 119.885 93.3445C118.375 91.675 117.5 89.8465 117.5 87.7C117.5 85.633 118.375 83.725 119.885 82.0555Z" fill="white"/>
<path d="M100.01 119.261C100.01 119.261 117.261 105.588 123.542 105.031C129.425 104.554 128.232 111.311 127.676 114.809L127.597 115.286C126.484 119.5 125.132 124.588 123.78 129.438C120.759 140.488 117.818 151.3 118.534 153.287C119.328 155.991 124.257 152.572 127.835 150.187C128.312 149.869 128.709 149.551 129.107 149.312C129.107 149.312 129.743 148.676 130.379 149.551C130.538 149.789 130.697 150.028 130.856 150.187C131.572 151.3 131.969 151.697 131.015 152.333L130.697 152.492C128.948 153.685 121.475 158.932 118.454 160.84C115.194 162.987 102.713 170.142 104.621 156.229C106.291 146.451 108.517 138.023 110.266 131.425C113.525 119.5 114.956 114.094 107.642 118.785C104.701 120.534 102.951 121.646 101.918 122.362C101.043 122.998 100.964 122.998 100.408 121.965L100.169 121.487L99.7715 120.852C99.215 120.057 99.215 119.977 100.01 119.261Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_2865_33046">
<rect width="159" height="159" fill="white" transform="translate(38 40)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
src/assets/repocube-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
src/assets/repocube-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
src/assets/repocube-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

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