Compare commits

...

23 Commits

Author SHA1 Message Date
github-actions[bot]
cd7947adbc chore: update versions (6-next) (#2450)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2021-09-25 17:44:58 +02:00
Juan Picado
5fed1955a9 refactor: rematch as web state storage for UI (#2447)
* trying rematch

refactor: rematch for store packages

migrate login to rematch

Update packages/plugins/ui-theme/src/store/store.ts

Co-authored-by: Sergio Moreno <sergiomorenoalbert@gmail.com>

hide temporary

fix test for login

migrate package download resource

fix tests

* add missing fixture

* migrate detail page support

* fix tests

* migrate search

* migrate search

* clean up tests

* remove tags

* fix lint

* add changeset

* fix: search model typings

* add type

* types

* apply suggestions

Co-authored-by: Sergio Moreno <22656541+semoal@users.noreply.github.com>
2021-09-25 17:35:03 +02:00
Juan Picado
9dbf73e955 remove typedoc dep, not need it now to fix the changesets 2021-09-19 21:34:38 +02:00
Zameer Haque
2c594910d8 fix: do not throw multiple logger deprecation warning if using default logger config (#2446) 2021-09-19 20:20:46 +02:00
Juan Picado
90818700a3 rename translation file (#2445)
avoid ui translations are being uploaded
2021-09-18 12:45:27 +02:00
Juan Picado
7af1e6cc84 update readme 2021-09-18 11:46:15 +02:00
Juan Picado
757bded72e improve ci syn translations 2021-09-18 10:01:22 +02:00
Juan Picado
1b9bf35c87 add steps to add new languages to ui contributing guidelines 2021-09-18 09:43:14 +02:00
Juan Picado
eb2afc4d6d sync translations on merge in master 2021-09-18 09:20:24 +02:00
Juan Picado
5be013a059 update contributing guidelines and chat reference 2021-09-18 09:07:22 +02:00
Juan Picado
8d6d6097c6 refactor: using ui translations from crowdin (#2442)
* refactor: using ui translations from crowdin

* move tasks

* fix tests
2021-09-17 21:19:50 +02:00
Juan Picado
531289f59d update ui crowdin assets 2021-09-17 07:41:11 +02:00
Juan Picado
5e784d1188 refactor: one single file for i18n conf ui (#2440)
* refactor: one single file for i18n conf ui

* add test for crowdin

* cleanup

* space
2021-09-17 07:17:34 +02:00
Juan Picado
7dde848d0c refactor: replace flag icons with library (#2439)
* feat: simplify i18n translations config and flags

* fix icons

* lint

* update test
2021-09-16 21:46:56 +02:00
Justin Johansson
71874de027 build: upgrade to husky@7.0.2 & configure for lint-staged (#2436) 2021-09-16 07:03:57 +02:00
Joon du Randt
f13dacef9c Updated config map param name in helm example (#2435) 2021-09-14 20:38:50 +02:00
Juan Picado
761f1696f2 update e2e page docs 2021-09-13 22:14:09 +02:00
Juan Picado
f412e8f8d6 Update README.md 2021-09-13 20:56:55 +02:00
rvo
f5fd7bf5bf docs(docker): spelling and minor grammar fix (#2434) 2021-09-12 21:23:02 +02:00
Juan Picado
3d26a8190c eslint deps (#2433)
* eslint deps

* Update pnpm-lock.yaml
2021-09-12 20:33:24 +02:00
Juan Picado
e50410a875 feat: fastify search endpoint v1 without auth and format (#2432) 2021-09-12 17:12:47 +02:00
Juan Picado
60d7b35d88 refactor: search logic to storage pkg (#2431)
* refactor: search logic to storage pkg

* add test

* update undici

* add tests

* Update ci.yml

* fix ci

* fix tests

* fix ci

* fix tests

* fix ci

* restore some deps

* Update Version.test.tsx

* disable debug
2021-09-12 16:21:19 +02:00
Juan Picado
1042f9bf76 update benchmark with v6.0.0-6-next.23 2021-09-08 20:32:38 +02:00
163 changed files with 2589 additions and 2763 deletions

View File

@@ -0,0 +1,5 @@
---
'@verdaccio/ui-theme': minor
---
feat: integrate rematch for ui state storage

View File

@@ -66,6 +66,7 @@
"many-vans-care",
"modern-spies-tell",
"neat-toes-report",
"olive-candles-speak",
"perfect-emus-clean",
"perfect-kangaroos-agree",
"plenty-news-remember",
@@ -74,6 +75,7 @@
"pretty-hounds-tap",
"proud-jeans-walk",
"red-chefs-float",
"shaggy-carrots-unite",
"shiny-chefs-heal",
"smart-apricots-kneel",
"spicy-frogs-press",

View File

@@ -0,0 +1,5 @@
---
'@verdaccio/logger': patch
---
do not show deprecation warning on default logger config

View File

@@ -7,7 +7,7 @@ contact_links:
url: https://github.com/verdaccio/verdaccio/security/policy
about: I want to report a security vulnerability
- name: Chat 🏘
url: http://chat.verdaccio.org
url: https://discord.gg/7qWJxBf
about: For a quick question you should do it through our community chat
- name: User Interface Report 👩🏼‍🎨👨🏼‍🎨
url: https://github.com/verdaccio/ui/issues/new/choose

View File

@@ -58,7 +58,7 @@ jobs:
- 3.13.1
- 4.12.2
- 5.1.3
- 6.0.0-6-next.22
- 6.0.0-6-next.23
name: Benchmark autocannon
runs-on: ubuntu-latest
steps:
@@ -119,7 +119,7 @@ jobs:
- 3.13.1
- 4.12.2
- 5.1.3
- 6.0.0-6-next.22
- 6.0.0-6-next.23
name: Benchmark hyperfine
runs-on: ubuntu-latest
steps:

View File

@@ -83,6 +83,7 @@ jobs:
run: pnpm recursive install --frozen-lockfile --ignore-scripts
- name: Lint
run: pnpm format:check
build:
runs-on: ubuntu-latest
name: build
@@ -101,6 +102,12 @@ jobs:
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install
run: pnpm recursive install --frozen-lockfile --ignore-scripts
- name: crowdin download
env:
CROWDIN_VERDACCIO_PROJECT_ID: ${{ secrets.CROWDIN_VERDACCIO_PROJECT_ID }}
CROWDIN_VERDACCIO_API_KEY: ${{ secrets.CROWDIN_VERDACCIO_API_KEY }}
CONTEXT: production
run: pnpm crowdin:download
- name: build
run: pnpm build
- name: tar packages
@@ -116,6 +123,7 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest]
## Node 16 breaks UI test, jest issue
node_version: [14]
name: ${{ matrix.os }} / Node ${{ matrix.node_version }}
runs-on: ${{ matrix.os }}
@@ -165,8 +173,8 @@ jobs:
run: pnpm recursive install --frozen-lockfile
- name: Test UI
run: pnpm test:e2e:ui
env:
DEBUG: verdaccio:e2e*
# env:
# DEBUG: verdaccio:e2e*
ci-e2e-cli:
needs: build
runs-on: ubuntu-latest
@@ -192,8 +200,8 @@ jobs:
run: pnpm recursive install --frozen-lockfile
- name: Test CLI
run: pnpm test:e2e:cli
env:
DEBUG: verdaccio*
# env:
# DEBUG: verdaccio*
test-windows:
needs: [format, lint]
runs-on: windows-latest
@@ -217,3 +225,35 @@ jobs:
run: pnpm build
- name: Test
run: pnpm test
sync-translations:
needs: [ci-e2e-cli, ci-e2e-ui, test-windows, test]
runs-on: ubuntu-latest
name: synchronize translations
if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v2.3.1
- uses: actions/setup-node@v1
with:
node-version: 14
- uses: actions/download-artifact@v2
with:
name: verdaccio-artifact
- name: untar packages
run: tar -xzvf pkg.tar.gz -C ${{ github.workspace }}/packages
- name: Install pnpm
run: npm i pnpm@6.10.3 -g
- uses: actions/cache@v2
with:
path: ~/.pnpm-store
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install
## we need scripts, pupetter downloads aditional content
run: pnpm recursive install --frozen-lockfile
- name: generate website translations
run: pnpm write-translations --filter ...@verdaccio/website
- name: sync
env:
CROWDIN_VERDACCIO_PROJECT_ID: ${{ secrets.CROWDIN_VERDACCIO_PROJECT_ID }}
CROWDIN_VERDACCIO_API_KEY: ${{ secrets.CROWDIN_VERDACCIO_API_KEY }}
CONTEXT: production
run: pnpm crowdin:sync

5
.gitignore vendored
View File

@@ -34,6 +34,9 @@ packages/standalone/dist/
## ui
packages/plugins/ui-theme/static
/packages/plugins/ui-theme/src/i18n/download_translations/
!/packages/plugins/ui-theme/src/i18n/crowdin/ui.json
# CI Pnpm cache
.pnpm-store/
@@ -45,5 +48,5 @@ hyper-results*.json
api-results*.json
#docs
api/
./api
packages/core/core/docs

5
.husky/pre-commit Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
#./node_modules/.bin/lint-staged
npm run husky:pre-commit

2
.nvmrc
View File

@@ -1 +1 @@
14
16

View File

@@ -19,6 +19,7 @@ guidelines for you:
- [What's is not considered a bug?](#whats-is-not-considered-a-bug)
- [Issue Search](#issue-search)
- [Chat](#chat)
- [Translations](#translations)
- [Request Features](#request-features)
- [Contributing Guidelines](#contributing-guidelines)
- [Submitting a Pull Request](#submitting-a-pull-request)
@@ -190,10 +191,28 @@ affecting multiple people.
### Chat
Questions can be asked via [Discord](http://chat.verdaccio.org/)
Questions can be asked via [Discord](https://discord.gg/7qWJxBf)
**Please use the `#help` channel.**
## Translations
All translations are provided by the `crowdin` platform:
[https://translate.verdaccio.org/](https://translate.verdaccio.org/)
If you want to contribute by adding translations, create an account (GitHub could be used as fast alternative), in the platform you can contribute to two areas, the website or improve User Interface translations.
If a language is not listed, ask for it in the [Discord](https://discord.gg/7qWJxBf) channel #contribute channel.
For adding a new **language** on the UI follow these steps:
1. Ensure the **language** has been enabled, must be visible in the `crowdin` platform.
2. Find in the explorer the file `en.US.json` in the path `packages/plugins/ui-theme/src/i18n/crowdin/ui.json` and complete the translations, **not need to find approval on this**.
3. Into the project, add a new field into `packages/plugins/ui-theme/src/i18n/crowdin/ui.json` file, in the section `lng`, the new language, eg: `{ lng: {korean:"Korean"}}`. (This file is English based, once the PR has been merged, this string will be available in crowdin for translate to the targeted language).
4. Add the language, [flag icon](https://www.npmjs.com/package/country-flag-icons), and the menu key fort he new language eg: `menuKey: 'lng.korean'` to the file `packages/plugins/ui-theme/src/i18n/enabledLanguages.ts`.
5. For local testing, read `packages/plugins/ui-theme/src/i18n/ABOUT_TRANSLATIONS.md`.
6. Add a `changeset` file, see more info below.
## Request Features
New feature requests are welcome. Analyse whether the idea fits within scope of

View File

@@ -75,7 +75,7 @@ booted in a couple of seconds, fast enough for any CI. Many open source projects
### **Testing the integrity of your React components by publishing in a private registry - React Finland 2021**.
[![beerjscrb](https://cdn.verdaccio.dev/readme/react-finland-2021-jpicado.jpeg)](https://www.youtube.com/watch?v=5olfi5wbgF4)
[![beerjscrb](https://cdn.verdaccio.dev/readme/react-finland-2021-jpicado.jpeg)](https://www.youtube.com/watch?v=bRKZbrlQqLY&t=16s&ab_channel=ReactFinland)
You might want to check out as well our previous talks:
@@ -190,7 +190,20 @@ Verdaccio aims to support all features of a standard npm client that make sense
If you want to report a security vulnerability, please follow the steps which we have defined for you in our [security policy](https://github.com/verdaccio/verdaccio/security/policy).
## Core Team
## Special Thanks
Thanks to the following companies to help us to achieve our goals providing free open source licenses. Every company provides enough resources to move this project forward.
| Company | Logo | License |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| JetBrains | [![jetbrain](assets/thanks/jetbrains/logo.png)](https://www.jetbrains.com/) | JetBrains provides licenses for products for active maintainers, renewable yearly |
| Crowdin | [![crowdin](assets/thanks/crowdin/logo.png)](https://crowdin.com/) | Crowdin provides platform for translations |
| BrowserStack | [![browserstack](https://cdn.verdaccio.dev/readme/browserstack_logo.png)](https://www.browserstack.com/) | BrowserStack provides plan to run End to End testing for the UI |
| Netlify | [![netlify](https://www.netlify.com/img/global/badges/netlify-color-accent.svg)](https://www.netlify.com/) | Netlify provides pro plan for website deployment |
| Algolia | [![algolia](https://cdn.verdaccio.dev/sponsor/logo/algolia/logo.png)](https://algolia.com/) | Algolia provides search services for the website |
| Docker | [![docker](https://cdn.verdaccio.dev/sponsor/logo/docker/docker.png)](https://www.docker.com/community/open-source/application) | Docker offers unlimited pulls and unlimited egress to any and all users |
## Maintainers
| [Juan Picado](https://github.com/juanpicado) | [Ayush Sharma](https://github.com/ayusharma) | [Sergio Hg](https://github.com/sergiohgz) |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
@@ -244,18 +257,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com
[![backers](https://opencollective.com/verdaccio/backers.svg?width=890)](https://opencollective.com/verdaccio#backers)
## Special Thanks
Thanks to the following companies to help us to achieve our goals providing free open source licenses.
[![jetbrain](assets/thanks/jetbrains/logo.png)](https://www.jetbrains.com/)
[![crowdin](assets/thanks/crowdin/logo.png)](https://crowdin.com/)
[![browserstack](https://cdn.verdaccio.dev/readme/browserstack_logo.png)](https://www.browserstack.com/)
[![netlify](https://www.netlify.com/img/global/badges/netlify-color-accent.svg)](https://www.netlify.com/)
[![algolia](https://cdn.verdaccio.dev/sponsor/logo/algolia/logo.png)](https://algolia.com/)
Verdaccio also is part of to the [Docker Open Source Program](https://www.docker.com/blog/expanded-support-for-open-source-software-projects/).
## Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
@@ -270,7 +271,7 @@ If you have any issue you can try the following options, do no desist to ask or
- [Donations](https://github.com/sponsors/verdaccio)
- [Reporting an issue](https://github.com/verdaccio/verdaccio/issues/new/choose)
- [Running discussions](https://github.com/verdaccio/verdaccio/issues?q=is%3Aissue+is%3Aopen+label%3Adiscuss)
- [Chat](http://chat.verdaccio.org/)
- [Chat](https://discord.gg/7qWJxBf)
- [Logos](https://verdaccio.org/docs/en/logo)
- [Docker Examples](https://github.com/verdaccio/verdaccio/tree/master/docker-examples)
- [FAQ](https://github.com/verdaccio/verdaccio/discussions/categories/q-a)

View File

@@ -5,6 +5,10 @@ preserve_hierarchy: true
files:
[
{
source: 'packages/plugins/ui-theme/src/i18n/crowdin/**/*',
translation: '/packages/plugins/ui-theme/src/i18n/download_translations/%locale%/**/%original_file_name%',
},
{
source: '/website/i18n/en/**/*',
translation: '/website/i18n/%locale%/**/%original_file_name%',
@@ -13,4 +17,4 @@ files:
source: '/website/docs/**/*',
translation: '/website/i18n/%locale%/docusaurus-plugin-content-docs/current/**/%original_file_name%',
}
]
]

View File

@@ -52,12 +52,6 @@
"@types/mime": "2.0.3",
"@types/minimatch": "3.0.5",
"@types/node": "14.6.0",
"@types/react": "17.0.19",
"@types/react-autosuggest": "10.1.5",
"@types/react-dom": "17.0.9",
"@types/react-helmet": "6.1.2",
"@types/react-router-dom": "5.1.8",
"@types/react-virtualized": "9.21.13",
"@types/request": "2.48.7",
"@types/semver": "7.3.8",
"@types/supertest": "2.0.11",
@@ -84,25 +78,13 @@
"debug": "4.3.2",
"detect-secrets": "1.0.6",
"eslint": "7.32.0",
"eslint-config-google": "0.14.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-babel": "5.3.1",
"eslint-plugin-import": "2.24.2",
"eslint-plugin-jest": "24.4.0",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "7.25.1",
"eslint-plugin-react-hooks": "4.2.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"eslint-plugin-verdaccio": "10.0.0",
"fs-extra": "10.0.0",
"husky": "4.3.5",
"husky": "7.0.2",
"in-publish": "2.0.1",
"jest": "27.1.0",
"jest-environment-jsdom": "27.1.0",
"jest-environment-jsdom-global": "3.0.0",
"jest-environment-node": "27.1.0",
"jest-fetch-mock": "3.0.3",
"jest-junit": "12.2.0",
"kleur": "3.0.3",
"lint-staged": "11.1.2",
@@ -124,6 +106,7 @@
"verdaccio-memory": "workspace:*"
},
"scripts": {
"husky:pre-commit": "lint-staged",
"clean": "pnpm recursive run clean",
"build": "pnpm recursive run build --filter=!@verdaccio/website",
"docker": "docker build -t verdaccio/verdaccio:local . --no-cache",
@@ -139,7 +122,7 @@
"benchmark:submit": "pnpm ts-node ./scripts/submit-metrics.ts",
"start:watch": "concurrently --kill-others \"pnpm _build:watch\" \"pnpm _start:server\" \"pnpm _debug:reload\"",
"_build:watch": "pnpm run --parallel watch --filter ./packages",
"_start:server": "node packages/verdaccio/debug/bootstrap.js --listen 8000",
"_start:server": "node --inspect packages/verdaccio/debug/bootstrap.js --listen 8000",
"_start:web": "pnpm start --filter ...@verdaccio/ui-theme",
"_debug:reload": "nodemon -d 3 packages/verdaccio/debug/bootstrap.js",
"start:ts": "ts-node packages/verdaccio/src/start.ts -- --listen 8000",
@@ -154,15 +137,11 @@
"ts:ref": "update-ts-references --discardComments",
"website": "pnpm build --filter ...@verdaccio/website",
"crowdin:upload": "crowdin upload sources --auto-update --config ./crowdin.yaml",
"crowdin:download": "crowdin download --config ./crowdin.yaml",
"crowdin:sync": "pnpm crowdin:upload && pnpm crowdin:download --verbose"
"crowdin:download": "crowdin download --verbose --config ./crowdin.yaml",
"crowdin:sync": "pnpm crowdin:upload && pnpm crowdin:download --verbose",
"postinstall": "husky install"
},
"license": "MIT",
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,yml,yaml,md}": "prettier --write",
"*.{js,jsx,ts,tsx}": "eslint --cache --fix"

View File

@@ -1,5 +1,16 @@
# @verdaccio/api
## 6.0.0-6-next.15
### Patch Changes
- Updated dependencies [2c594910]
- @verdaccio/logger@6.0.0-6-next.5
- @verdaccio/auth@6.0.0-6-next.12
- @verdaccio/hooks@6.0.0-6-next.7
- @verdaccio/middleware@6.0.0-6-next.12
- @verdaccio/store@6.0.0-6-next.13
## 6.0.0-6-next.14
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/api",
"version": "6.0.0-6-next.14",
"version": "6.0.0-6-next.15",
"description": "loaders logic",
"main": "./build/index.js",
"types": "build/index.d.ts",
@@ -39,14 +39,14 @@
},
"license": "MIT",
"dependencies": {
"@verdaccio/auth": "workspace:6.0.0-6-next.11",
"@verdaccio/auth": "workspace:6.0.0-6-next.12",
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
"@verdaccio/config": "workspace:6.0.0-6-next.8",
"@verdaccio/core": "workspace:6.0.0-6-next.1",
"@verdaccio/hooks": "workspace:6.0.0-6-next.6",
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
"@verdaccio/middleware": "workspace:6.0.0-6-next.11",
"@verdaccio/store": "workspace:6.0.0-6-next.12",
"@verdaccio/hooks": "workspace:6.0.0-6-next.7",
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
"@verdaccio/middleware": "workspace:6.0.0-6-next.12",
"@verdaccio/store": "workspace:6.0.0-6-next.13",
"@verdaccio/tarball": "workspace:11.0.0-6-next.7",
"@verdaccio/utils": "workspace:6.0.0-6-next.6",
"cookies": "0.8.0",
@@ -57,7 +57,8 @@
"semver": "7.3.5"
},
"devDependencies": {
"@verdaccio/server": "workspace:6.0.0-6-next.19",
"@types/node": "16.9.1",
"@verdaccio/server": "workspace:6.0.0-6-next.20",
"@verdaccio/types": "workspace:11.0.0-6-next.8",
"body-parser": "1.19.0",
"lodash": "4.17.21",

View File

@@ -1,124 +1,14 @@
import { Transform, pipeline, PassThrough } from 'stream';
import _ from 'lodash';
import buildDebug from 'debug';
import { Package } from '@verdaccio/types';
import { logger } from '@verdaccio/logger';
import { IAuth } from '@verdaccio/auth';
import { searchUtils } from '@verdaccio/core';
import { HTTP_STATUS, getInternalError } from '@verdaccio/commons-api';
import { HTTP_STATUS } from '@verdaccio/commons-api';
import { Storage } from '@verdaccio/store';
import { Package } from '@verdaccio/types';
const debug = buildDebug('verdaccio:api:search');
type SearchResults = {
objects: searchUtils.SearchItemPkg[];
total: number;
time: string;
};
// const personMatch = (person, search) => {
// if (typeof person === 'string') {
// return person.includes(search);
// }
// if (typeof person === 'object') {
// for (const field of Object.values(person)) {
// if (typeof field === 'string' && field.includes(search)) {
// return true;
// }
// }
// }
// return false;
// };
// const matcher = function (query) {
// const match = query.match(/author:(.*)/);
// if (match !== null) {
// return function (pkg) {
// return personMatch(pkg.author, match[1]);
// };
// }
// // TODO: maintainer, keywords, boost-exact
// // TODO implement some scoring system for freetext
// return (pkg) => {
// return ['name', 'displayName', 'description']
// .map((k) => {
// return pkg[k];
// })
// .filter((x) => {
// return x !== undefined;
// })
// .some((txt) => {
// return txt.includes(query);
// });
// };
// };
function removeDuplicates(results) {
const pkgNames: any[] = [];
return results.filter((pkg) => {
if (pkgNames.includes(pkg?.package?.name)) {
return false;
}
pkgNames.push(pkg?.package?.name);
return true;
});
}
function checkAccess(pkg: any, auth: any, remoteUser): Promise<Package | null> {
return new Promise((resolve, reject) => {
auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {
if (err) {
if (err.status && String(err.status).match(/^4\d\d$/)) {
// auth plugin returns 4xx user error,
// that's equivalent of !allowed basically
allowed = false;
return resolve(null);
} else {
reject(err);
}
} else {
return resolve(allowed ? pkg : null);
}
});
});
}
class TransFormResults extends Transform {
public constructor(options) {
super(options);
}
/**
* Transform either array of packages or a single package into a stream of packages.
* From uplinks the chunks are array but from local packages are objects.
* @param {string} chunk
* @param {string} encoding
* @param {function} done
* @returns {void}
* @override
*/
public _transform(chunk, _encoding, callback) {
if (_.isArray(chunk)) {
(chunk as searchUtils.SearchItem[])
.filter((pkgItem) => {
debug(`streaming remote pkg name ${pkgItem?.package?.name}`);
return true;
})
.forEach((pkgItem) => {
this.push(pkgItem);
});
return callback();
} else {
debug(`streaming local pkg name ${chunk?.package?.name}`);
this.push(chunk);
return callback();
}
}
}
/**
* Endpoint for npm search v1
* Empty value
@@ -126,37 +16,40 @@ class TransFormResults extends Transform {
* req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'
*/
export default function (route, auth: IAuth, storage: Storage): void {
function checkAccess(pkg: any, auth: any, remoteUser): Promise<Package | null> {
return new Promise((resolve, reject) => {
auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {
if (err) {
if (err.status && String(err.status).match(/^4\d\d$/)) {
// auth plugin returns 4xx user error,
// that's equivalent of !allowed basically
allowed = false;
return resolve(null);
} else {
reject(err);
}
} else {
return resolve(allowed ? pkg : null);
}
});
});
}
route.get('/-/v1/search', async (req, res, next) => {
let [size, from] = ['size', 'from'].map((k) => req.query[k]);
let data;
size = parseInt(size, 10) || 20;
from = parseInt(from, 10) || 0;
const data: any[] = [];
const transformResults = new TransFormResults({ objectMode: true });
const streamPassThrough = new PassThrough({ objectMode: true });
storage.searchManager?.search(streamPassThrough, {
query: req.query,
url: req.url,
});
const outPutStream = new PassThrough({ objectMode: true });
pipeline(streamPassThrough, transformResults, outPutStream, (err) => {
if (err) {
next(getInternalError(err ? err.message : 'unknown error'));
} else {
debug('Pipeline succeeded.');
}
});
outPutStream.on('data', (chunk) => {
data.push(chunk);
});
outPutStream.on('finish', async () => {
try {
data = await storage.searchManager?.search({
query: req.query,
url: req.url,
});
debug('stream finish');
const checkAccessPromises: searchUtils.SearchItemPkg[] = await Promise.all(
removeDuplicates(data).map((pkgItem) => {
data.map((pkgItem) => {
return checkAccess(pkgItem, auth, req.remote_user);
})
);
@@ -166,13 +59,17 @@ export default function (route, auth: IAuth, storage: Storage): void {
.slice(from, size);
logger.debug(`search results ${final?.length}`);
const response: SearchResults = {
const response: searchUtils.SearchResults = {
objects: final,
total: final.length,
time: new Date().toUTCString(),
};
res.status(HTTP_STATUS.OK).json(response);
});
} catch (error) {
logger.error({ error }, 'search endpoint has failed @{error.message}');
next(next);
return;
}
});
}

View File

@@ -1,5 +1,13 @@
# @verdaccio/auth
## 6.0.0-6-next.12
### Patch Changes
- Updated dependencies [2c594910]
- @verdaccio/logger@6.0.0-6-next.5
- @verdaccio/loaders@6.0.0-6-next.5
## 6.0.0-6-next.11
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/auth",
"version": "6.0.0-6-next.11",
"version": "6.0.0-6-next.12",
"description": "logger",
"main": "./build/index.js",
"types": "build/index.d.ts",
@@ -41,8 +41,8 @@
"dependencies": {
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
"@verdaccio/config": "workspace:6.0.0-6-next.8",
"@verdaccio/loaders": "workspace:6.0.0-6-next.4",
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
"@verdaccio/loaders": "workspace:6.0.0-6-next.5",
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
"@verdaccio/utils": "workspace:6.0.0-6-next.6",
"debug": "4.3.2",
"express": "4.17.1",

View File

@@ -1,5 +1,14 @@
# @verdaccio/cli
## 6.0.0-6-next.22
### Patch Changes
- Updated dependencies [2c594910]
- @verdaccio/logger@6.0.0-6-next.5
- @verdaccio/fastify-migration@6.0.0-6-next.13
- @verdaccio/node-api@6.0.0-6-next.21
## 6.0.0-6-next.21
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/cli",
"version": "6.0.0-6-next.21",
"version": "6.0.0-6-next.22",
"author": {
"name": "Juan Picado",
"email": "juanpicado19@gmail.com"
@@ -45,9 +45,9 @@
},
"dependencies": {
"@verdaccio/config": "workspace:6.0.0-6-next.8",
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
"@verdaccio/node-api": "workspace:6.0.0-6-next.20",
"@verdaccio/fastify-migration": "workspace:6.0.0-6-next.12",
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
"@verdaccio/node-api": "workspace:6.0.0-6-next.21",
"@verdaccio/fastify-migration": "workspace:6.0.0-6-next.13",
"clipanion": "3.0.1",
"envinfo": "7.8.1",
"kleur": "3.0.3",

View File

@@ -39,8 +39,7 @@
"semver": "7.3.5"
},
"devDependencies": {
"@verdaccio/types": "workspace:11.0.0-6-next.8",
"typedoc": "next"
"@verdaccio/types": "workspace:11.0.0-6-next.8"
},
"scripts": {
"clean": "rimraf ./build",
@@ -49,8 +48,7 @@
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
"watch": "pnpm build:js -- --watch",
"build": "pnpm run build:js && pnpm run build:types",
"doc": "typedoc src/index.ts --tsconfig tsconfig.build.json"
"build": "pnpm run build:js && pnpm run build:types"
},
"funding": {
"type": "opencollective",

View File

@@ -26,6 +26,12 @@ export type Score = {
detail: SearchMetrics;
};
export type SearchResults = {
objects: SearchItemPkg[];
total: number;
time: string;
};
type PublisherMaintainer = {
username: string;
email: string;

View File

@@ -1,5 +1,14 @@
# @verdaccio/fastify-migration
## 6.0.0-6-next.13
### Patch Changes
- Updated dependencies [2c594910]
- @verdaccio/logger@6.0.0-6-next.5
- @verdaccio/auth@6.0.0-6-next.12
- @verdaccio/store@6.0.0-6-next.13
## 6.0.0-6-next.12
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/fastify-migration",
"version": "6.0.0-6-next.12",
"version": "6.0.0-6-next.13",
"description": "Fastify server migration package",
"keywords": [
"private",
@@ -35,15 +35,16 @@
},
"dependencies": {
"@verdaccio/config": "workspace:6.0.0-6-next.8",
"@verdaccio/auth": "workspace:6.0.0-6-next.11",
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
"@verdaccio/store": "workspace:6.0.0-6-next.12",
"@verdaccio/auth": "workspace:6.0.0-6-next.12",
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
"@verdaccio/store": "workspace:6.0.0-6-next.13",
"core-js": "3.17.2",
"debug": "4.3.2",
"fastify": "3.20.2",
"fastify-plugin": "3.0.0"
},
"devDependencies": {
"@types/node": "16.9.1",
"@verdaccio/types": "workspace:11.0.0-6-next.8",
"ts-node": "10.2.1"
},

View File

@@ -0,0 +1,25 @@
/* eslint-disable no-console */
/* eslint-disable no-invalid-this */
import { logger } from '@verdaccio/logger';
async function searchRoute(fastify) {
fastify.get('/-/v1/search', async (request, reply) => {
// TODO: apply security layer here like in
// packages/api/src/v1/search.ts
// TODO: add validations for query, some parameters are mandatory
// TODO: review which query fields are mandatory
const { url, query } = request;
const storage = fastify.storage;
const data = await storage.searchManager?.search({
query: query,
url: url,
});
logger.http('search endpoint');
reply.code(200).send(data);
});
}
export default searchRoute;

View File

@@ -6,6 +6,7 @@ import buildDebug from 'debug';
import fp from 'fastify-plugin';
import ping from './endpoints/ping';
import search from './endpoints/search';
import { storageService } from './plugins/storage';
const debug = buildDebug('verdaccio:fastify');
@@ -22,6 +23,7 @@ async function startServer({ logger, config }) {
app.register((instance, opts, done) => {
instance.decorate('utility', new Map());
instance.register(ping);
instance.register(search);
done();
});

View File

@@ -1,5 +1,12 @@
# @verdaccio/hooks
## 6.0.0-6-next.7
### Patch Changes
- Updated dependencies [2c594910]
- @verdaccio/logger@6.0.0-6-next.5
## 6.0.0-6-next.6
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/hooks",
"version": "6.0.0-6-next.6",
"version": "6.0.0-6-next.7",
"description": "loaders logic",
"main": "./build/index.js",
"types": "build/index.d.ts",
@@ -31,15 +31,16 @@
},
"dependencies": {
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
"core-js": "3.17.2",
"debug": "4.3.2",
"handlebars": "4.7.7",
"undici": "4.5.1",
"undici": "4.4.7",
"undici-fetch": "1.0.0-rc.4"
},
"devDependencies": {
"@verdaccio/auth": "workspace:6.0.0-6-next.11",
"@types/node": "16.9.1",
"@verdaccio/auth": "workspace:6.0.0-6-next.12",
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
"@verdaccio/config": "workspace:6.0.0-6-next.8",
"@verdaccio/types": "workspace:11.0.0-6-next.8"

View File

@@ -1,5 +1,12 @@
# @verdaccio/loaders
## 6.0.0-6-next.5
### Patch Changes
- Updated dependencies [2c594910]
- @verdaccio/logger@6.0.0-6-next.5
## 6.0.0-6-next.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/loaders",
"version": "6.0.0-6-next.4",
"version": "6.0.0-6-next.5",
"description": "loaders logic",
"main": "./build/index.js",
"types": "build/index.d.ts",
@@ -13,7 +13,7 @@
"url": "https://github.com/verdaccio/verdaccio"
},
"dependencies": {
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
"debug": "4.3.2",
"lodash": "4.17.21"
},

View File

@@ -46,8 +46,8 @@
"prettier-bytes": "1.0.4"
},
"devDependencies": {
"@types/pino": "6.3.11",
"pino": "6.13.1"
"@types/pino": "^6.3.3",
"pino": "6.2.1"
},
"funding": {
"type": "opencollective",

View File

@@ -1,5 +1,11 @@
# @verdaccio/logger
## 6.0.0-6-next.5
### Patch Changes
- 2c594910: do not show deprecation warning on default logger config
## 6.0.0-6-next.4
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/logger",
"version": "6.0.0-6-next.4",
"version": "6.0.0-6-next.5",
"description": "logger",
"main": "./build/index.js",
"types": "build/index.d.ts",
@@ -42,10 +42,10 @@
"@verdaccio/logger-prettify": "workspace:6.0.0-alpha.3",
"debug": "4.3.2",
"lodash": "4.17.21",
"pino": "6.13.1"
"pino": "6.2.1"
},
"devDependencies": {
"@types/pino": "6.3.11",
"@types/pino": "6.3.3",
"@verdaccio/types": "workspace:11.0.0-6-next.8"
},
"funding": {

View File

@@ -98,7 +98,7 @@ export type LoggerConfigItem = {
export type LoggerConfig = LoggerConfigItem[];
export function setup(options: LoggerConfig | LoggerConfigItem = [DEFAULT_LOGGER_CONF]) {
export function setup(options: LoggerConfig | LoggerConfigItem = DEFAULT_LOGGER_CONF) {
debug('setup logger');
const isLegacyConf = Array.isArray(options);
if (isLegacyConf) {

View File

@@ -13,4 +13,29 @@ describe('logger', () => {
// FIXME: check expect
// expect(spyOn).toHaveBeenCalledTimes(2);
});
test('throw deprecation warning if multiple loggers configured', () => {
const spy = jest.spyOn(process, 'emitWarning');
setup([
{
level: 'info',
},
{
level: 'http',
},
]);
expect(spy).toHaveBeenCalledWith(
'deprecate: multiple logger configuration is deprecated, please check the migration guide.'
);
spy.mockRestore();
});
test('regression: do not throw deprecation warning if no logger config is provided', () => {
const spy = jest.spyOn(process, 'emitWarning');
setup();
expect(spy).not.toHaveBeenCalledWith(
'deprecate: multiple logger configuration is deprecated, please check the migration guide.'
);
spy.mockRestore();
});
});

View File

@@ -1,5 +1,13 @@
# @verdaccio/middleware
## 6.0.0-6-next.12
### Patch Changes
- Updated dependencies [2c594910]
- @verdaccio/logger@6.0.0-6-next.5
- @verdaccio/auth@6.0.0-6-next.12
## 6.0.0-6-next.11
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/middleware",
"version": "6.0.0-6-next.11",
"version": "6.0.0-6-next.12",
"description": "loaders logic",
"main": "./build/index.js",
"types": "build/index.d.ts",
@@ -39,9 +39,9 @@
},
"dependencies": {
"debug": "4.3.2",
"@verdaccio/auth": "workspace:6.0.0-6-next.11",
"@verdaccio/auth": "workspace:6.0.0-6-next.12",
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
"@verdaccio/utils": "workspace:6.0.0-6-next.6",
"lodash": "4.17.21"
},

View File

@@ -6,7 +6,11 @@ import { parseConfigFile } from '@verdaccio/config';
/**
* Override the default.yaml configuration file with any new config provided.
*/
function configExample(externalConfig, configFile: string = 'default.yaml', location: string = '') {
function configExample(
externalConfig = {},
configFile: string = 'default.yaml',
location: string = ''
) {
const locationFile = location
? path.join(location, configFile)
: path.join(__dirname, `./config/yaml/${configFile}`);

View File

@@ -1,5 +1,13 @@
# @verdaccio/node-api
## 6.0.0-6-next.21
### Patch Changes
- Updated dependencies [2c594910]
- @verdaccio/logger@6.0.0-6-next.5
- @verdaccio/server@6.0.0-6-next.20
## 6.0.0-6-next.20
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/node-api",
"version": "6.0.0-6-next.20",
"version": "6.0.0-6-next.21",
"description": "node API",
"main": "build/index.js",
"types": "build/index.d.ts",
@@ -41,13 +41,14 @@
"dependencies": {
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
"@verdaccio/config": "workspace:6.0.0-6-next.8",
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
"@verdaccio/server": "workspace:6.0.0-6-next.19",
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
"@verdaccio/server": "workspace:6.0.0-6-next.20",
"core-js": "3.17.2",
"debug": "4.3.2",
"lodash": "4.17.21"
},
"devDependencies": {
"@types/node": "16.9.1",
"@verdaccio/mock": "workspace:6.0.0-6-next.9",
"@verdaccio/types": "workspace:11.0.0-6-next.8",
"jest-mock-process": "1.4.1",

View File

@@ -1,5 +1,11 @@
# @verdaccio/ui-theme
## 6.0.0-6-next.11
### Minor Changes
- 5fed1955: feat: integrate rematch for ui state storage
## 6.0.0-6-next.10
### Major Changes

View File

@@ -1,94 +0,0 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import translationCS from './translations/cs-CZ.json';
import translationDE from './translations/de-DE.json';
import translationEN from './translations/en-US.json';
import translationES from './translations/es-ES.json';
import translationFR from './translations/fr-FR.json';
import translationJP from './translations/ja-JP.json';
import translationKM from './translations/km-KH.json';
import translationPT from './translations/pt-BR.json';
import translationRU from './translations/ru-RU.json';
import translationTR from './translations/tr-TR.json';
import translationUA from './translations/uk-UA.json';
import translationCN from './translations/zh-CN.json';
import translatiobTW from './translations/zh-TW.json';
const languages = {
'en-US': {
translation: translationEN,
},
'cs-CZ': {
translation: translationCS,
},
'pt-BR': {
translation: translationPT,
},
'es-ES': {
translation: translationES,
},
'de-DE': {
translation: translationDE,
},
'fr-FR': {
translation: translationFR,
},
'zh-CN': {
translation: translationCN,
},
'ja-JP': {
translation: translationJP,
},
'ru-RU': {
translation: translationRU,
},
'tr-TR': {
translation: translationTR,
},
'uk-UA': {
translation: translationUA,
},
'km-KH': {
translation: translationKM,
},
'zh-TW': {
translation: translatiobTW,
},
};
type Language = keyof typeof languages;
i18n
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
// in case window.VEDACCIO_LANGUAGE is undefined,it will fall back to 'en-US'
lng: window?.__VERDACCIO_BASENAME_UI_OPTIONS?.language || 'en-US',
fallbackLng: 'en-US',
whitelist: [
'en-US',
'cs-CZ',
'pt-BR',
'es-ES',
'de-DE',
'fr-FR',
'zh-CN',
'ja-JP',
'ru-RU',
'tr-TR',
'uk-UA',
'km-KH',
'zh-TW',
],
load: 'currentOnly',
resources: languages,
debug: false,
interpolation: {
escapeValue: false, // react already safes from xss
},
});
export default i18n;
export { Language };

View File

@@ -0,0 +1,42 @@
[
{
"name": "test",
"version": "1.0.22",
"description": "test",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "http",
"url": "git+https://github.com/test/test.git"
},
"keywords": [],
"author": {
"name": "",
"email": "",
"url": "",
"avatar": "data:image/svg+xml;utf8,%3Csvg%20height%3D%22100%22%20viewBox%3D%22-27%2024%20100%20100%22%20width%3D%22100%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Ccircle%20cx%3D%2223%22%20cy%3D%2274%22%20id%3D%22a%22%20r%3D%2250%22%2F%3E%3C%2Fdefs%3E%3Cuse%20fill%3D%22%23F5EEE5%22%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23a%22%2F%3E%3CclipPath%20id%3D%22b%22%3E%3Cuse%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23a%22%2F%3E%3C%2FclipPath%3E%3Cg%20clip-path%3D%22url(%23b)%22%3E%3Cdefs%3E%3Cpath%20d%3D%22M36%2095.9c0%204%204.7%205.2%207.1%205.8%207.6%202%2022.8%205.9%2022.8%205.9%203.2%201.1%205.7%203.5%207.1%206.6v9.8H-27v-9.8c1.3-3.1%203.9-5.5%207.1-6.6%200%200%2015.2-3.9%2022.8-5.9%202.4-.6%207.1-1.8%207.1-5.8V85h26v10.9z%22%20id%3D%22c%22%2F%3E%3C%2Fdefs%3E%3Cuse%20fill%3D%22%23E6C19C%22%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23c%22%2F%3E%3CclipPath%20id%3D%22d%22%3E%3Cuse%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23c%22%2F%3E%3C%2FclipPath%3E%3Cpath%20clip-path%3D%22url(%23d)%22%20d%3D%22M23.2%2035h.2c3.3%200%208.2.2%2011.4%202%203.3%201.9%207.3%205.6%208.5%2012.1%202.4%2013.7-2.1%2035.4-6.3%2042.4-4%206.7-9.8%209.2-13.5%209.4H23h-.1c-3.7-.2-9.5-2.7-13.5-9.4-4.2-7-8.7-28.7-6.3-42.4%201.2-6.5%205.2-10.2%208.5-12.1%203.2-1.8%208.1-2%2011.4-2h.2z%22%20fill%3D%22%23D4B08C%22%2F%3E%3C%2Fg%3E%3Cpath%20d%3D%22M22.6%2040c19.1%200%2020.7%2013.8%2020.8%2015.1%201.1%2011.9-3%2028.1-6.8%2033.7-4%205.9-9.8%208.1-13.5%208.3h-.5c-3.8-.3-9.6-2.5-13.6-8.4-3.8-5.6-7.9-21.8-6.8-33.8C2.3%2053.7%203.5%2040%2022.6%2040z%22%20fill%3D%22%23F2CEA5%22%2F%3E%3C%2Fsvg%3E"
},
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21"
},
"readmeFilename": "README.md",
"bugs": {
"url": "https://github.com/test/test/issues"
},
"homepage": "https://github.com/test/test#readme",
"_id": "test@1.0.22",
"_nodeVersion": "14.17.4",
"_npmVersion": "7.20.5",
"dist": {
"integrity": "sha512-2IDD0lLzGUL7YJ+17Oh9VtbOwdKLqBLS+ZFATDXi5R22TL2hZ9LBFE10bzsDovNc4xtgwZAk1/K+5LHTye4ztg==",
"shasum": "c9152f57636bce762ccb5a83113c42a5831579bc",
"tarball": "http://localhost:4873/test/-/test-1.0.22.tgz"
},
"contributors": [],
"time": "2021-08-14T20:15:19.336Z",
"users": {}
}
]

View File

@@ -10,9 +10,9 @@ module.exports = Object.assign({}, config, {
'^.+\\.(js|ts|tsx)$': 'babel-jest',
},
moduleFileExtensions: ['js', 'ts', 'tsx'],
testURL: 'http://localhost',
testURL: 'http://localhost:9000/',
rootDir: '..',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '<rootDir>/jest/setup-env.ts'],
setupFiles: ['<rootDir>/jest/setup.ts'],
transformIgnorePatterns: ['<rootDir>/node_modules/(?!react-syntax-highlighter)'],
modulePathIgnorePatterns: [

View File

@@ -0,0 +1,9 @@
import { rest } from 'msw';
const packagesPayload = require('./api/packages.json');
export const handlers = [
rest.get('http://localhost:9000/-/verdaccio/packages', (req, res, ctx) => {
return res(ctx.json(packagesPayload));
}),
];

View File

@@ -0,0 +1,6 @@
import { setupServer } from 'msw/node';
import { handlers } from './server-handlers';
const server = setupServer(...handlers);
export { server };

View File

@@ -0,0 +1,15 @@
import '@testing-library/jest-dom/extend-expect';
import 'whatwg-fetch';
import '@testing-library/jest-dom';
import { server } from './server';
beforeAll(() => {
server.listen({
onUnhandledRequest: 'warn',
});
});
afterEach(() => server.resetHandlers());
afterAll(() => {
server.close();
});

View File

@@ -2,29 +2,24 @@
* Setup configuration for Jest
* This file includes global settings for the JEST environment.
*/
import { GlobalWithFetchMock } from 'jest-fetch-mock';
import 'mutationobserver-shim';
// @ts-ignore : Property '__VERDACCIO_BASENAME_UI_OPTIONS' does not exist on type 'Global'.
global.__VERDACCIO_BASENAME_UI_OPTIONS = {
base: 'http://localhost',
base: 'http://localhost:9000/',
protocol: 'http',
host: 'localhost',
primaryColor: '#4b5e40',
url_prefix: '',
darkMode: false,
language: 'en-US',
uri: 'http://localhost:4873',
uri: 'http://localhost:9000/',
pkgManagers: ['pnpm', 'yarn', 'npm'],
title: 'Verdaccio Dev UI',
scope: '',
version: 'v1.0.0',
};
const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;
customGlobal.fetch = require('jest-fetch-mock');
customGlobal.fetchMock = customGlobal.fetch;
// mocking few DOM methods
// @ts-ignore : Property 'document' does not exist on type 'Global'.
if (global.document) {

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/ui-theme",
"version": "6.0.0-6-next.10",
"version": "6.0.0-6-next.11",
"description": "Verdaccio User Interface",
"author": {
"name": "Verdaccio Contributors",
@@ -17,6 +17,13 @@
"npm": ">=6"
},
"devDependencies": {
"@types/react": "17.0.19",
"@types/react-autosuggest": "10.1.5",
"@types/react-dom": "17.0.9",
"@types/react-helmet": "6.1.2",
"@types/redux": "3.6.0",
"@types/react-router-dom": "5.1.8",
"@types/react-virtualized": "9.21.13",
"@emotion/core": "10.1.1",
"@emotion/jest": "11.3.0",
"@emotion/styled": "10.0.27",
@@ -24,10 +31,12 @@
"@material-ui/core": "4.11.4",
"@material-ui/icons": "4.11.2",
"@material-ui/styles": "4.11.4",
"@testing-library/dom": "8.2.0",
"@rematch/core": "2.1.0",
"@rematch/loading": "2.1.0",
"@testing-library/dom": "8.5.0",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "12.0.0",
"@verdaccio/node-api": "workspace:6.0.0-6-next.20",
"@testing-library/react": "12.1.0",
"@verdaccio/node-api": "workspace:6.0.0-6-next.21",
"autosuggest-highlight": "3.1.1",
"babel-loader": "8.2.2",
"babel-plugin-dynamic-import-node": "2.3.3",
@@ -42,8 +51,9 @@
"harmony-reflect": "1.6.2",
"history": "4.10.1",
"html-webpack-plugin": "5.3.2",
"i18next": "20.6.0",
"i18next": "20.6.1",
"in-publish": "2.0.1",
"country-flag-icons": "1.4.10",
"js-base64": "3.6.1",
"js-yaml": "3.14.1",
"localstorage-memory": "1.0.3",
@@ -58,13 +68,16 @@
"react": "17.0.2",
"react-autosuggest": "10.1.0",
"react-dom": "17.0.2",
"react-hook-form": "7.14.2",
"react-hook-form": "7.15.3",
"react-hot-loader": "4.13.0",
"react-i18next": "11.11.4",
"react-i18next": "11.12.0",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-virtualized": "9.22.3",
"react-redux": "7.2.1",
"redux": "4.1.1",
"rimraf": "3.0.2",
"msw": "0.35.0",
"standard-version": "9.3.1",
"style-loader": "3.2.1",
"stylelint": "13.13.1",
@@ -123,7 +136,7 @@
"type-check": "tsc --noEmit -p tsconfig.build.json",
"start": "babel-node tools/dev.server.js",
"test:clean": "jest --clearCache",
"test": "cross-env BABEL_ENV=test cross-env NODE_ENV=test cross-env TZ=UTC jest --config ./jest/jest.config.js --maxWorkers 2 --passWithNoTests",
"test": "cross-env BABEL_ENV=test cross-env NODE_ENV=test cross-env TZ=UTC jest --config ./jest/jest.config.js --runInBand",
"test:update-snapshot": "yarn run test -- -u",
"test:size": "bundlesize",
"lint": "pnpm lint:js && pnpm lint:css",

View File

@@ -1,10 +1,16 @@
import React from 'react';
import storage from 'verdaccio-ui/utils/storage';
import { render, waitFor, fireEvent } from 'verdaccio-ui/utils/test-react-testing-library';
import {
renderWithStore,
act,
waitFor,
fireEvent,
screen,
} from 'verdaccio-ui/utils/test-react-testing-library';
// eslint-disable-next-line jest/no-mocks-import
import { generateTokenWithTimeRange } from '../../jest/unit/components/__mocks__/token';
import { store } from '../store';
import App from './App';
@@ -30,68 +36,88 @@ jest.mock('verdaccio-ui/utils/storage', () => {
return new LocalStorageMock();
});
jest.mock('verdaccio-ui/providers/API/api', () => ({
// eslint-disable-next-line jest/no-mocks-import
request: require('../../jest/unit/components/__mocks__/api').default.request,
}));
// force the windows to expand to display items
// https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107
jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600);
jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600);
/* eslint-disable react/jsx-no-bind*/
describe('<App />', () => {
// test('should display the Header component ', async () => {
// const { queryByTestId } = render(<App />);
//
// expect(queryByTestId('loading')).toBeTruthy();
//
// // wait for the Header component appearance and return the element
// const headerElement = await waitFor(() => queryByTestId('header'));
// expect(headerElement).toBeTruthy();
// });
describe('login - log out', () => {
test('handleLogout - logouts the user and clear localstorage', async () => {
const { queryByTestId } = renderWithStore(<App />, store);
store.dispatch.login.logInUser({
username: 'verdaccio',
token: generateTokenWithTimeRange(24),
});
test('handleLogout - logouts the user and clear localstorage', async () => {
storage.setItem('username', 'verdaccio');
storage.setItem('token', generateTokenWithTimeRange(24));
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitFor(() => queryByTestId('logInDialogIcon'));
expect(accountCircleElement).toBeTruthy();
const { queryByTestId } = render(<App />);
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitFor(() => queryByTestId('header--menu-accountcircle'));
expect(accountCircleElement).toBeTruthy();
// wait for the Button's logout element component appearance and return the element
const buttonLogoutElement = await waitFor(() => queryByTestId('logOutDialogIcon'));
expect(buttonLogoutElement).toBeTruthy();
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
if (buttonLogoutElement) {
fireEvent.click(buttonLogoutElement);
// wait for the Button's logout element component appearance and return the element
const buttonLogoutElement = await waitFor(() => queryByTestId('header--button-logout'));
expect(buttonLogoutElement).toBeTruthy();
if (buttonLogoutElement) {
fireEvent.click(buttonLogoutElement);
expect(queryByTestId('greetings-label')).toBeFalsy();
expect(queryByTestId('greetings-label')).toBeFalsy();
}
}
}
});
test('isUserAlreadyLoggedIn: token already available in storage', async () => {
const { queryByTestId, queryAllByText } = renderWithStore(<App />, store);
store.dispatch.login.logInUser({
username: 'verdaccio',
token: generateTokenWithTimeRange(24),
});
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitFor(() => queryByTestId('logInDialogIcon'));
expect(accountCircleElement).toBeTruthy();
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
// wait for the Greeting's label element component appearance and return the element
const greetingsLabelElement = await waitFor(() => queryByTestId('greetings-label'));
expect(greetingsLabelElement).toBeTruthy();
if (greetingsLabelElement) {
expect(queryAllByText('verdaccio')).toBeTruthy();
}
}
});
});
test('isUserAlreadyLoggedIn: token already available in storage', async () => {
storage.setItem('username', 'verdaccio');
storage.setItem('token', generateTokenWithTimeRange(24));
describe('list packages', () => {
test('should display the Header component', async () => {
renderWithStore(<App />, store);
const { queryByTestId, queryAllByText } = render(<App />);
await waitFor(() => {
expect(screen.queryByTestId('loading')).toBeTruthy();
});
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitFor(() => queryByTestId('header--menu-accountcircle'));
expect(accountCircleElement).toBeTruthy();
// wait for the Header component appearance and return the element
const headerElement = await waitFor(() => screen.queryByTestId('header'));
expect(headerElement).toBeTruthy();
});
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
test('should display package lists', async () => {
act(() => {
renderWithStore(<App />, store);
});
// wait for the Greeting's label element component appearance and return the element
const greetingsLabelElement = await waitFor(() => queryByTestId('greetings-label'));
expect(greetingsLabelElement).toBeTruthy();
await waitFor(() => {
expect(screen.getByTestId('package-item-list')).toBeInTheDocument();
});
if (greetingsLabelElement) {
expect(queryAllByText('verdaccio')).toBeTruthy();
}
}
expect(store.getState().packages.response).toHaveLength(1);
}, 10000);
});
});

View File

@@ -1,7 +1,6 @@
/* eslint-disable react/jsx-max-depth */
import styled from '@emotion/styled';
import isNil from 'lodash/isNil';
import React, { useState, useEffect, Suspense } from 'react';
import React, { useEffect, Suspense } from 'react';
import { Router } from 'react-router-dom';
import Box from 'verdaccio-ui/components/Box';
@@ -9,15 +8,12 @@ import Loading from 'verdaccio-ui/components/Loading';
import loadDayJSLocale from 'verdaccio-ui/design-tokens/load-dayjs-locale';
import StyleBaseline from 'verdaccio-ui/design-tokens/StyleBaseline';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { isTokenExpire } from 'verdaccio-ui/utils/login';
import storage from 'verdaccio-ui/utils/storage';
import AppContextProvider from './AppContextProvider';
import AppRoute, { history } from './AppRoute';
import Footer from './Footer';
import Header from './Header';
import '../../i18n/config';
import '../i18n/config';
const StyledBox = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor: theme?.palette.background.default,
@@ -32,35 +28,8 @@ const StyledBoxContent = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
},
}));
/* eslint-disable react/jsx-no-bind */
/* eslint-disable react-hooks/exhaustive-deps */
const App: React.FC = () => {
const [user, setUser] = useState<undefined | { username: string | null }>();
/**
* Logout user
* Required by: <Header />
*/
const logout = () => {
storage.removeItem('username');
storage.removeItem('token');
setUser(undefined);
};
const checkUserAlreadyLoggedIn = () => {
// checks for token validity
const token = storage.getItem('token');
const username = storage.getItem('username');
if (isTokenExpire(token) || isNil(username)) {
logout();
return;
}
setUser({ username });
};
useEffect(() => {
checkUserAlreadyLoggedIn();
loadDayJSLocale();
}, []);
@@ -70,12 +39,10 @@ const App: React.FC = () => {
<StyledBox display="flex" flexDirection="column" height="100%">
<>
<Router history={history}>
<AppContextProvider user={user}>
<Header />
<StyledBoxContent flexGrow={1}>
<AppRoute />
</StyledBoxContent>
</AppContextProvider>
<Header />
<StyledBoxContent flexGrow={1}>
<AppRoute />
</StyledBoxContent>
</Router>
<Footer />
</>

View File

@@ -1,18 +0,0 @@
import { createContext } from 'react';
export interface AppProps {
user?: User;
scope: string;
}
export interface User {
username: string | null;
}
export interface AppContextProps extends AppProps {
setUser: (user?: User) => void;
}
const AppContext = createContext<undefined | AppContextProps>(undefined);
export default AppContext;

View File

@@ -1,44 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useConfig } from 'verdaccio-ui/providers/config';
import AppContext, { AppProps, User } from './AppContext';
interface Props {
user?: User;
}
/* eslint-disable react-hooks/exhaustive-deps */
const AppContextProvider: React.FC<Props> = ({ children, user }) => {
const { configOptions } = useConfig();
const [state, setState] = useState<AppProps>({
scope: configOptions.scope ?? '',
user,
});
useEffect(() => {
setState({
...state,
user,
});
}, [user]);
const setUser = (user?: User) => {
setState({
...state,
user,
});
};
return (
<AppContext.Provider
value={{
...state,
setUser,
}}>
{children}
</AppContext.Provider>
);
};
export default AppContextProvider;

View File

@@ -1,9 +1,7 @@
import { createBrowserHistory } from 'history';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import React from 'react';
import { Route as ReactRouterDomRoute, Switch, Router } from 'react-router-dom';
import AppContext from './AppContext';
import loadable from './utils/loadable';
const NotFound = loadable(
@@ -28,22 +26,11 @@ export const history = createBrowserHistory({
});
const AppRoute: React.FC = () => {
const appContext = useContext(AppContext);
const { t } = useTranslation();
if (!appContext) {
throw Error(t('app-context-not-correct-used'));
}
const { user } = appContext;
const isUserLoggedIn = user?.username;
return (
<Router history={history}>
<Switch>
<ReactRouterDomRoute exact={true} path={Route.ROOT}>
<HomePage isUserLoggedIn={!!isUserLoggedIn} />
<HomePage />
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE}>
<VersionContextProvider>

View File

@@ -1,18 +1,11 @@
/* eslint-disable react/jsx-max-depth */
/* eslint-disable react/jsx-pascal-case */
import styled from '@emotion/styled';
import FlagsIcon from 'country-flag-icons/react/3x2';
import React from 'react';
import { useTranslation, Trans } from 'react-i18next';
import {
Austria,
Brazil,
Earth,
Nicaragua,
Spain,
Germany,
India,
China,
Taiwan,
} from 'verdaccio-ui/components/Icons';
import { Earth } from 'verdaccio-ui/components/Icons';
import Logo from 'verdaccio-ui/components/Logo';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { useConfig } from 'verdaccio-ui/providers/config';
@@ -32,14 +25,30 @@ const Footer = () => {
<ToolTip>
<StyledEarth />
<Flags>
<Spain />
<Nicaragua />
<India />
<Brazil />
<China />
<Austria />
<Germany />
<Taiwan />
<Icon>
<FlagsIcon.ES />
</Icon>
<Icon>
<FlagsIcon.NI />
</Icon>
<Icon>
<FlagsIcon.IN />
</Icon>
<Icon>
<FlagsIcon.BR />
</Icon>
<Icon>
<FlagsIcon.CN />
</Icon>
<Icon>
<FlagsIcon.AU />
</Icon>
<Icon>
<FlagsIcon.DE />
</Icon>
<Icon>
<FlagsIcon.TW />
</Icon>
</Flags>
</ToolTip>
</Left>
@@ -87,6 +96,10 @@ const Flags = styled('span')<{ theme?: Theme }>(({ theme }) => ({
},
}));
const Icon = styled('div')({
width: '10px',
});
const ToolTip = styled('span')({
position: 'relative',
height: '18px',

View File

@@ -111,8 +111,7 @@ exports[`<Footer /> component should load the initial state of Footer component
}
.emotion-15 {
width: 18px;
height: 18px;
width: 10px;
}
.emotion-31 {
@@ -211,344 +210,235 @@ exports[`<Footer /> component should load the initial state of Footer component
<span
class="emotion-13 emotion-14"
>
<svg
class="MuiSvgIcon-root emotion-15 emotion-12"
focusable="false"
role="img"
viewBox="0 0 45 45"
<div
class="emotion-15 emotion-16"
>
<defs>
<clippath
id="prefix__a"
>
<path
d="M0 36h36V0H0v36z"
/>
</clippath>
</defs>
<g
clip-path="url(#prefix__a)"
transform="matrix(1.25 0 0 -1.25 0 45)"
<svg
viewBox="0 85.333 512 341.333"
>
<path
d="M36 9a4 4 0 00-4-4H4a4 4 0 00-4 4v18a4 4 0 004 4h28a4 4 0 004-4V9z"
fill="#c60a1d"
d="M0 85.331h512v341.337H0z"
fill="#FFDA44"
/>
<path
d="M36 12H0v12h36V12z"
fill="#ffc400"
d="M0 85.331h512v113.775H0zm0 227.551h512v113.775H0z"
fill="#D80027"
/>
<path
d="M9 19v-3a3 3 0 116 0v3H9z"
fill="#ea596e"
/>
<path
d="M12 17h3v3h-3v-3z"
fill="#f4a2b2"
/>
<path
d="M12 17H9v3h3v-3z"
fill="#dd2e44"
/>
<path
d="M15 21.5c0-.829-1.343-1.5-3-1.5s-3 .671-3 1.5 1.343 1.5 3 1.5 3-.671 3-1.5"
fill="#ea596e"
/>
<path
d="M15 22.25c0 .414-1.343.75-3 .75s-3-.336-3-.75 1.343-.75 3-.75 3 .336 3 .75"
fill="#ffac33"
/>
<path
d="M7 13h1v7H7v-7zm10 0h-1v7h1v-7z"
fill="#99aab5"
/>
<path
d="M9 13H6v1h3v-1zm9 0h-3v1h3v-1zM8 20H7v1h1v-1zm9 0h-1v1h1v-1z"
fill="#66757f"
/>
</g>
<title>
Spain
</title>
</svg>
<svg
class="MuiSvgIcon-root emotion-15 emotion-12"
focusable="false"
role="img"
viewBox="0 0 512 512"
</svg>
</div>
<div
class="emotion-15 emotion-16"
>
<path
d="M512 384c0 31.418-25.473 56.889-56.889 56.889H56.89C25.472 440.889 0 415.417 0 384V128c0-31.418 25.472-56.889 56.889-56.889H455.11C486.53 71.111 512 96.584 512 128v256z"
fill="#265fb5"
/>
<path
d="M512 327.111H0V184.89h512v142.22z"
fill="#eee"
/>
<path
d="M320.811 256c0 35.797-29.014 64.811-64.811 64.811-35.783 0-64.811-29.014-64.811-64.811s29.027-64.811 64.811-64.811c35.797 0 64.811 29.013 64.811 64.811"
fill="#a9bf4c"
/>
<path
d="M312.889 256c0 31.418-25.473 56.889-56.889 56.889S199.111 287.416 199.111 256s25.473-56.889 56.889-56.889 56.889 25.471 56.889 56.889"
fill="#eee"
/>
<path
d="M209.891 286.649l45.909-79.517 45.909 79.517H209.89z"
fill="#265fb5"
/>
<path
d="M215.04 283.591l40.818-70.685 40.803 70.685H215.04z"
fill="#55acee"
/>
<path
d="M215.04 283.591l9.841-17.052 61.483-.783 10.297 17.835H215.04z"
fill="#bbddf5"
/>
<path
d="M222.891 272.441l15.331-12.445 6.67 7.553 5.774-6.215 4.893 4.892 4.665-5.12 5.561 5.12 5.106-5.334 4.665 5.334 4.451-4.892 7.325 9.102 1.338 2.674s-7.78 1.55-18.446.669c-10.667-.896-16.882 1.55-25.33 2.66-8.448 1.109-23.553-1.109-23.553-1.109l1.55-2.889z"
fill="#5c913b"
/>
<path
d="M237.995 262.67l10.226 11.107-5.12.442-5.774-8.434.668-3.115zm12.231.883l8.22 7.338-3.782.654-3.996-5.546-.442-2.446zm10.439-.442l7.111 6.67L266 270.89l-5.774-6.229.44-1.55zm9.33 0l-2.888 2.66 1.338 1.565 1.55-4.225zm8.889.228l-3.328 4.224 1.109 1.99 2.446-4.664-.227-1.55z"
fill="#e2f09f"
/>
<path
d="M256.426 233.671l.939 13.227 7.566-10.867-5.675 11.805 11.805-5.66-10.851 7.553 13.213.939-13.213.952 10.851 7.553-11.805-5.66 5.675 11.805-7.566-10.867-.939 13.226-.939-13.226-7.566 10.867 5.675-11.805-11.805 5.66 10.851-7.553-13.212-.953 13.212-.938-10.85-7.553 11.804 5.66-5.675-11.805 7.566 10.866.94-13.226z"
fill="#bbddf5"
/>
<path
d="M256 244.665l-2.66 2.66s.654 4.011.441 4.679C253.554 252.658 256 256 256 256l3.327-3.996-.88-5.334-2.447-2.005z"
fill="#dd2e44"
/>
<path
d="M257.28 240.071c10.894 0 17.109 5.334 17.109 5.334l-3.996-7.111s-6.443-4.893-13.995-4.893c-7.567 0-16 6.001-16 6.001l-3.783 7.553s9.771-6.884 20.665-6.884"
fill="#269"
/>
<path
d="M257.28 240.071c10.894 0 17.109 5.334 17.109 5.334l-2.888-5.106s-7.78-4.665-15.331-4.665-16.896 5.987-16.896 5.987l-2.66 5.334c.001 0 9.772-6.884 20.666-6.884"
fill="#ffcc4d"
/>
<path
d="M257.28 240.071c10.894 0 17.109 5.334 17.109 5.334l-2.005-3.327s-5.988-4.224-16.214-3.783c-7.553 0-18.005 5.561-18.005 5.561l-1.55 3.1c0-.001 9.771-6.885 20.665-6.885"
fill="#dd2e44"
/>
<path
d="M264.291 322.873h-14.165V309.29h14.165v13.582zm-16.426-118.898h-10.183l-5.106-12.459 10.198-1.137 5.091 13.596zm23.21 5.66l-6.784-2.261 3.385-16.426 13.028 1.137-9.629 17.55zm-61.141 69.646l-19.811 4.523-.57-13.583 19.812-2.83.569 11.89zm5.106 14.152l-16.426 6.812-3.954-10.766 15.289-5.106 5.091 9.06zm109.269-12.444l-18.105-5.091v-9.63l20.366-2.83-2.261 17.55zm-9.003 18.674l-16.981-7.937 5.646-10.182 18.703 5.66-7.368 12.459z"
fill="#eee"
/>
<title>
Nicaragua
</title>
</svg>
<svg
class="MuiSvgIcon-root emotion-15 emotion-12"
focusable="false"
role="img"
viewBox="0 0 512 512"
>
<path
d="M0 384c0 31.418 25.473 56.889 56.889 56.889H455.11c31.42 0 56.89-25.473 56.89-56.889v-56.889H0V384z"
fill="#138808"
/>
<path
d="M0 327.111h512V184.89H0v142.22z"
fill="#eee"
/>
<path
d="M512 184.889V128c0-31.417-25.473-56.889-56.889-56.889H56.89C25.472 71.111 0 96.583 0 128v56.889h512z"
fill="#f93"
/>
<path
d="M312.889 256c0-31.431-25.473-56.902-56.903-56.902-31.417 0-56.888 25.472-56.888 56.902 0 31.418 25.472 56.889 56.888 56.889 31.432 0 56.903-25.472 56.903-56.889"
fill="navy"
/>
<path
d="M298.666 256c0-23.566-19.115-42.681-42.681-42.681S213.319 232.434 213.319 256s19.1 42.666 42.666 42.666 42.681-19.1 42.681-42.666"
fill="#eee"
/>
<path
d="M256 213.334l2.076 32.199 14.251-28.943-10.396 30.535 24.235-21.305-21.291 24.249 30.535-10.396-28.942 14.25L298.666 256l-32.198 2.076 28.942 14.237-30.535-10.383 21.291 24.235-24.235-21.29 10.396 30.535-14.25-28.942L256 298.666l-2.076-32.198-14.252 28.942 10.397-30.535-24.249 21.291 21.305-24.235-30.535 10.383 28.942-14.236L213.334 256l32.199-2.076-28.943-14.251 30.535 10.396-21.305-24.249 24.249 21.305-10.396-30.535 14.25 28.943 2.077-32.2z"
fill="navy"
opacity="0.6"
/>
<path
d="M241.778 256c0-7.851 6.37-14.223 14.222-14.223s14.223 6.372 14.223 14.223-6.372 14.223-14.223 14.223-14.223-6.372-14.223-14.223"
fill="navy"
/>
<title>
India
</title>
</svg>
<svg
class="MuiSvgIcon-root emotion-15 emotion-12"
focusable="false"
role="img"
viewBox="0 0 45 45"
>
<defs>
<clippath
id="prefix__a"
>
<path
d="M0 36h36V0H0v36z"
/>
</clippath>
</defs>
<g
clip-path="url(#prefix__a)"
transform="matrix(1.25 0 0 -1.25 0 45)"
<svg
viewBox="0 85.333 512 341.333"
>
<path
d="M36 9a4 4 0 00-4-4H4a4 4 0 00-4 4v18a4 4 0 004 4h28a4 4 0 004-4V9z"
d="M0 85.337h512v341.326H0z"
fill="#FFF"
/>
<path
d="M0 85.337h512v113.775H0zm0 227.551h512v113.775H0z"
fill="#338AF3"
/>
<path
d="M256 214.447c-22.949 0-41.553 18.603-41.553 41.553S233.05 297.553 256 297.553c22.949 0 41.553-18.603 41.553-41.553S278.949 214.447 256 214.447zm0 65.298c-13.114 0-23.745-10.631-23.745-23.745s10.631-23.745 23.745-23.745 23.745 10.631 23.745 23.745-10.631 23.745-23.745 23.745z"
fill="#FFDA44"
/>
<path
d="M276.563 261.936L256 256l-20.563 5.936-6.855 11.873h54.836z"
fill="#0052B4"
/>
<path
d="M256 226.32l-13.709 23.744L256 256l13.709-5.936z"
fill="#338AF3"
/>
<path
d="M235.437 261.936h41.126l-6.854-11.872h-27.418z"
fill="#6DA544"
/>
</svg>
</div>
<div
class="emotion-15 emotion-16"
>
<svg
viewBox="0 0 513 342"
>
<path
d="M17.3 0h478.4v342H17.3V0z"
fill="#181A93"
/>
<path
d="M0 0h513v114H0V0z"
fill="#FFA44A"
/>
<path
d="M0 228h513v114H0V228z"
fill="#1A9F0B"
/>
<path
d="M0 114h513v114H0V114z"
fill="#FFF"
/>
<circle
cx="256.5"
cy="171"
fill="#FFF"
r="34.2"
/>
<path
d="M256.5 216.6c-25.1 0-45.6-20.5-45.6-45.6s20.5-45.6 45.6-45.6 45.6 20.5 45.6 45.6-20.5 45.6-45.6 45.6zm0-11.4c18.2 0 34.2-16 34.2-34.2s-15.9-34.2-34.2-34.2-34.2 16-34.2 34.2 16 34.2 34.2 34.2z"
fill="#181A93"
/>
<circle
cx="256.5"
cy="171"
fill="#181A93"
r="22.8"
/>
</svg>
</div>
<div
class="emotion-15 emotion-16"
>
<svg
viewBox="0 0 513 342"
>
<path
d="M0 0h513v342H0z"
fill="#009b3a"
/>
<path
d="M32.727 18L18 6.876 3.27 18 18 29.125 32.727 18z"
fill="#fedf01"
d="M256.5 19.3l204.9 151.4L256.5 322 50.6 170.7z"
fill="#fedf00"
/>
<circle
cx="256.5"
cy="171"
fill="#FFF"
r="80.4"
/>
<path
d="M24.434 18.076a6.458 6.458 0 11-12.917 0 6.458 6.458 0 0112.917 0"
d="M215.9 165.7c-13.9 0-27.4 2.1-40.1 6 .6 43.9 36.3 79.3 80.3 79.3 27.2 0 51.3-13.6 65.8-34.3-24.9-31-63.2-51-106-51zm119 20.3c.9-5 1.5-10.1 1.5-15.4 0-44.4-36-80.4-80.4-80.4-33.1 0-61.5 20.1-73.9 48.6 10.9-2.2 22.1-3.4 33.6-3.4 46.8.1 89 19.5 119.2 50.6z"
fill="#002776"
/>
<path
d="M12.277 21.113a6.406 6.406 0 01-.672-2.023c3.994.29 9.417-1.892 11.744-4.596.402.604.7 1.28.882 2.004-2.871 2.809-7.916 4.63-11.954 4.615"
fill="#cbe9d4"
/>
<path
d="M13 16.767h-1v1h1v-1zm1-2h-1v1h1v-1z"
fill="#88c9f9"
/>
<path
d="M16 16.767h-1v1h1v-1zm2-1h-1v1h1v-1zm4-2h-1v1h1v-1zm-3-1h-1v1h1v-1zm3 6h-1v1h1v-1z"
fill="#55acee"
/>
<path
d="M20 14.767h-1v1h1v-1z"
fill="#3b88c3"
/>
</g>
<title>
Brazil
</title>
</svg>
<svg
class="MuiSvgIcon-root emotion-15 emotion-12"
focusable="false"
role="img"
viewBox="0 0 45 45"
</svg>
</div>
<div
class="emotion-15 emotion-16"
>
<defs>
<clippath
id="prefix__a"
>
<path
d="M0 36h36V0H0v36z"
/>
</clippath>
</defs>
<g
clip-path="url(#prefix__a)"
transform="matrix(1.25 0 0 -1.25 0 45)"
<svg
viewBox="0 0 513 342"
>
<path
d="M36 9a4 4 0 00-4-4H4a4 4 0 00-4 4v18a4 4 0 004 4h28a4 4 0 004-4V9z"
fill="#de2910"
d="M0 0h513v342H0z"
fill="#D80027"
/>
<path
d="M7 25.049l.929-2.67 2.826-.06-2.253-1.706.819-2.707L7 19.52l-2.321-1.615.819 2.707-2.253 1.707 2.826.059.929 2.67zm6 3.423l.34-.688.759-.11-.549-.536.129-.756-.679.357-.679-.357.13.756-.55.536.76.11.339.688zm2-4l.34-.688.759-.11-.549-.536.129-.756-.679.357-.679-.357.13.756-.55.536.76.11.339.688zm0-4l.34-.688.759-.11-.549-.536.129-.756-.679.357-.679-.357.13.756-.55.536.76.11.339.688zm-2-3.999l.34-.69.759-.11-.549-.534.129-.757-.679.357-.679-.357.13.757-.55.535.76.11.339.689z"
fill="#ffde02"
d="M226.8 239.2l-9.7-15.6-17.9 4.4 11.9-14.1-9.7-15.6 17.1 6.9 11.8-14.1-1.3 18.4 17.1 6.9-17.9 4.4zM290.6 82l-10.1 15.4 11.6 14.3-17.7-4.8-10.1 15.5-1-18.4-17.7-4.8 17.2-6.6-1-18.4 11.6 14.3zm-54.4-56.6l-2 18.3 16.8 7.6-18 3.8-2 18.3-9.2-16-17.9 3.8 12.3-13.7-9.2-15.9 16.8 7.5zm56.6 136.4l-14.9 10.9 5.8 17.5-14.9-10.8-14.9 11 5.6-17.6-14.9-10.7 18.4-.1 5.6-17.6 5.8 17.5zM115 46.3l17.3 53.5h56.2l-45.4 32.9 17.3 53.5-45.4-33-45.5 33 17.4-53.5-45.5-32.9h56.3z"
fill="#FFDA44"
/>
</g>
<title>
China
</title>
</svg>
<svg
class="MuiSvgIcon-root emotion-15 emotion-12"
focusable="false"
role="img"
viewBox="0 0 512 512"
</svg>
</div>
<div
class="emotion-15 emotion-16"
>
<path
d="M473.655 88.276H38.345C17.167 88.276 0 105.443 0 126.621v73.471h512v-73.471c0-21.178-17.167-38.345-38.345-38.345zM0 385.379c0 21.177 17.167 38.345 38.345 38.345h435.31c21.177 0 38.345-17.167 38.345-38.345v-73.471H0v73.471z"
fill="#ff4b55"
/>
<path
d="M0 200.09h512V311.9H0z"
fill="#f5f5f5"
/>
<title>
Austria
</title>
</svg>
<svg
class="MuiSvgIcon-root emotion-15 emotion-12"
focusable="false"
role="img"
viewBox="0 0 512 512"
<svg
viewBox="0 0 513 342"
>
<path
d="M0 0h513v342H0z"
fill="#10338c"
/>
<g
fill="#FFF"
>
<path
d="M222.2 170.7c.3-.3.5-.6.8-.9-.2.3-.5.6-.8.9zM188 212.6l11 22.9 24.7-5.7-11 22.8 19.9 15.8-24.8 5.6.1 25.4-19.9-15.9-19.8 15.9.1-25.4-24.8-5.6 19.9-15.8-11.1-22.8 24.8 5.7zm197.9 28.5l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6v12.2l-9.4-7.6-9.5 7.6.1-12.2-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zm-48.6-116l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.9-2.7 9.5-7.5-5.3-10.9L332 136zm48.6-66.2l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.8-2.7 9.5-7.5-5.3-10.9 11.8 2.7zm42.5 49.7l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6V150l-9.4-7.6-9.5 7.6v-12.2l-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM398 166.5l4.1 12.7h13.3l-10.8 7.8 4.2 12.7-10.8-7.9-10.8 7.9 4.1-12.7-10.7-7.8h13.3z"
/>
<path
d="M254.8 0v30.6l-45.1 25.1h45.1V115h-59.1l59.1 32.8v22.9h-26.7l-73.5-40.9v40.9H99v-48.6l-87.4 48.6H-1.2v-30.6L44 115H-1.2V55.7h59.1L-1.2 22.8V0h26.7L99 40.8V0h55.6v48.6L242.1 0z"
/>
</g>
<path
d="M142.8 0h-32v69.3h-112v32h112v69.4h32v-69.4h112v-32h-112z"
fill="#D80027"
/>
<path
d="M154.6 115l100.2 55.7v-15.8L183 115z"
fill="#0052B4"
/>
<path
d="M154.6 115l100.2 55.7v-15.8L183 115z"
fill="#FFF"
/>
<path
d="M154.6 115l100.2 55.7v-15.8L183 115zm-83.9 0l-71.9 39.9v15.8L99 115z"
fill="#D80027"
/>
<path
d="M99 55.7L-1.2 0v15.7l71.9 40z"
fill="#0052B4"
/>
<path
d="M99 55.7L-1.2 0v15.7l71.9 40z"
fill="#FFF"
/>
<path
d="M99 55.7L-1.2 0v15.7l71.9 40zm84 0l71.8-40V0L154.6 55.7z"
fill="#D80027"
/>
</svg>
</div>
<div
class="emotion-15 emotion-16"
>
<path
d="M473.655 88.276H38.345C17.167 88.276 0 105.443 0 126.621v73.471h512v-73.471c0-21.178-17.167-38.345-38.345-38.345z"
fill="#464655"
/>
<path
d="M0 385.379c0 21.177 17.167 38.345 38.345 38.345h435.31c21.177 0 38.345-17.167 38.345-38.345v-73.471H0v73.471z"
fill="#ffe15a"
/>
<path
d="M0 200.09h512V311.9H0z"
fill="#ff4b55"
/>
<title>
Germany
</title>
</svg>
<svg
class="MuiSvgIcon-root emotion-15 emotion-12"
focusable="false"
role="img"
viewBox="-60 -40 240 160"
<svg
viewBox="0 85.333 512 341.333"
>
<path
d="M0 85.331h512v341.337H0z"
fill="#D80027"
/>
<path
d="M0 85.331h512v113.775H0z"
/>
<path
d="M0 312.882h512v113.775H0z"
fill="#FFDA44"
/>
</svg>
</div>
<div
class="emotion-15 emotion-16"
>
<rect
fill="#fe0000"
height="100%"
width="100%"
x="-60"
y="-40"
/>
<rect
fill="#000095"
height="50%"
width="50%"
x="-60"
y="-40"
/>
<path
d="M8 0L0 30-8 0l8-30M0 8l30-8L0-8l-30 8"
fill="#fff"
id="prefix__a"
/>
<use
transform="rotate(30)"
xlink:href="#prefix__a"
/>
<use
transform="rotate(60)"
xlink:href="#prefix__a"
/>
<circle
fill="#000095"
r="17"
/>
<circle
fill="#fff"
r="15"
/>
<title>
Taiwan
</title>
</svg>
<svg
viewBox="0 85.333 512 341.333"
>
<path
d="M0 85.337h512v341.326H0z"
fill="#D80027"
/>
<path
d="M0 85.337h256V256H0z"
fill="#0052B4"
/>
<path
d="M186.435 170.669L162.558 181.9l12.714 23.125-25.927-4.961-3.286 26.192L128 206.993l-18.06 19.263-3.285-26.192-25.927 4.96 12.714-23.125-23.877-11.23 23.877-11.231-12.714-23.125 25.927 4.96 3.286-26.192L128 134.344l18.06-19.263 3.285 26.192 25.928-4.96-12.715 23.125z"
fill="#FFF"
/>
<circle
cx="128"
cy="170.674"
fill="#0052B4"
r="29.006"
/>
<path
d="M128 190.06c-10.692 0-19.391-8.7-19.391-19.391 0-10.692 8.7-19.391 19.391-19.391 10.692 0 19.391 8.7 19.391 19.391 0 10.691-8.699 19.391-19.391 19.391z"
fill="#FFF"
/>
</svg>
</div>
</span>
</span>
</div>

View File

@@ -2,79 +2,78 @@ import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render,
renderWithStore,
fireEvent,
waitFor,
cleanup,
screen,
waitForElementToBeRemoved,
} from 'verdaccio-ui/utils/test-react-testing-library';
import translationEN from '../../../i18n/translations/en-US.json';
import { AppContextProvider } from '../../App';
import translationEN from '../../i18n/crowdin/ui.json';
import { store } from '../../store';
import Header from './Header';
const props = {
user: {
username: 'verddacio-user',
},
packages: [],
};
/* eslint-disable react/jsx-no-bind*/
describe('<Header /> component with logged in state', () => {
afterEach(cleanup);
test('should load the component in logged out state', () => {
render(
renderWithStore(
<Router>
<AppContextProvider>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
expect(screen.queryByTestId('header--menu-accountcircle')).toBeNull();
expect(screen.queryByTestId('logInDialogIcon')).toBeNull();
expect(screen.getByText('Login')).toBeTruthy();
expect(screen.queryByTestId('header--button-login')).toBeInTheDocument();
});
test('should load the component in logged in state', () => {
const { getByTestId, queryByText } = render(
test('should load the component in logged in state', async () => {
renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
store.dispatch.login.logInUser({ username: 'store', token: '12345' });
expect(getByTestId('header--menu-accountcircle')).toBeTruthy();
expect(queryByText('Login')).toBeNull();
await waitFor(() => {
expect(screen.getByTestId('logInDialogIcon')).toBeTruthy();
expect(screen.queryByText('Login')).toBeNull();
});
});
test('should open login dialog', async () => {
const { getByTestId } = render(
renderWithStore(
<Router>
<AppContextProvider>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const loginBtn = getByTestId('header--button-login');
store.dispatch.login.logOutUser();
const loginBtn = screen.getByTestId('header--button-login');
fireEvent.click(loginBtn);
const loginDialog = await waitFor(() => getByTestId('login--dialog'));
const loginDialog = await waitFor(() => screen.getByTestId('login--dialog'));
expect(loginDialog).toBeTruthy();
});
test('should logout the user', async () => {
const { getByText, getByTestId } = render(
const { getByText, getByTestId } = renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const headerMenuAccountCircle = getByTestId('header--menu-accountcircle');
store.dispatch.login.logInUser({ username: 'store', token: '12345' });
const headerMenuAccountCircle = getByTestId('logInDialogIcon');
fireEvent.click(headerMenuAccountCircle);
// wait for button Logout's appearance and return the element
@@ -84,12 +83,11 @@ describe('<Header /> component with logged in state', () => {
});
test("The question icon should open a new tab of verdaccio's website - installation doc", () => {
const { getByTestId } = render(
const { getByTestId } = renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const documentationBtn = getByTestId('header--tooltip-documentation');
@@ -99,12 +97,11 @@ describe('<Header /> component with logged in state', () => {
});
test('should open the registrationInfo modal when clicking on the info icon', async () => {
const { getByTestId } = render(
const { getByTestId } = renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const infoBtn = getByTestId('header--tooltip-info');
@@ -116,12 +113,11 @@ describe('<Header /> component with logged in state', () => {
});
test('should close the registrationInfo modal when clicking on the button close', async () => {
const { getByTestId, getByText, queryByTestId } = render(
const { getByTestId, getByText, queryByTestId } = renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
const infoBtn = getByTestId('header--tooltip-info');
@@ -138,17 +134,15 @@ describe('<Header /> component with logged in state', () => {
});
test('should hide login if is disabled', () => {
// @ts-expect-error
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
base: 'foo',
login: false,
};
render(
renderWithStore(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
<Header />
</Router>,
store
);
expect(screen.queryByTestId('header--button-login')).not.toBeInTheDocument();

View File

@@ -1,11 +1,11 @@
import React, { useState, useContext } from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'verdaccio-ui/components/Button';
import { useConfig } from 'verdaccio-ui/providers/config';
import storage from 'verdaccio-ui/utils/storage';
import AppContext from '../../App/AppContext';
import { Dispatch, RootState } from '../../store/store';
import HeaderInfoDialog from './HeaderInfoDialog';
import HeaderLeft from './HeaderLeft';
@@ -21,26 +21,15 @@ interface Props {
/* eslint-disable react/jsx-no-bind*/
const Header: React.FC<Props> = ({ withoutSearch }) => {
const { t } = useTranslation();
const appContext = useContext(AppContext);
const [isInfoDialogOpen, setOpenInfoDialog] = useState<boolean>(false);
const [showMobileNavBar, setShowMobileNavBar] = useState<boolean>(false);
const [showLoginModal, setShowLoginModal] = useState<boolean>(false);
if (!appContext) {
throw Error(t('app-context-not-correct-used'));
}
const { user, scope, setUser } = appContext;
const loginStore = useSelector((state: RootState) => state.login);
const configStore = useSelector((state: RootState) => state.configuration);
const { configOptions } = useConfig();
/**
* Logouts user
* Required by: <Header />
*/
const dispatch = useDispatch<Dispatch>();
const handleLogout = () => {
storage.removeItem('username');
storage.removeItem('token');
setUser(undefined);
dispatch.login.logOutUser();
};
return (
@@ -54,7 +43,7 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
onOpenRegistryInfoDialog={() => setOpenInfoDialog(true)}
onToggleLogin={() => setShowLoginModal(!showLoginModal)}
onToggleMobileNav={() => setShowMobileNavBar(!showMobileNavBar)}
username={user?.username}
username={loginStore?.username}
withoutSearch={withoutSearch}
/>
</InnerNavBar>
@@ -62,7 +51,7 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
isOpen={isInfoDialogOpen}
onCloseDialog={() => setOpenInfoDialog(false)}
registryUrl={configOptions.base}
scope={scope}
scope={configStore.scope}
/>
</NavBar>
{showMobileNavBar && !withoutSearch && (
@@ -75,7 +64,9 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
</Button>
</MobileNavBar>
)}
{!user && <LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />}
{!loginStore.user && (
<LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />
)}
</>
);
};

View File

@@ -30,7 +30,7 @@ const HeaderMenu: React.FC<Props> = ({
<>
<IconButton
color="inherit"
data-testid="header--menu-accountcircle"
data-testid="logInDialogIcon"
id="header--button-account"
onClick={onLoggedInMenu}>
<AccountCircle />
@@ -52,8 +52,8 @@ const HeaderMenu: React.FC<Props> = ({
</MenuItem>
<MenuItem
button={true}
data-testid="header--button-logout"
id="header--button-logout"
data-testid="logOutDialogIcon"
id="logOutDialogIcon"
onClick={onLogout}>
{t('button.logout')}
</MenuItem>

View File

@@ -1,86 +1,25 @@
/* eslint-disable react/jsx-pascal-case */
import styled from '@emotion/styled';
import { withStyles } from '@material-ui/core/styles';
import LanguageIcon from '@material-ui/icons/Language';
import i18next, { TFunctionKeys } from 'i18next';
import i18next from 'i18next';
import React, { useCallback, useContext, useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AutoComplete } from 'verdaccio-ui/components/AutoComplete/AutoCompleteV2';
import {
France,
Brazil,
Germany,
Spain,
China,
Russia,
Turkey,
Ukraine,
Khmer,
Japan,
Usa,
Czech,
Taiwan,
} from 'verdaccio-ui/components/Icons';
import MenuItem from 'verdaccio-ui/components/MenuItem';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import ThemeContext from 'verdaccio-ui/design-tokens/ThemeContext';
import { Language } from '../../../i18n/config';
import { Language, listLanguages } from '../../i18n/enabledLanguages';
const lngDetails: Record<Language, { translation: TFunctionKeys; icon: React.ReactElement }> = {
'fr-FR': {
translation: 'lng.french',
icon: <France size="md" />,
},
'pt-BR': {
translation: 'lng.portuguese',
icon: <Brazil size="md" />,
},
'de-DE': {
translation: 'lng.german',
icon: <Germany size="md" />,
},
'es-ES': {
translation: 'lng.spanish',
icon: <Spain size="md" />,
},
'zh-CN': {
translation: 'lng.chinese',
icon: <China size="md" />,
},
'ru-RU': {
translation: 'lng.russian',
icon: <Russia size="md" />,
},
'tr-TR': {
translation: 'lng.turkish',
icon: <Turkey size="md" />,
},
'uk-UA': {
translation: 'lng.ukraine',
icon: <Ukraine size="md" />,
},
'km-KH': {
translation: 'lng.khmer',
icon: <Khmer size="md" />,
},
'ja-JP': {
translation: 'lng.japanese',
icon: <Japan size="md" />,
},
'en-US': {
translation: 'lng.english',
icon: <Usa size="md" />,
},
'cs-CZ': {
translation: 'lng.czech',
icon: <Czech size="md" />,
},
'zh-TW': {
translation: 'lng.chineseTraditional',
icon: <Taiwan size="md" />,
},
};
const listConverted = listLanguages.reduce((prev, item) => {
prev[item.lng] = {
translation: item.menuKey,
icon: item.icon,
};
return prev;
}, {});
const LanguageSwitch = () => {
const themeContext = useContext(ThemeContext);
@@ -104,10 +43,10 @@ const LanguageSwitch = () => {
const getCurrentLngDetails = useCallback(
(language: Language) => {
const { icon, translation } = lngDetails[language] || lngDetails['en-US'];
const lng = listConverted[language] || listConverted['en-US'];
return {
icon,
translation: t(translation),
icon: <lng.icon />,
translation: t(lng.translation),
};
},
[t]

View File

@@ -2,7 +2,7 @@ import React from 'react';
import api from 'verdaccio-ui/providers/API/api';
import {
render,
renderWithStore,
waitFor,
fireEvent,
cleanup,
@@ -10,15 +10,10 @@ import {
act,
} from 'verdaccio-ui/utils/test-react-testing-library';
import AppContext, { AppContextProps } from '../../AppContext';
import { store } from '../../../store';
import LoginDialog from './LoginDialog';
const appContextValue: AppContextProps = {
scope: '',
setUser: jest.fn(),
};
describe('<LoginDialog /> component', () => {
beforeEach(() => {
jest.resetModules();
@@ -30,11 +25,7 @@ describe('<LoginDialog /> component', () => {
const props = {
onClose: jest.fn(),
};
const { container } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} />
</AppContext.Provider>
);
const { container } = renderWithStore(<LoginDialog onClose={props.onClose} />, store);
expect(container.firstChild).toMatchSnapshot();
});
@@ -44,10 +35,9 @@ describe('<LoginDialog /> component', () => {
onClose: jest.fn(),
};
const { getByTestId } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
const { getByTestId } = renderWithStore(
<LoginDialog onClose={props.onClose} open={props.open} />,
store
);
const loginDialogHeading = await waitFor(() => getByTestId('login-dialog-form-login-button'));
@@ -60,18 +50,18 @@ describe('<LoginDialog /> component', () => {
onClose: jest.fn(),
};
const { getByTestId } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
const { getByTestId } = renderWithStore(
<LoginDialog onClose={props.onClose} open={props.open} />,
store
);
const loginDialogButton = await waitFor(() => getByTestId('close-login-dialog-button'));
expect(loginDialogButton).toBeTruthy();
act(() => {
await act(() => {
fireEvent.click(loginDialogButton, { open: false });
});
expect(props.onClose).toHaveBeenCalled();
});
@@ -88,11 +78,9 @@ describe('<LoginDialog /> component', () => {
})
);
render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
);
await act(async () => {
renderWithStore(<LoginDialog onClose={props.onClose} open={props.open} />, store);
});
const userNameInput = screen.getByPlaceholderText('Your username');
expect(userNameInput).toBeInTheDocument();
@@ -104,13 +92,18 @@ describe('<LoginDialog /> component', () => {
const passwordInput = screen.getByPlaceholderText('Your strong password');
expect(userNameInput).toBeInTheDocument();
fireEvent.focus(passwordInput);
fireEvent.change(passwordInput, { target: { value: '1234' } });
act(async () => {
const signInButton = await screen.getByTestId('login-dialog-form-login-button');
expect(signInButton).not.toBeDisabled();
await act(async () => {
fireEvent.change(passwordInput, { target: { value: '1234' } });
});
const signInButton = screen.getByTestId('login-dialog-form-login-button');
expect(signInButton).not.toBeDisabled();
await act(async () => {
fireEvent.click(signInButton);
});
expect(props.onClose).toHaveBeenCalledTimes(1);
// screen.debug();
});
test.todo('validateCredentials: should validate credentials');

View File

@@ -1,15 +1,13 @@
import i18next from 'i18next';
import isEmpty from 'lodash/isEmpty';
import React, { useState, useContext, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import React, { useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Dialog from 'verdaccio-ui/components/Dialog';
import DialogContent from 'verdaccio-ui/components/DialogContent';
import { useAPI, LoginBody } from 'verdaccio-ui/providers/API/APIProvider';
import { LoginError } from 'verdaccio-ui/utils/login';
import storage from 'verdaccio-ui/utils/storage';
import AppContext from '../../../App/AppContext';
import { LoginBody } from '../../../store/models/login';
import { Dispatch, RootState } from '../../../store/store';
import LoginDialogCloseButton from './LoginDialogCloseButton';
import LoginDialogForm, { FormValues } from './LoginDialogForm';
@@ -21,62 +19,48 @@ interface Props {
}
const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
const { t } = useTranslation();
const appContext = useContext(AppContext);
const { doLogin } = useAPI();
const loginStore = useSelector((state: RootState) => state.login);
const dispatch = useDispatch<Dispatch>();
const makeLogin = useCallback(
async (username?: string, password?: string): Promise<LoginBody> => {
async (username?: string, password?: string): Promise<LoginBody | void> => {
// checks isEmpty
if (isEmpty(username) || isEmpty(password)) {
const error = {
dispatch.login.addError({
type: 'error',
description: i18next.t('form-validation.username-or-password-cant-be-empty'),
};
return { error };
});
return;
}
try {
const response: LoginBody = await doLogin(username as string, password as string);
return response;
dispatch.login.getUser({ username, password });
// const response: LoginBody = await doLogin(username as string, password as string);
dispatch.login.clearError();
} catch (e: any) {
// eslint-disable-next-line no-console
console.error('login error', e.message);
const error = {
dispatch.login.addError({
type: 'error',
description: i18next.t('form-validation.unable-to-sign-in'),
};
return { error };
});
// eslint-disable-next-line no-console
console.error('login error', e.message);
}
},
[doLogin]
[dispatch]
);
if (!appContext) {
throw Error(t('app-context-not-correct-used'));
}
const [error, setError] = useState<LoginError>();
const handleDoLogin = useCallback(
async (data: FormValues) => {
const { username, token, error } = await makeLogin(data.username, data.password);
if (error) {
setError(error);
}
if (username && token) {
storage.setItem('username', username);
storage.setItem('token', token);
appContext.setUser({ username });
onClose();
}
await makeLogin(data.username, data.password);
},
[appContext, onClose, makeLogin]
[makeLogin]
);
useEffect(() => {
if (loginStore.token && typeof loginStore.error === 'undefined') {
onClose();
}
}, [loginStore, onClose]);
return (
<Dialog
data-testid="login--dialog"
@@ -88,7 +72,7 @@ const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
<LoginDialogCloseButton onClose={onClose} />
<DialogContent>
<LoginDialogHeader />
<LoginDialogForm error={error} onSubmit={handleDoLogin} />
<LoginDialogForm error={loginStore.error} onSubmit={handleDoLogin} />
</DialogContent>
</Dialog>
);

View File

@@ -18,7 +18,6 @@ describe('<RegistryInfoContent /> component', () => {
const props = { registryUrl: 'http://localhost:4872', scope: '@' };
render(<RegistryInfoContent registryUrl={props.registryUrl} scope={props.scope} />);
screen.debug();
expect(screen.getByText('pnpm set @:registry http://localhost:4872')).toBeInTheDocument();
expect(screen.getByText('pnpm adduser --registry http://localhost:4872')).toBeInTheDocument();
expect(

View File

@@ -2,7 +2,9 @@ import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import api from 'verdaccio-ui/providers/API/api';
import { render, fireEvent, waitFor } from 'verdaccio-ui/utils/test-react-testing-library';
import { renderWithStore, fireEvent, waitFor } from 'verdaccio-ui/utils/test-react-testing-library';
import { store } from '../../../store/store';
jest.mock('lodash/debounce', () =>
jest.fn((fn) => {
@@ -41,12 +43,15 @@ describe('<Search /> component', () => {
});
test('should load the component in default state', () => {
const { container } = render(<ComponentToBeRendered />);
const { container } = renderWithStore(<ComponentToBeRendered />, store);
expect(container.firstChild).toMatchSnapshot();
});
test('handleSearch: when user type package name in search component, show suggestions', async () => {
const { getByPlaceholderText, getAllByText } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getAllByText } = renderWithStore(
<ComponentToBeRendered />,
store
);
const autoCompleteInput = getByPlaceholderText('Search Packages');
@@ -62,7 +67,10 @@ describe('<Search /> component', () => {
});
test('onBlur: should cancel all search requests', async () => {
const { getByPlaceholderText, getByRole, getAllByText } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getByRole, getAllByText } = renderWithStore(
<ComponentToBeRendered />,
store
);
const autoCompleteInput = getByPlaceholderText('Search Packages');
@@ -80,7 +88,7 @@ describe('<Search /> component', () => {
});
test('handleSearch: cancel all search requests when there is no value in search component with type method', async () => {
const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getByRole } = renderWithStore(<ComponentToBeRendered />, store);
const autoCompleteInput = getByPlaceholderText('Search Packages');
fireEvent.focus(autoCompleteInput);
@@ -92,7 +100,7 @@ describe('<Search /> component', () => {
});
test('handleSearch: when method is not type method', async () => {
const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getByRole } = renderWithStore(<ComponentToBeRendered />, store);
const autoCompleteInput = getByPlaceholderText('Search Packages');
@@ -105,7 +113,7 @@ describe('<Search /> component', () => {
});
test('handleSearch: loading is been displayed', async () => {
const { getByPlaceholderText, getByText } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getByText } = renderWithStore(<ComponentToBeRendered />, store);
const autoCompleteInput = getByPlaceholderText('Search Packages');
fireEvent.focus(autoCompleteInput);
@@ -117,7 +125,10 @@ describe('<Search /> component', () => {
});
test('handlePackagesClearRequested: should clear suggestions', async () => {
const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getAllByText, getByRole } = renderWithStore(
<ComponentToBeRendered />,
store
);
const autoCompleteInput = getByPlaceholderText('Search Packages');
fireEvent.focus(autoCompleteInput);
@@ -135,7 +146,10 @@ describe('<Search /> component', () => {
});
test('handleClickSearch: should change the window location on click or return key', async () => {
const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
const { getByPlaceholderText, getAllByText, getByRole } = renderWithStore(
<ComponentToBeRendered />,
store
);
const autoCompleteInput = getByPlaceholderText('Search Packages');
fireEvent.focus(autoCompleteInput);

View File

@@ -1,11 +1,13 @@
import debounce from 'lodash/debounce';
import React, { useState, FormEvent, useCallback, useRef, useEffect } from 'react';
import React, { useState, FormEvent, useCallback } from 'react';
import { SuggestionSelectedEventData } from 'react-autosuggest';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router';
import AutoComplete from 'verdaccio-ui/components/AutoComplete';
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
import { Dispatch, RootState } from '../../../store/store';
import SearchAdornment from './SearchAdornment';
@@ -16,22 +18,18 @@ const CONSTANTS = {
const Search: React.FC<RouteComponentProps> = ({ history }) => {
const { t } = useTranslation();
const [suggestions, setSuggestions] = useState([]);
const [loaded, setLoaded] = useState(false);
const [search, setSearch] = useState('');
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const mountedRef = useRef(true);
const [requestList, setRequestList] = useState<{ abort: () => void }[]>([]);
const { callSearch } = useAPI();
// const mountedRef = useRef(true);
const { isError, suggestions } = useSelector((state: RootState) => state.search);
const isLoading = useSelector((state: RootState) => state?.loading?.models.search);
const dispatch = useDispatch<Dispatch>();
/**
* Cancel all the requests which are in pending state.
*/
const cancelAllSearchRequests = useCallback(() => {
requestList.forEach((request) => request.abort());
setRequestList([]);
}, [requestList, setRequestList]);
dispatch.search.clearRequestQueue();
}, [dispatch]);
/**
* As user focuses out from input, we cancel all the request from requestList
@@ -41,12 +39,9 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
(event: FormEvent<HTMLInputElement>) => {
// stops event bubbling
event.stopPropagation();
setLoaded(false);
setLoading(false);
setError(false);
cancelAllSearchRequests();
},
[setLoaded, setLoading, cancelAllSearchRequests, setError]
[cancelAllSearchRequests]
);
/**
@@ -58,9 +53,6 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
event.stopPropagation();
if (method === 'type') {
const value = newValue.trim();
setLoading(true);
setError(false);
setSearch(value);
setLoaded(false);
/**
@@ -79,8 +71,8 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
* Cancel all the request from list and make request list empty.
*/
const handlePackagesClearRequested = useCallback(() => {
setSuggestions([]);
}, [setSuggestions]);
dispatch.search.saveSearch({ suggestions: [] });
}, [dispatch]);
/**
* When an user select any package by clicking or pressing return key.
@@ -108,46 +100,12 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
* For AbortController see: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
*/
const handleFetchPackages = useCallback(
async ({ value }: { value: string }) => {
try {
const controller = new window.AbortController();
const signal = controller.signal;
if (!mountedRef.current) {
return null;
}
// Keep track of search requests.
setRequestList([...requestList, controller]);
const suggestions = await callSearch(value, signal);
// FIXME: Argument of type 'unknown' is not assignable to parameter of type 'SetStateAction<never[]>'
setSuggestions(suggestions as any);
setLoaded(true);
} catch (error: any) {
/**
* AbortError is not the API error.
* It means browser has cancelled the API request.
*/
if (error.name === CONSTANTS.ABORT_ERROR) {
setError(false);
setLoaded(false);
} else {
setError(true);
setLoaded(false);
}
} finally {
if (mountedRef.current) {
setLoading(false);
}
}
({ value }: { value: string }) => {
dispatch.search.getSuggestions({ value });
},
[requestList, setRequestList, setSuggestions, setLoaded, setError, setLoading, callSearch]
[dispatch]
);
useEffect(() => {
return () => {
mountedRef.current = false;
};
}, []);
return (
<AutoComplete
onBlur={handleOnBlur}
@@ -158,9 +116,9 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
placeholder={t('search.packages')}
startAdornment={<SearchAdornment />}
suggestions={suggestions}
suggestionsError={error}
suggestionsError={isError}
suggestionsLoaded={loaded}
suggestionsLoading={loading}
suggestionsLoading={isLoading}
value={search}
/>
);

View File

@@ -1,2 +1 @@
export { default } from './App';
export { default as AppContextProvider } from './AppContextProvider';

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { render, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
import { renderWithStore, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
import { DetailContext, DetailContextProps } from '../../pages/Version';
import { store } from '../../store/store';
import ActionBar from './ActionBar';
@@ -44,7 +45,10 @@ describe('<ActionBar /> component', () => {
});
test('should render the component in default state', () => {
const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
const { container } = renderWithStore(
<ComponentToBeRendered contextValue={detailContextValue} />,
store
);
expect(container.firstChild).toMatchSnapshot();
});
@@ -62,22 +66,25 @@ describe('<ActionBar /> component', () => {
},
};
const { container } = render(
<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />
const { container } = renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />,
store
);
expect(container.firstChild).toMatchSnapshot();
});
test('when there is a button to download a tarball', () => {
const { getByTitle } = render(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
const { getByTitle } = renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />,
store
);
expect(getByTitle('Download tarball')).toBeTruthy();
});
test('when there is a button to open an issue', () => {
const { getByTitle } = render(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
const { getByTitle } = renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />,
store
);
expect(getByTitle('Open an issue')).toBeTruthy();
});

View File

@@ -4,11 +4,11 @@ import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
import { extractFileName, downloadFile } from 'verdaccio-ui/utils/url';
import { Dispatch } from '../../store/store';
import FloatingActionButton from '../FloatingActionButton';
import Link from '../Link';
import Tooltip from '../Tooltip';
@@ -34,13 +34,11 @@ export interface ActionBarActionProps {
/* eslint-disable react/jsx-no-bind */
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
const { t } = useTranslation();
const { getResource } = useAPI();
const dispatch = useDispatch<Dispatch>();
const handleDownload = useCallback(async () => {
const fileStream = await getResource(link);
const fileName = extractFileName(link);
downloadFile(fileStream, fileName);
}, [getResource, link]);
dispatch.download.getTarball({ link });
}, [dispatch, link]);
switch (type) {
case 'VISIT_HOMEPAGE':

View File

@@ -51,7 +51,6 @@ describe('<Author /> component', () => {
};
const wrapper = render(withAuthorComponent(packageMeta));
wrapper.debug();
expect(wrapper.queryAllByText('verdaccio')).toHaveLength(0);
});

View File

@@ -37,7 +37,7 @@ interface Props<Option extends {}> extends Pick<TextFieldProps, 'variant'> {
inputStartAdornment?: React.ReactNode;
hasClearIcon?: boolean;
className?: string;
onClick?: (option: Option) => void;
onClick?: (option: any) => void;
}
const AutoComplete = <Option extends {}>({

View File

@@ -1,23 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Austria = React.forwardRef(function Austria(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.austria')}>
<path
d="M473.655 88.276H38.345C17.167 88.276 0 105.443 0 126.621v73.471h512v-73.471c0-21.178-17.167-38.345-38.345-38.345zM0 385.379c0 21.177 17.167 38.345 38.345 38.345h435.31c21.177 0 38.345-17.167 38.345-38.345v-73.471H0v73.471z"
fill="#ff4b55"
/>
<path fill="#f5f5f5" d="M0 200.09h512V311.9H0z" />
</SvgIcon>
);
});
Austria.displayName = 'Austria';
export { Austria };

View File

@@ -1,44 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Brazil = React.forwardRef(function Brazil(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 45 45" {...props} ref={ref} title={t('flag.brazil')}>
<defs>
<clipPath id="prefix__a">
<path d="M0 36h36V0H0v36z" />
</clipPath>
</defs>
<g clipPath="url(#prefix__a)" transform="matrix(1.25 0 0 -1.25 0 45)">
<path
d="M36 9a4 4 0 00-4-4H4a4 4 0 00-4 4v18a4 4 0 004 4h28a4 4 0 004-4V9z"
fill="#009b3a"
/>
<path d="M32.727 18L18 6.876 3.27 18 18 29.125 32.727 18z" fill="#fedf01" />
<path
d="M24.434 18.076a6.458 6.458 0 11-12.917 0 6.458 6.458 0 0112.917 0"
fill="#002776"
/>
<path
d="M12.277 21.113a6.406 6.406 0 01-.672-2.023c3.994.29 9.417-1.892 11.744-4.596.402.604.7 1.28.882 2.004-2.871 2.809-7.916 4.63-11.954 4.615"
fill="#cbe9d4"
/>
<path d="M13 16.767h-1v1h1v-1zm1-2h-1v1h1v-1z" fill="#88c9f9" />
<path
d="M16 16.767h-1v1h1v-1zm2-1h-1v1h1v-1zm4-2h-1v1h1v-1zm-3-1h-1v1h1v-1zm3 6h-1v1h1v-1z"
fill="#55acee"
/>
<path d="M20 14.767h-1v1h1v-1z" fill="#3b88c3" />
</g>
</SvgIcon>
);
});
Brazil.displayName = 'Brazil';
export { Brazil };

View File

@@ -1,33 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const China = React.forwardRef(function China(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 45 45" {...props} ref={ref} title={t('flag.china')}>
<defs>
<clipPath id="prefix__a">
<path d="M0 36h36V0H0v36z" />
</clipPath>
</defs>
<g clipPath="url(#prefix__a)" transform="matrix(1.25 0 0 -1.25 0 45)">
<path
d="M36 9a4 4 0 00-4-4H4a4 4 0 00-4 4v18a4 4 0 004 4h28a4 4 0 004-4V9z"
fill="#de2910"
/>
<path
d="M7 25.049l.929-2.67 2.826-.06-2.253-1.706.819-2.707L7 19.52l-2.321-1.615.819 2.707-2.253 1.707 2.826.059.929 2.67zm6 3.423l.34-.688.759-.11-.549-.536.129-.756-.679.357-.679-.357.13.756-.55.536.76.11.339.688zm2-4l.34-.688.759-.11-.549-.536.129-.756-.679.357-.679-.357.13.756-.55.536.76.11.339.688zm0-4l.34-.688.759-.11-.549-.536.129-.756-.679.357-.679-.357.13.756-.55.536.76.11.339.688zm-2-3.999l.34-.69.759-.11-.549-.534.129-.757-.679.357-.679-.357.13.757-.55.535.76.11.339.689z"
fill="#ffde02"
/>
</g>
</SvgIcon>
);
});
China.displayName = 'China';
export { China };

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Czech = React.forwardRef(function Czech(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 45 45" {...props} ref={ref} title={t('flag.czech')}>
<path fill="#d7141a" d="M0 22.5h45v16H0z" />
<path fill="#fff" d="M0 6.5h45v16H0z" />
<path d="M22.5 22.5L0 6.5v32z" fill="#11457e" />
</SvgIcon>
);
});
Czech.displayName = 'Czech';
export { Czech };

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const France = React.forwardRef(function France(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.france')}>
<path
d="M38.345 88.273C17.167 88.273 0 105.44 0 126.618v258.759c0 21.177 17.167 38.345 38.345 38.345h132.322V88.273H38.345z"
fill="#41479b"
/>
<path fill="#f5f5f5" d="M170.67 88.277h170.67v335.45H170.67z" />
<path
d="M473.655 88.273H341.333v335.448h132.322c21.177 0 38.345-17.167 38.345-38.345V126.618c0-21.178-17.167-38.345-38.345-38.345z"
fill="#ff4b55"
/>
</SvgIcon>
);
});
France.displayName = 'France';
export { France };

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Germany = React.forwardRef(function Germany(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.germany')}>
<path
d="M473.655 88.276H38.345C17.167 88.276 0 105.443 0 126.621v73.471h512v-73.471c0-21.178-17.167-38.345-38.345-38.345z"
fill="#464655"
/>
<path
d="M0 385.379c0 21.177 17.167 38.345 38.345 38.345h435.31c21.177 0 38.345-17.167 38.345-38.345v-73.471H0v73.471z"
fill="#ffe15a"
/>
<path fill="#ff4b55" d="M0 200.09h512V311.9H0z" />
</SvgIcon>
);
});
Germany.displayName = 'Germany';
export { Germany };

View File

@@ -1,44 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const India = React.forwardRef(function India(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.india')}>
<path
d="M0 384c0 31.418 25.473 56.889 56.889 56.889H455.11c31.42 0 56.89-25.473 56.89-56.889v-56.889H0V384z"
fill="#138808"
/>
<path d="M0 327.111h512V184.89H0v142.22z" fill="#eee" />
<path
d="M512 184.889V128c0-31.417-25.473-56.889-56.889-56.889H56.89C25.472 71.111 0 96.583 0 128v56.889h512z"
fill="#f93"
/>
<path
d="M312.889 256c0-31.431-25.473-56.902-56.903-56.902-31.417 0-56.888 25.472-56.888 56.902 0 31.418 25.472 56.889 56.888 56.889 31.432 0 56.903-25.472 56.903-56.889"
fill="navy"
/>
<path
d="M298.666 256c0-23.566-19.115-42.681-42.681-42.681S213.319 232.434 213.319 256s19.1 42.666 42.666 42.666 42.681-19.1 42.681-42.666"
fill="#eee"
/>
<path
d="M256 213.334l2.076 32.199 14.251-28.943-10.396 30.535 24.235-21.305-21.291 24.249 30.535-10.396-28.942 14.25L298.666 256l-32.198 2.076 28.942 14.237-30.535-10.383 21.291 24.235-24.235-21.29 10.396 30.535-14.25-28.942L256 298.666l-2.076-32.198-14.252 28.942 10.397-30.535-24.249 21.291 21.305-24.235-30.535 10.383 28.942-14.236L213.334 256l32.199-2.076-28.943-14.251 30.535 10.396-21.305-24.249 24.249 21.305-10.396-30.535 14.25 28.943 2.077-32.2z"
fill="navy"
opacity={0.6}
/>
<path
d="M241.778 256c0-7.851 6.37-14.223 14.222-14.223s14.223 6.372 14.223 14.223-6.372 14.223-14.223 14.223-14.223-6.372-14.223-14.223"
fill="navy"
/>
</SvgIcon>
);
});
India.displayName = 'India';
export { India };

View File

@@ -1,23 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Japan = React.forwardRef(function Japan(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.japan')}>
<path
d="M473.655 88.275H38.345C17.167 88.275 0 105.442 0 126.62v258.76c0 21.177 17.167 38.345 38.345 38.345h435.31c21.177 0 38.345-17.167 38.345-38.345V126.62c0-21.178-17.167-38.345-38.345-38.345z"
fill="#f5f5f5"
/>
<circle cx={256} cy={255.999} r={97.1} fill="#ff4b55" />
</SvgIcon>
);
});
Japan.displayName = 'Japan';
export { Japan };

View File

@@ -1,40 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Khmer = React.forwardRef(function Khmer(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.khmer')}>
<path
d="M0 304.8v64.8a48.3 48.3 0 0048 48.8h400c26.4 0 48-21.6 48-48.8v-64.8H0zM448 77.6H48c-26.4 0-48 21.6-48 48.8v64.8h496v-64.8a48.3 48.3 0 00-48-48.8z"
fill="#032ea1"
/>
<path fill="#e00025" d="M0 189.6h496v116H0z" />
<path
d="M448 418.4c26.4 0 48-21.6 48-48.8v-64.8H315.2L448 418.4zm0-340.8H48l132.8 113.6H496v-64.8a48.3 48.3 0 00-48-48.8z"
fill="#042a7f"
/>
<path fill="#b50030" d="M316 306.4h180V189.6H180z" />
<path fill="#002566" d="M448 77.6H48l370.4 113.6H496v-64.8a48.3 48.3 0 00-48-48.8z" />
<path fill="#8c002b" d="M496 214.4v-24.8h-81.6z" />
<path fill="#002566" d="M496 369.6a48.3 48.3 0 01-48 48.8H48c-26.4 0-48-21.6-48-48.8" />
<g fill="#fff">
<path d="M249.6 200s0-2.4-1.6-2.4-1.6 2.4-1.6 2.4h3.2zm72 90.4v-7.2h-24 20v-6.4h-4V272h-16H312v-4h-3.2c-.8-.8-2.4-1.6-2.4-3.2v-12c.8-1.6 1.6-3.2 2.4-3.2v-11.2c-1.6 0-2.4 1.6-2.4 1.6v-4.8h-1.6v9.6-6.4h-2.4 2.4v-1.6h-2.4v-5.6c-.8 0-.8 2.4-2.4 2.4-.8 0-.8-.8 0-1.6s.8-1.6 0-4c-.8 1.6-1.6 1.6-1.6.8.8-1.6 1.6-1.6.8-4 0 2.4-1.6 2.4-1.6.8 0-1.6.8-1.6 0-4-.8 2.4-1.6 2.4-1.6.8 0-2.4 0-4-2.4-4.8 0 0 0-1.6-.8-1.6s-.8 1.6-.8 1.6c-2.4.8-2.4 3.2-2.4 4.8s-.8 1.6-1.6-.8c-.8 1.6 0 2.4 0 4s-.8 1.6-1.6-.8c-.8 2.4 0 2.4.8 4 0 .8-.8.8-1.6-.8-.8 1.6 0 3.2 0 4 .8.8.8 1.6 0 1.6-1.6 0-1.6-2.4-2.4-2.4v9.6H280v-.8h-1.6v.8H264V232c-1.6.8-1.6 1.6-1.6 1.6v7.2-5.6h-1.6s-.8 0-.8-1.6.8-2.4 1.6-2.4V228s-3.2 0-3.2 4v-3.2c-.8 0-.8.8-.8 4h-2.4 1.6v-3.2c0-1.6 1.6-1.6 1.6-3.2V224c-.8 2.4-1.6 1.6-1.6.8s.8-1.6 2.4-4c.8-.8 0-2.4-.8-3.2 0 1.6-.8 2.4-1.6 2.4s-.8 0-.8-1.6c0-.8.8-1.6.8-3.2.8-1.6 0-2.4-.8-3.2 0 1.6 0 2.4-.8 2.4-1.6-.8 0-3.2 0-3.2 0-.8-.8-2.4-.8-2.4-.8 1.6-.8 1.6-1.6 1.6s-.8-1.6 0-2.4c.8-.8.8-1.6 0-1.6-.8 1.6-1.6.8-1.6 0l.8-2.4h-10.4l.8 2.4c0 .8-.8.8-1.6 0-.8.8-.8 1.6 0 1.6.8.8.8 1.6 0 2.4-.8 0-.8 0-1.6-1.6 0 0-.8 1.6-.8 2.4 0 .8 1.6 3.2 0 3.2-.8 0-.8-.8-.8-2.4-.8.8-.8 1.6-.8 3.2 0 .8.8 1.6.8 3.2 0 .8-.8 1.6-.8 1.6-.8 0-.8-.8-1.6-2.4-.8.8-.8 3.2-.8 3.2 1.6 2.4 2.4 3.2 2.4 4s-.8 1.6-1.6-.8c-.8.8 0 2.4 0 2.4 0 1.6 1.6 1.6 1.6 3.2v3.2h1.6-2.4c0-3.2 0-4-.8-4v3.2c0-3.2-3.2-3.2-3.2-3.2v3.2c.8 0 1.6.8 1.6 2.4s-.8 1.6-.8 1.6h-1.6v5.6-7.2s0-1.6-1.6-1.6v8.8h-14.4V240h-1.6v.8h-3.2v-9.6c-.8 0-.8 2.4-2.4 2.4-.8 0-.8-.8 0-1.6s.8-1.6 0-4c-.8 1.6-1.6 1.6-1.6.8.8-1.6 1.6-1.6.8-4 0 2.4-1.6 2.4-1.6.8 0-1.6.8-1.6 0-4-.8 2.4-1.6 2.4-1.6.8 0-2.4 0-4-2.4-4.8 0 0 0-1.6-.8-1.6s-.8 1.6-.8 1.6c-2.4.8-2.4 3.2-2.4 4.8s-.8 1.6-1.6-.8c-.8 1.6 0 2.4 0 4s-.8 1.6-1.6-.8c-.8 2.4 0 2.4.8 4 0 .8-.8.8-1.6-.8-.8 1.6 0 3.2 0 4 .8.8.8 1.6 0 1.6-1.6 0-1.6-2.4-2.4-2.4v5.6h-2.4v-.8h-1.6v4s-.8-1.6-2.4-1.6v12c.8 0 2.4 1.6 2.4 3.2v12c0 1.6-1.6 2.4-2.4 3.2h-.8v3.2h13.6-16v4.8h-4v6.4h20-24v7.2h-4v7.2h156v-7.2h-4zm-16.8-25.6zm-2.4-17.6v.8-.8zm-3.2.8h4-4zm-105.6-9.6h-2.4 2.4zm3.2 10.4h-3.2 3.2zm12-.8h4-4zm-.8 24h35.2H208zm35.2 11.2H208h35.2zm.8-50.4zm8 0zm32 15.2h3.2-3.2c-.8.8-.8 0 0 0zm4 35.2h-36 36zm0-11.2h-36 36z" />
<path d="M252.8 205.6v-2.4h-9.6v2.4zm-.8-2.4v-1.6h-8v1.6zm-1.6-1.6V200h-4.8v1.6z" />
</g>
<g fill="#cecece">
<path d="M231.2 232v2.4l1.6 1.6v-1.6c0-.8 0-2.4-1.6-2.4z" />
<path d="M321.6 283.2h-24 20v-6.4h-4V272h-16H312v-4h-3.2c-.8-.8-2.4-1.6-2.4-3.2v-12c.8-1.6 1.6-3.2 2.4-3.2v-11.2c-1.6 0-2.4 1.6-2.4 1.6v-4.8h-1.6v9.6-6.4h-2.4 2.4v-1.6h-2.4v-5.6c-.8 0-.8 2.4-2.4 2.4-.8 0-.8-.8 0-1.6s.8-1.6 0-4c-.8 1.6-1.6 1.6-1.6.8.8-1.6 1.6-1.6.8-4 0 2.4-1.6 2.4-1.6.8 0-1.6.8-1.6 0-4-.8 2.4-1.6 2.4-1.6.8 0-2.4 0-4-2.4-4.8 0 0 0-1.6-.8-1.6s-.8 1.6-.8 1.6c-2.4.8-2.4 3.2-2.4 4.8s-.8 1.6-1.6-.8c-.8 1.6 0 2.4 0 4s-.8 1.6-1.6-.8c-.8 2.4 0 2.4.8 4 0 .8-.8.8-1.6-.8-.8 1.6 0 3.2 0 4 .8.8.8 1.6 0 1.6-1.6 0-1.6-2.4-2.4-2.4v9.6H280v-.8h-1.6v.8H264V232c-1.6.8-1.6 1.6-1.6 1.6v7.2-5.6h-1.6s-.8 0-.8-1.6.8-2.4 1.6-2.4V228s-3.2 0-3.2 4v-3.2c-.8 0-.8.8-.8 4h-.8v-3.2c0-1.6 1.6-1.6 1.6-3.2V224c-.8 2.4-1.6 1.6-1.6.8s.8-1.6 2.4-4c.8-.8 0-2.4-.8-3.2 0 1.6-.8 2.4-1.6 2.4s-.8 0-.8-1.6c0-.8.8-1.6.8-3.2.8-1.6 0-2.4-.8-3.2 0 1.6 0 2.4-.8 2.4-1.6-.8 0-3.2 0-3.2 0-.8-.8-2.4-.8-2.4-.8 1.6-.8 1.6-1.6 1.6s-.8-1.6 0-2.4c.8-.8.8-1.6 0-1.6-.8 1.6-1.6.8-1.6 0l.8-2.4h-10.4l.8 2.4c0 .8-.8.8-1.6 0-.8.8-.8 1.6 0 1.6.8.8.8 1.6 0 2.4-.8 0-.8 0-1.6-1.6 0 0-.8 1.6-.8 2.4 0 .8 1.6 3.2 0 3.2-.8 0-.8-.8-.8-2.4-.8.8-.8 1.6-.8 3.2 0 .8.8 1.6.8 3.2 0 .8-.8 1.6-.8 1.6-.8 0-.8-.8-1.6-2.4-.8.8-.8 3.2-.8 3.2 1.6 2.4 2.4 3.2 2.4 4s-.8 1.6-1.6-.8c-.8.8 0 2.4 0 2.4 0 1.6 1.6 1.6 1.6 3.2v3.2h-.8c0-3.2 0-4-.8-4v3.2c0-3.2-3.2-3.2-3.2-3.2v3.2c.8 0 1.6.8 1.6 2.4s-.8 1.6-.8 1.6h-1.6l42.4 36.8H288h-12l31.2 26.4h19.2V292h-4v-8.8h-.8zm-19.2-36v.8-.8zm-3.2.8h4-4zm-12 .8h-4 4zM248 197.6c-1.6 0-1.6 2.4-1.6 2.4h2.4c.8 0 .8-2.4-.8-2.4z" />
<path d="M243.2 203.2v2.4h9.6v-2.4zm8.8 0v-1.6h-8v1.6zm-1.6-1.6V200h-4.8v1.6z" />
</g>
</SvgIcon>
);
});
Khmer.displayName = 'Khmer';
export { Khmer };

View File

@@ -1,66 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Nicaragua = React.forwardRef(function Nicaragua(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.nicaragua')}>
<path
d="M512 384c0 31.418-25.473 56.889-56.889 56.889H56.89C25.472 440.889 0 415.417 0 384V128c0-31.418 25.472-56.889 56.889-56.889H455.11C486.53 71.111 512 96.584 512 128v256z"
fill="#265fb5"
/>
<path d="M512 327.111H0V184.89h512v142.22z" fill="#eee" />
<path
d="M320.811 256c0 35.797-29.014 64.811-64.811 64.811-35.783 0-64.811-29.014-64.811-64.811s29.027-64.811 64.811-64.811c35.797 0 64.811 29.013 64.811 64.811"
fill="#a9bf4c"
/>
<path
d="M312.889 256c0 31.418-25.473 56.889-56.889 56.889S199.111 287.416 199.111 256s25.473-56.889 56.889-56.889 56.889 25.471 56.889 56.889"
fill="#eee"
/>
<path d="M209.891 286.649l45.909-79.517 45.909 79.517H209.89z" fill="#265fb5" />
<path d="M215.04 283.591l40.818-70.685 40.803 70.685H215.04z" fill="#55acee" />
<path d="M215.04 283.591l9.841-17.052 61.483-.783 10.297 17.835H215.04z" fill="#bbddf5" />
<path
d="M222.891 272.441l15.331-12.445 6.67 7.553 5.774-6.215 4.893 4.892 4.665-5.12 5.561 5.12 5.106-5.334 4.665 5.334 4.451-4.892 7.325 9.102 1.338 2.674s-7.78 1.55-18.446.669c-10.667-.896-16.882 1.55-25.33 2.66-8.448 1.109-23.553-1.109-23.553-1.109l1.55-2.889z"
fill="#5c913b"
/>
<path
d="M237.995 262.67l10.226 11.107-5.12.442-5.774-8.434.668-3.115zm12.231.883l8.22 7.338-3.782.654-3.996-5.546-.442-2.446zm10.439-.442l7.111 6.67L266 270.89l-5.774-6.229.44-1.55zm9.33 0l-2.888 2.66 1.338 1.565 1.55-4.225zm8.889.228l-3.328 4.224 1.109 1.99 2.446-4.664-.227-1.55z"
fill="#e2f09f"
/>
<path
d="M256.426 233.671l.939 13.227 7.566-10.867-5.675 11.805 11.805-5.66-10.851 7.553 13.213.939-13.213.952 10.851 7.553-11.805-5.66 5.675 11.805-7.566-10.867-.939 13.226-.939-13.226-7.566 10.867 5.675-11.805-11.805 5.66 10.851-7.553-13.212-.953 13.212-.938-10.85-7.553 11.804 5.66-5.675-11.805 7.566 10.866.94-13.226z"
fill="#bbddf5"
/>
<path
d="M256 244.665l-2.66 2.66s.654 4.011.441 4.679C253.554 252.658 256 256 256 256l3.327-3.996-.88-5.334-2.447-2.005z"
fill="#dd2e44"
/>
<path
d="M257.28 240.071c10.894 0 17.109 5.334 17.109 5.334l-3.996-7.111s-6.443-4.893-13.995-4.893c-7.567 0-16 6.001-16 6.001l-3.783 7.553s9.771-6.884 20.665-6.884"
fill="#269"
/>
<path
d="M257.28 240.071c10.894 0 17.109 5.334 17.109 5.334l-2.888-5.106s-7.78-4.665-15.331-4.665-16.896 5.987-16.896 5.987l-2.66 5.334c.001 0 9.772-6.884 20.666-6.884"
fill="#ffcc4d"
/>
<path
d="M257.28 240.071c10.894 0 17.109 5.334 17.109 5.334l-2.005-3.327s-5.988-4.224-16.214-3.783c-7.553 0-18.005 5.561-18.005 5.561l-1.55 3.1c0-.001 9.771-6.885 20.665-6.885"
fill="#dd2e44"
/>
<path
d="M264.291 322.873h-14.165V309.29h14.165v13.582zm-16.426-118.898h-10.183l-5.106-12.459 10.198-1.137 5.091 13.596zm23.21 5.66l-6.784-2.261 3.385-16.426 13.028 1.137-9.629 17.55zm-61.141 69.646l-19.811 4.523-.57-13.583 19.812-2.83.569 11.89zm5.106 14.152l-16.426 6.812-3.954-10.766 15.289-5.106 5.091 9.06zm109.269-12.444l-18.105-5.091v-9.63l20.366-2.83-2.261 17.55zm-9.003 18.674l-16.981-7.937 5.646-10.182 18.703 5.66-7.368 12.459z"
fill="#eee"
/>
</SvgIcon>
);
});
Nicaragua.displayName = 'Nicaragua';
export { Nicaragua };

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Russia = React.forwardRef(function Russia(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.russia')}>
<path
d="M473.655 88.276H38.345C17.167 88.276 0 105.443 0 126.621v73.471h512v-73.471c0-21.178-17.167-38.345-38.345-38.345z"
fill="#f5f5f5"
/>
<path d="M0 200.09h512V311.9H0z" fill="#0039a6" />
<path
d="M0 385.379c0 21.177 17.167 38.345 38.345 38.345h435.31c21.177 0 38.345-17.167 38.345-38.345v-73.471H0v73.471z"
fill="#d52b1e"
/>
</SvgIcon>
);
});
Russia.displayName = 'Russia';
export { Russia };

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Spain = React.forwardRef(function Spain(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 45 45" {...props} ref={ref} title={t('flag.spain')}>
<defs>
<clipPath id="prefix__a">
<path d="M0 36h36V0H0v36z" />
</clipPath>
</defs>
<g clipPath="url(#prefix__a)" transform="matrix(1.25 0 0 -1.25 0 45)">
<path
d="M36 9a4 4 0 00-4-4H4a4 4 0 00-4 4v18a4 4 0 004 4h28a4 4 0 004-4V9z"
fill="#c60a1d"
/>
<path d="M36 12H0v12h36V12z" fill="#ffc400" />
<path d="M9 19v-3a3 3 0 116 0v3H9z" fill="#ea596e" />
<path d="M12 17h3v3h-3v-3z" fill="#f4a2b2" />
<path d="M12 17H9v3h3v-3z" fill="#dd2e44" />
<path
d="M15 21.5c0-.829-1.343-1.5-3-1.5s-3 .671-3 1.5 1.343 1.5 3 1.5 3-.671 3-1.5"
fill="#ea596e"
/>
<path
d="M15 22.25c0 .414-1.343.75-3 .75s-3-.336-3-.75 1.343-.75 3-.75 3 .336 3 .75"
fill="#ffac33"
/>
<path d="M7 13h1v7H7v-7zm10 0h-1v7h1v-7z" fill="#99aab5" />
<path d="M9 13H6v1h3v-1zm9 0h-3v1h3v-1zM8 20H7v1h1v-1zm9 0h-1v1h1v-1z" fill="#66757f" />
</g>
</SvgIcon>
);
});
Spain.displayName = 'Spain';
export { Spain };

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Taiwan = React.forwardRef(function Taiwan(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="-60 -40 240 160" {...props} ref={ref} title={t('flag.taiwan')}>
<rect x={-60} y={-40} width="100%" height="100%" fill="#fe0000" />
<rect x={-60} y={-40} width="50%" height="50%" fill="#000095" />
<path id="prefix__a" d="M8 0L0 30-8 0l8-30M0 8l30-8L0-8l-30 8" fill="#fff" />
<use xlinkHref="#prefix__a" transform="rotate(30)" />
<use xlinkHref="#prefix__a" transform="rotate(60)" />
<circle r={17} fill="#000095" />
<circle r={15} fill="#fff" />
</SvgIcon>
);
});
Taiwan.displayName = 'Taiwan';
export { Taiwan };

View File

@@ -1,23 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Turkey = React.forwardRef(function Turkey(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.turkey')}>
<path fill="#d80027" d="M0 85.337h512v341.326H0z" />
<g fill="#f0f0f0">
<path d="M247.213 216.787l17.594 24.246 28.493-9.239-17.621 24.224 17.592 24.244-28.484-9.274-17.621 24.225.018-29.955-28.484-9.275 28.494-9.239z" />
<path d="M199.202 316.602c-33.469 0-60.602-27.133-60.602-60.602s27.133-60.602 60.602-60.602c10.436 0 20.254 2.639 28.827 7.284-13.448-13.152-31.84-21.269-52.135-21.269-41.193 0-74.586 33.394-74.586 74.586s33.394 74.586 74.586 74.586c20.295 0 38.687-8.117 52.135-21.269-8.572 4.647-18.391 7.286-28.827 7.286z" />
</g>
</SvgIcon>
);
});
Turkey.displayName = 'Turkey';
export { Turkey };

View File

@@ -1,30 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgIcon } from '../SvgIcon';
type Props = React.ComponentProps<typeof SvgIcon>;
const Ukraine = React.forwardRef(function Ukraine(props: Props, ref: React.Ref<SVGSVGElement>) {
const { t } = useTranslation();
return (
<SvgIcon viewBox="0 0 512 512" {...props} ref={ref} title={t('flag.ukraine')}>
<path
d="M0 248v121.6C0 396.8 21.6 416 48 416h400c26.4 0 48-19.2 48-46.4V248H0z"
fill="#fdce0c"
/>
<path d="M248 248l197.6 168c26.4 0 50.4-19.2 50.4-46.4V248H248z" fill="#f4ba00" />
<path
d="M448 80H48C21.6 80 0 99.2 0 126.4V248h496V126.4c0-27.2-21.6-46.4-48-46.4z"
fill="#44c1ef"
/>
<path d="M448 80H48l200 168h248V126.4c0-27.2-21.6-46.4-48-46.4z" fill="#18b4ea" />
<path d="M496 368.8c0 29.6-21.6 47.2-48 47.2H48c-26.4 0-48-20.8-48-48" fill="#f2a700" />
<path d="M48 80h400c26.4 0 48 19.2 48 46.4V216" fill="#10a2e2" />
</SvgIcon>
);
});
Ukraine.displayName = 'Ukraine';
export { Ukraine };

File diff suppressed because one or more lines are too long

View File

@@ -1,21 +1,5 @@
export { Austria } from './Austria';
export { Brazil } from './Brazil';
export { China } from './China';
export { Czech } from './Czech';
export { France } from './France';
export { Germany } from './Germany';
export { India } from './India';
export { Japan } from './Japan';
export { Nicaragua } from './Nicaragua';
export { Taiwan } from './Taiwan';
export { Spain } from './Spain';
export { Khmer } from './Khmer';
export { Russia } from './Russia';
export { Ukraine } from './Ukraine';
export { Usa } from './Usa';
export { Turkey } from './Turkey';
export { Version } from './Version';
export { Time } from './Time';
export { Earth } from './Earth';
export { FileBinary } from './FileBinary';
export { Law } from './Law';
export { Earth } from './Earth';

View File

@@ -1,6 +1,6 @@
import { createContext } from 'react';
import { Language } from 'src/i18n/enabledLanguages';
import { Language } from '../../i18n/config';
interface Props {
isDarkMode: boolean;
setIsDarkMode: (isDarkMode: boolean) => void;

View File

@@ -0,0 +1,13 @@
## Translate UI Strings
If you are willing to translate User Interfances strings, are located in the `crowdin` project which is available [here](https://crowdin.com/project/verdaccio) at `packages/plugins/src/i18n/crowdin/ui.json`.
### Requirements
- Need a crowdin account, use your GitHub account for fast access as an option.
### Adding a new language to the UI
- If the language is not available, ask in the Discord channel to enable it, that will be pretty fast.
- After you have translated the strings, add to `packages/plugins/ui-theme/src/i18n/enabledLanguages.ts` the configuration for the new language.
- In development mode the file must not be available since you need admin credentials to download translations, as a work around, create a folder withing the `crowdin` folder eg: `/packages/plugins/ui-theme/src/i18n/download_translations/fr-FR/ui.json` and then will be available for testing. If the language is not found, will fallback to `en-US`.

View File

@@ -0,0 +1,52 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import {
DEFAULT_LANGUAGE,
LanguageConfiguration,
listLanguages,
listLanguagesAsString,
} from './enabledLanguages';
/**
* In development mode translations files might be not available,
* crowdin translations are only available in CI.
*/
function loadTranslationFile(lng) {
try {
return require(`./download_translations/${lng}/ui.json`);
} catch {
// eslint-disable-next-line no-console
console.warn(`language ${lng} file not found, fallback to en-US`);
// in case the file is not there, fallback to en-US
return require(`./crowdin/ui.json`);
}
}
const languages = listLanguages.reduce((acc, item: LanguageConfiguration) => {
acc[item.lng] = {
translation:
item.lng === DEFAULT_LANGUAGE ? require(`./crowdin/ui.json`) : loadTranslationFile(item.lng),
};
return acc;
}, {});
i18n
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
// in case window.VEDACCIO_LANGUAGE is undefined,it will fall back to 'en-US'
lng: window?.__VERDACCIO_BASENAME_UI_OPTIONS?.language || DEFAULT_LANGUAGE,
fallbackLng: DEFAULT_LANGUAGE,
whitelist: [...listLanguagesAsString],
load: 'currentOnly',
resources: languages,
debug: false,
interpolation: {
escapeValue: false, // react already safes from xss
},
});
export default i18n;

View File

@@ -0,0 +1,170 @@
{
"copy-to-clipboard": "Copy to clipboard",
"author-anonymous": "Anonymous",
"author-unknown": "Unknown",
"action-bar-action": {
"visit-home-page": "Visit homepage",
"open-an-issue": "Open an issue",
"download-tarball": "Download tarball"
},
"dialog": {
"registry-info": {
"title": "Registry Info"
}
},
"header": {
"documentation": "Documentation",
"registry-info": "Registry Information",
"registry-info-link": "Learn more",
"registry-no-conf": "No configurations available",
"greetings": "Hi "
},
"search": {
"packages": "Search Packages"
},
"autoComplete": {
"loading": "Loading...",
"no-results-found": "No results found",
"clear": "Clear",
"expand": "Expand",
"collapse": "Collapse"
},
"tab": {
"uplinks": "Uplinks",
"versions": "Versions",
"dependencies": "Dependencies",
"readme": "Readme"
},
"uplinks": {
"title": "Uplinks",
"no-items": "{{name}} has no uplinks."
},
"versions": {
"current-tags": "Current Tags",
"version-history": "Version history",
"not-available": "Not available"
},
"package": {
"published-on": "Published on {{time}} •",
"version": "v{{version}}",
"visit-home-page": "Visit homepage",
"homepage": "Homepage",
"open-an-issue": "Open an issue",
"bugs": "Bugs",
"download": "Download {{what}}",
"the-tar-file": "the tar file",
"tarball": "Tarball"
},
"dependencies": {
"has-no-dependencies": "{{package}} has no dependencies.",
"dependency-block": "{{package}}@{{version}}"
},
"form": {
"username": "Username",
"password": "Password"
},
"form-placeholder": {
"username": "Your username",
"password": "Your strong password"
},
"form-validation": {
"required-field": "This field is required",
"required-min-length": "This field required the min length of {{length}}",
"unable-to-sign-in": "Unable to sign in",
"username-or-password-cant-be-empty": "Username or password can't be empty!"
},
"help": {
"title": "No Package Published Yet.",
"sub-title": "To publish your first package just:",
"first-step": "1. Login",
"first-step-command-line": "npm adduser --registry {{registryUrl}}",
"second-step": "2. Publish",
"second-step-command-line": "npm publish --registry {{registryUrl}}",
"third-step": "3. Refresh this page"
},
"sidebar": {
"detail": {
"latest-version": "Latest v{{version}}",
"version": "v{{version}}"
},
"installation": {
"title": "Installation",
"install-using-yarn": "Install using yarn",
"install-using-yarn-command": "yarn add {{packageName}}",
"install-using-npm": "Install using npm",
"install-using-npm-command": "npm install {{packageName}}",
"install-using-pnpm": "Install using pnpm",
"install-using-pnpm-command": "pnpm install {{packageName}}"
},
"repository": {
"title": "Repository"
},
"author": {
"title": "Author"
},
"distribution": {
"title": "Latest Distribution",
"license": "License",
"size": "Size",
"file-count": "file count"
},
"maintainers": {
"title": "Maintainers"
},
"contributors": {
"title": "Contributors"
},
"engines": {
"npm-version": "NPM Version",
"node-js": "NODE JS"
}
},
"footer": {
"powered-by": "Powered by",
"made-with-love-on": "Made with <0>♥</0> on"
},
"button": {
"close": "Close",
"cancel": "Cancel",
"login": "Login",
"logout": "Logout",
"go-to-the-home-page": "Go to the home page",
"learn-more": "Learn More",
"fund-this-package": "<0>Fund</0> this package"
},
"error": {
"unspecific": "Something went wrong.",
"404": {
"page-not-found": "404 - Page not found",
"sorry-we-could-not-find-it": "Sorry, we couldn't find it..."
},
"app-context-not-correct-used": "The app context was not used correctly",
"theme-context-not-correct-used": "The theme context was not used correctly",
"package-meta-is-required-at-detail-context": "packageMeta is required at DetailContext"
},
"lng": {
"english": "English",
"czech": "Czech",
"japanese": "Japanese",
"portuguese": "Portuguese",
"spanish": "Spanish",
"german": "German",
"chinese": "Chinese",
"chineseTraditional": "Chinese (Traditional)",
"french": "French",
"russian": "Russian",
"turkish": "Turkish",
"ukraine": "Ukraine",
"khmer": "Khmer"
},
"flag": {
"austria": "Austria",
"brazil": "Brazil",
"spain": "Spain",
"nicaragua": "Nicaragua",
"india": "India",
"china": "China",
"germany": "Germany",
"taiwan": "Taiwan"
}
}

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