diff --git a/.dockerignore b/.dockerignore index 641d01d3d..a9c1b9e85 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,50 @@ -.git/ -docker-compose* -Dockerfile* +hyperglass/hyperglass/static +.git +.github +docs +.tests +tests +**/ssh.key +**/node_modules +**/node_modules/.pnpm +.DS_Store +hyperglass/ui/.env* +hyperglass.json +custom.*[js, html] +.next +out/ +fonts/ +__pycache__ +.python-version +.venv +.mypy_cache +.pytest_cache +.ruff_cache +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +*.rdb +*.py[cod] +.ipynb* diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 7f0a5e667..000000000 --- a/.flake8 +++ /dev/null @@ -1,23 +0,0 @@ -[flake8] -max-line-length=88 -count=True -show-source=False -statistics=True -exclude=.git, __pycache__, hyperglass/api/examples/*.py, hyperglass/compat/_sshtunnel.py, test.py -filename=*.py -per-file-ignores= - hyperglass/main.py:E402 - # Disable classmethod warning for validator decorators - hyperglass/models/*.py:N805,E0213,R0903,E501,C0301 - hyperglass/models/api/*.py:N805,E0213,R0903,E501,C0301 - hyperglass/models/commands/*.py:N805,E0213,R0903,E501,C0301 - hyperglass/parsing/models/*.py:N805,E0213,R0903 - hyperglass/configuration/models/*.py:N805,E0213,R0903,E501,C0301 - # Disable unused import warning for modules - hyperglass/*/__init__.py:F401 - hyperglass/models/*/__init__.py:F401 -ignore=W503,C0330,R504,D202,S403,S301,S404 -select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W -disable-noqa=False -hang-closing=False -max-complexity=10 diff --git a/.github/ISSUE_TEMPLATE/1-feature-request.md b/.github/ISSUE_TEMPLATE/1-feature-request.md deleted file mode 100644 index be25817ba..000000000 --- a/.github/ISSUE_TEMPLATE/1-feature-request.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Feature Request -about: Suggest an idea for hyperglass -labels: feature ---- - - - -# Feature Description - - - -### Is your feature request related to a problem? Please describe. - - - -# Environment & Use Case - - - -# Additional Context - - diff --git a/.github/ISSUE_TEMPLATE/1-feature-request.yaml b/.github/ISSUE_TEMPLATE/1-feature-request.yaml new file mode 100644 index 000000000..ae9a76b3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-feature-request.yaml @@ -0,0 +1,47 @@ +name: Feature Request +description: Suggest an idea for hyperglass +labels: + - feature +body: + - type: markdown + attributes: + value: > + If the answer to any of these questions is "no", your feature request will most likely be rejected (but may still be considered). + - Is the new feature _only_ applicable to one [platform](https://hyperglass.dev/platforms)? + - Would the new feature work only on mobile, or only on desktop? + - Would the new feature only support IPv4, or IPv6? + - Is the new feature something that can be reasonably customized by hyperglass users? + - type: input + id: version + attributes: + label: Version + description: What version of hyperglass are you currently running? + placeholder: v2.0.4 + validations: + required: true + - type: textarea + id: feature-details + attributes: + label: Feature Details + description: Describe the solution or change you would like in detail. + validations: + required: true + - type: dropdown + id: feature-type + validations: + required: true + attributes: + label: Feature Type + multiple: true + options: + - New Platform + - Web UI + - New Functionality + - Change to Existing Functionality + - type: textarea + id: use-case + validations: + required: true + attributes: + label: Use Case + description: How will this feature benefit hyperglass users (providers, end-users, or both)? diff --git a/.github/ISSUE_TEMPLATE/2-bug-report.md b/.github/ISSUE_TEMPLATE/2-bug-report.md deleted file mode 100644 index 6132fb64d..000000000 --- a/.github/ISSUE_TEMPLATE/2-bug-report.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: Bug Report -about: Report a problem or unexpected behavior -labels: possible-bug -assignees: checktheroads ---- - - - -# Bug Description - - - -# Expected behavior - - - -# Steps to Reproduce - - - -## Local Configurations - - - -``` - -``` - -## Logs - - - -``` - -``` - -# Possible Solution - - - -# Environment - -## Server - - - - - -## Client - -- OS & Version: -- Browser: - -### Smartphone Details (if applicable) - -- Device: -- OS: -- Browser: diff --git a/.github/ISSUE_TEMPLATE/2-bug-report.yaml b/.github/ISSUE_TEMPLATE/2-bug-report.yaml new file mode 100644 index 000000000..fa8824f9b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-bug-report.yaml @@ -0,0 +1,96 @@ +name: Bug Report +description: Report a problem or unexpected behavior +labels: + - possible-bug +body: + - type: dropdown + id: deployment-type + validations: + required: true + attributes: + label: Deployment Type + description: How are you running hyperglass? + multiple: false + options: + - Docker + - Manual + - Other (please explain) + - type: input + id: version + attributes: + label: Version + description: What version of hyperglass are you currently running? + placeholder: v2.0.4 + validations: + required: true + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: Steps to Reproduce + description: > + Describe in detail the exact steps one can take to reproduce this bug. + If reporting a UI bug, be sure to include screenshots, browser version, and operating system and platform. + If you've deployed hyperglass manually, be sure to include Python and NodeJS versions. + placeholder: | + 1. Click the thing + 2. Type the stuff + 3. See the error + - type: textarea + id: expected-behavior + validations: + required: true + attributes: + label: Expected Behavior + description: What did you expect to happen? + placeholder: A thing should have happened. + - type: textarea + id: observed-behavior + validations: + required: true + attributes: + label: Observed Behavior + description: What actually happened? + placeholder: An error was shown. + - type: textarea + id: configuration + attributes: + label: Configuration + description: hyperglass [config](https://hyperglass.dev/configuration/config) file in YAML format. + placeholder: | + org_name: Beloved Hyperglass User + plugins: [] + primary_asn: 65000 + request_timeout: 90 + site_description: Beloved Hyperglass User Network Looking Glass + site_title: Beloved Hyperglass User + render: yaml + - type: textarea + id: devices + attributes: + label: Devices + description: > + hyperglass [devices](https://hyperglass.dev/configuration/devices) file in YAML format + **with passwords obfuscated or removed**. + placeholder: | + devices: + - name: New York, NY + address: 192.0.2.1 + platform: cisco_ios + credential: + username: *** + password: *** + - name: San Francisco, CA + address: 192.0.2.2 + platform: juniper + credential: + username: *** + password: *** + render: yaml + - type: textarea + id: logs + attributes: + label: Logs + description: Include any relevant log messages related to the bug. + render: console diff --git a/.github/ISSUE_TEMPLATE/3-new-nos.yaml b/.github/ISSUE_TEMPLATE/3-new-nos.yaml deleted file mode 100644 index eaa71258c..000000000 --- a/.github/ISSUE_TEMPLATE/3-new-nos.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: New Network Operating System -description: Request native support for a network operating system -labels: feature -body: - - type: markdown - attributes: - value: > - In order to natively support a new network operating system for hyperglass, please make sure it is supported by either Netmiko or Scrapli ([see here](https://hyperglass.io/docs/platforms)). - - type: input - attributes: - label: Manufacturer - description: What is the network vendor that manufactures the NOS? For example, for Juniper Junos, this would be Juniper. - validations: - required: true - - type: input - attributes: - label: Platform Name - description: What is the name of this NOS? For example, for Juniper Junos, this would be Junos - validations: - required: true - - type: textarea - attributes: - label: Commands - description: > - Please provide the commands required to execute hyperglass commands ([see here](https://hyperglass.io/docs/commands#adding-a-custom-command-set)). If you do not know the commands, it is likely that this request will be either denied or may take a long time to implement. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3ba13e0ce..3b753305b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ blank_issues_enabled: false +contact_links: + - name: Contributing Policy + url: https://github.com/thatmattlove/hyperglass/blob/main/CONTRIBUTING.md + about: Please read through the contributing policy before opening an issue or pull request. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6222c327a..a9503d481 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ - + diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 000000000..a0f606056 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,35 @@ +name: Backend Testing +on: + workflow_call: + workflow_dispatch: +permissions: + contents: read +jobs: + backend: + runs-on: ubuntu-latest + services: + redis: + image: redis:7.4.7-alpine@sha256:02f2cc4882f8bf87c79a220ac958f58c700bdec0dfb9b9ea61b62fb0e8f1bfcf + ports: ['6379:6379'] + options: --health-cmd "redis-cli ping" --health-interval 5s --health-retries 12 + env: + HYPERGLASS_APP_PATH: ${{ github.workspace }}/.cache/config + HYPERGLASS_HOST: 127.0.0.1 + HYPERGLASS_REDIS_DB: 14 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12.12' + - uses: actions/setup-node@v4 + with: + node-version: '22.22.0' + - run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev pkg-config + mkdir -p "$HYPERGLASS_APP_PATH" + pip install hatchling==1.24.2 editables==0.5 setuptools==69.2.0 wheel==0.43.0 + pip install --no-deps --no-build-isolation -r requirements-dev.lock + pip check + - run: ruff check hyperglass + - run: pytest hyperglass --ignore hyperglass/plugins/external diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 000000000..80489544c --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,24 @@ +name: Frontend Testing +on: + workflow_call: + workflow_dispatch: +permissions: + contents: read +jobs: + frontend: + runs-on: ubuntu-latest + defaults: + run: + working-directory: hyperglass/ui + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22.22.0' + - run: npm install --global pnpm@10.11.0 + - run: pnpm install --frozen-lockfile + - run: echo '{}' > hyperglass.json + - run: pnpm run format:check + - run: pnpm run lint + - run: pnpm run typecheck + - run: pnpm run test --maxWorkers=2 --minWorkers=1 diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml deleted file mode 100644 index d82381bf5..000000000 --- a/.github/workflows/release-pypi.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Release to PyPI -on: - release: - types: [published] - -jobs: - release: - name: Release to PyPI - strategy: - fail-fast: false - matrix: - python-version: [3.6] - poetry-version: [1.1.4] - os: [ubuntu-latest] - runs-on: ubuntu-latest - - steps: - - name: Git Checkout - uses: actions/checkout@v2 - - - name: Install Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Poetry - uses: abatilo/actions-poetry@v2.0.0 - with: - poetry-version: ${{ matrix.poetry-version }} - - - name: Build hyperglass - run: | - poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }} - poetry build - - - name: Publish hyperglass release - run: poetry publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c45118714..7af2c355e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,117 +1,46 @@ -name: Tests -on: [push, pull_request] - +name: Tests and Container +on: + push: + pull_request: +permissions: + contents: read jobs: - frontend: - name: Frontend Tests - strategy: - fail-fast: false - matrix: - node-version: [14.x] - os: [ubuntu-latest] - runs-on: ${{ matrix.os }} - env: - working-directory: ./hyperglass/ui - steps: - - name: Git Checkout - uses: actions/checkout@v2 - - - name: Install Node - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - - - name: Install Dependencies - working-directory: ${{ env.working-directory }} - run: yarn install - - - name: TypeScript - run: ./.tests/pre-commit-frontend.sh --typescript - - - name: ESLint - run: ./.tests/pre-commit-frontend.sh --eslint - - - name: Prettier - run: ./.tests/pre-commit-frontend.sh --prettier backend: - name: Backend Tests - strategy: - fail-fast: false - matrix: - node-version: [14.x] - poetry-version: ["2.1"] - python-version: ["3.10"] - redis-version: ["6"] - os: [ubuntu-latest] - runs-on: ${{ matrix.os }} - + uses: ./.github/workflows/backend.yml + frontend: + uses: ./.github/workflows/frontend.yml + container: + needs: [backend, frontend] + runs-on: ubuntu-latest steps: - - name: Git Checkout - uses: actions/checkout@v2 - - - name: Install Python - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/build-push-action@v6 with: - python-version: ${{ matrix.python-version }} - - - name: Install Poetry - uses: X4BNet/actions-poetry@v2.0.0 + context: . + load: true + tags: hyperglass:ci + build-args: APPLICATION_REVISION=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Cold start, offline UI build, restart, and shutdown + run: .tests/container-smoke.sh hyperglass:ci + - name: Log in to Docker Hub + if: github.event_name == 'push' && github.repository == 'X4BNet/hyperglass' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + uses: docker/login-action@v3 with: - poetry-version: ${{ matrix.poetry-version }} - - - name: Install Node - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - - name: Start Redis - uses: X4BNet/redis-github-action@1.8.0 - with: - redis-version: ${{ matrix.redis-version }} - - - name: Install Python Dependencies - run: | - sudo apt-get update - sudo apt-get install -y libjpeg-dev zlib1g-dev libxml2-dev libxslt1-dev - poetry install - - - name: Run Flake8 - run: poetry run flake8 hyperglass - - - name: Run hyperglass - run: '.tests/ga-backend-app.sh' - - build_container: - needs: [ frontend, backend ] - runs-on: ubuntu-latest - env: - IMAGE_NAME: hyperglass - steps: - - uses: actions/checkout@v2 - - run: | - docker build . --build-arg arg_k8s_application_commit=${{ github.sha }} --build-arg arg_k8s_application_version=${{ github.ref }} --tag $IMAGE_NAME - - name: Log into registry - run: echo "${{ secrets.dockerhub_token }}" | docker login -u x4bnet --password-stdin - - - name: Push image - shell: bash + username: x4bnet + password: ${{ secrets.dockerhub_token }} + - name: Publish tested application revision + if: github.event_name == 'push' && github.repository == 'X4BNet/hyperglass' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + env: + APPLICATION_SHA: ${{ github.sha }} + APPLICATION_REF: ${{ github.ref }} run: | - IMAGE_ID=x4bnet/$IMAGE_NAME - - IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') - - # tag name - [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') - [ "$VERSION" == "main" ] && VERSION=latest - - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION - - docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION - - if [[ "$VERSION" == "latest" ]]; then - docker tag $IMAGE_NAME $IMAGE_ID:git-${{ github.sha }} - docker push $IMAGE_ID:git-${{ github.sha }} - fi \ No newline at end of file + docker tag hyperglass:ci "x4bnet/hyperglass:git-$APPLICATION_SHA" + docker push "x4bnet/hyperglass:git-$APPLICATION_SHA" + if [ "$APPLICATION_REF" = refs/heads/main ]; then + docker tag hyperglass:ci x4bnet/hyperglass:latest + docker push x4bnet/hyperglass:latest + fi + docker inspect --format '{{join .RepoDigests "\n"}}' "x4bnet/hyperglass:git-$APPLICATION_SHA" >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index f190b9684..dff8f8cde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,32 @@ # Project hyperglass/hyperglass/static +TODO* +.env test.py .DS_Store .idea -.vscode -old_*.py +*_old.py *.rdb # # Github Default from https://github.com/martinohanlon/flightlight/issues/1 # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] +.ipynb* # Pyenv .python-version +.venv + +# MyPy +.mypy_cache + +# Pytest +.pytest_cache + +# Ruff +.ruff_cache # C extensions *.so diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 536022ef1..000000000 --- a/.isort.cfg +++ /dev/null @@ -1,14 +0,0 @@ -[settings] -skip_glob = hyperglass/api/examples/*.py -line_length = 88 -indent = ' ' -include_trailing_comma = True -multi_line_output = 3 -balanced_wrapping = True -length_sort = True -force_single_line = False -import_heading_stdlib = Standard Library -import_heading_thirdparty = Third Party -import_heading_firstparty = Project -import_heading_localfolder = Local -known_third_party = starlette,fastapi,inquirer \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 3891142f0..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,37 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 - hooks: - - id: flake8 - stages: - - commit - - repo: local - hooks: - - id: typescript - name: TypeScript - files: 'hyperglass/ui/*' - exclude: 'hyperglass/ui/node_modules|hyperglass/ui/.next' - stages: - - commit - entry: ./.tests/pre-commit-frontend.sh --typescript - language: script - - repo: local - hooks: - - id: eslint - name: ESLint - files: 'hyperglass/ui/*' - exclude: 'hyperglass/ui/node_modules|hyperglass/ui/.next' - stages: - - commit - entry: ./.tests/pre-commit-frontend.sh --eslint - language: script - - repo: local - hooks: - - id: prettier - name: Prettier - files: 'hyperglass/ui/*' - exclude: 'hyperglass/ui/node_modules|hyperglass/ui/.next' - stages: - - commit - entry: ./.tests/pre-commit-frontend.sh --prettier - language: script diff --git a/.samples/Caddyfile b/.samples/Caddyfile new file mode 100644 index 000000000..2d75ebe59 --- /dev/null +++ b/.samples/Caddyfile @@ -0,0 +1,14 @@ +lg.example.com:443 { + tls person@example.com + file_server { + root /etc/hyperglass/static/ui + index /etc/hyperglass/static/ui/index.html + } + file_server /custom { + root /etc/hyperglass/static/custom + } + file_server /images { + root /etc/hyperglass/static/images + } + reverse_proxy localhost:8001 +} \ No newline at end of file diff --git a/hyperglass/models/parsing/arista_route.json b/.samples/arista_route.json similarity index 100% rename from hyperglass/models/parsing/arista_route.json rename to .samples/arista_route.json diff --git a/hyperglass/models/parsing/frr_bgp_community.json b/.samples/frr_bgp_community.json similarity index 100% rename from hyperglass/models/parsing/frr_bgp_community.json rename to .samples/frr_bgp_community.json diff --git a/hyperglass/models/parsing/frr_bgp_route.json b/.samples/frr_bgp_route.json similarity index 95% rename from hyperglass/models/parsing/frr_bgp_route.json rename to .samples/frr_bgp_route.json index 90b936d80..aae884c86 100644 --- a/hyperglass/models/parsing/frr_bgp_route.json +++ b/.samples/frr_bgp_route.json @@ -12,13 +12,8 @@ ], "length": 2 }, - "aggregatorAs": 13335, - "aggregatorId": "108.162.239.1", "origin": "IGP", - "med": 25090, - "metric": 25090, - "localpref": 100, - "weight": 100, + "locPrf": 100, "valid": true, "community": { "string": "174:21001 174:22003 14525:0 14525:40 14525:1021 14525:2840 14525:3003 14525:4004 14525:9001", @@ -70,7 +65,7 @@ "origin": "IGP", "med": 0, "metric": 0, - "localpref": 150, + "locPrf": 150, "weight": 200, "valid": true, "bestpath": { @@ -124,7 +119,7 @@ "origin": "IGP", "med": 0, "metric": 0, - "localpref": 100, + "locPrf": 100, "weight": 100, "valid": true, "bestpath": { @@ -180,7 +175,7 @@ "origin": "IGP", "med": 2020, "metric": 2020, - "localpref": 150, + "locPrf": 150, "weight": 200, "valid": true, "bestpath": { diff --git a/.samples/hyperglass-docker.service b/.samples/hyperglass-docker.service new file mode 100644 index 000000000..3e8385a77 --- /dev/null +++ b/.samples/hyperglass-docker.service @@ -0,0 +1,15 @@ +[Unit] +Description=hyperglass +PartOf=docker.service +After=docker.service + +[Service] +Type=oneshot +RemainAfterExit=true +WorkingDirectory=/opt/hyperglass +EnvironmentFile=/etc/hyperglass/hyperglass.env +ExecStart=/usr/bin/docker compose up -d --remove-orphans +ExecStop=/usr/bin/docker compose down + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/.samples/hyperglass-manual.service b/.samples/hyperglass-manual.service new file mode 100644 index 000000000..7fa04dff8 --- /dev/null +++ b/.samples/hyperglass-manual.service @@ -0,0 +1,13 @@ +[Unit] +Description=hyperglass +After=network.target +Requires=redis-server + +[Service] +User=root +Group=root +EnvironmentFile=/etc/hyperglass/hyperglass.env +ExecStart=python3 -m hyperglass.console start + +[Install] +WantedBy=multi-user.target diff --git a/.samples/hyperglass.nginx b/.samples/hyperglass.nginx new file mode 100644 index 000000000..2fd3abd47 --- /dev/null +++ b/.samples/hyperglass.nginx @@ -0,0 +1,49 @@ +server { + listen 80; + listen [::]:80; + server_name lg.example.com; + return 301 https://$host$request_uri; +} +server { + listen [::]:443 ssl ipv6only=on; + listen 443 ssl; + ssl_certificate + ssl_certificate_key + + client_max_body_size 2M; + + server_name lg.example.com; + + root /etc/hyperglass/static; + + location / { + try_files $uri $uri/ /ui /ui/$uri =404; + index /ui/index.html; + } + + location /openapi.json { + try_files $uri @proxy_to_app; + } + + location /custom/ { + try_files $uri $uri/ /custom; + } + + location /images/ { + try_files $uri $uri/ /images; + } + + location /api { + try_files $uri @proxy_to_app; + } + + location @proxy_to_app { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://[::1]:8001; + } + +} \ No newline at end of file diff --git a/hyperglass/models/parsing/juniper_route_aspath.xml b/.samples/juniper_route_aspath.xml similarity index 100% rename from hyperglass/models/parsing/juniper_route_aspath.xml rename to .samples/juniper_route_aspath.xml diff --git a/hyperglass/models/parsing/juniper_route_direct.xml b/.samples/juniper_route_direct.xml similarity index 100% rename from hyperglass/models/parsing/juniper_route_direct.xml rename to .samples/juniper_route_direct.xml diff --git a/hyperglass/models/parsing/juniper_route_indirect.xml b/.samples/juniper_route_indirect.xml similarity index 100% rename from hyperglass/models/parsing/juniper_route_indirect.xml rename to .samples/juniper_route_indirect.xml diff --git a/.samples/sample_devices.yaml b/.samples/sample_devices.yaml new file mode 100644 index 000000000..a94941617 --- /dev/null +++ b/.samples/sample_devices.yaml @@ -0,0 +1,10 @@ +devices: + - name: Example Device + address: 127.0.0.1 + credential: + username: test + password: test + platform: juniper + attrs: + source4: 127.0.0.1 + source6: 2001:db8::1 diff --git a/.tests/app/setup.sh b/.tests/app/setup.sh deleted file mode 100755 index 4f4cc87f0..000000000 --- a/.tests/app/setup.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash - -echo "[INFO] Starting Redis..." -redis-server & - -cd /tmp/hyperglass - -echo "[INFO] Starting setup..." -poetry run hyperglass setup -d -echo "[SUCCESS] Setup completed." -sleep 2 - -echo "listen_address: 127.0.0.1" >> /root/hyperglass/hyperglass.yaml - -echo "[INFO] Starting UI build." -poetry run hyperglass build-ui - -if [[ ! $? == 0 ]]; then - echo "[ERROR] Failed to start hyperglass." - exit 1 -else - echo "[SUCCESS] UI build completed." -fi - -echo "[INFO] Starting hyperglass..." -poetry run hyperglass start &> /var/log/hyperglassci.log & -sleep 180 - -if [[ ! $? == 0 ]]; then - echo "[ERROR] Failed to start hyperglass." - exit 1 -else - echo "[SUCCESS] Started hyperglass." -fi - -echo "[INFO] Running HTTP test..." - -STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8001) - -echo "[INFO] Status code: $STATUS" - -if [[ ! $? == 0 ]]; then - echo "[ERROR] HTTP test failed." - exit 1 -elif [[ ! "$STATUS" == "200" ]]; then - echo "[ERROR] HTTP test failed. Startup log:" - cat /var/log/hyperglassci.log - exit 1 -fi - -echo "[SUCCESS] Tests ran successfully." -exit 0 \ No newline at end of file diff --git a/.tests/app/ubuntu/Dockerfile b/.tests/app/ubuntu/Dockerfile deleted file mode 100644 index 73bd15e34..000000000 --- a/.tests/app/ubuntu/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM ubuntu:bionic as base -ENV LC_ALL=C.UTF-8 -ENV LANG=C.UTF-8 -WORKDIR /tmp -RUN apt-get update \ - && apt-get install -y git curl net-tools \ - && curl -sL https://deb.nodesource.com/setup_14.x | bash - \ - && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ - && apt-get update \ - && apt-get install -y python3 python3-pip python3-venv redis-server nodejs yarn \ - # && curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 \ - # - # Pinning Poetry installer to this specific version. As of 2020 07 24, the script from master - # fails to install due to Python 2's executable matching first. See #2106 - # - && curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/e70ee3112ab06374dfef4ab84e6dded2382cc7dd/get-poetry.py | python3 \ - && python3 --version \ - && echo "NodeJS $(node --version)" \ - && echo "Yarn $(yarn --version)" -COPY ./ /tmp/hyperglass -ENV PATH=$PATH:/root/.poetry/bin - -FROM base as install -WORKDIR /tmp/hyperglass -RUN poetry install --no-ansi - -FROM install as setup -WORKDIR /tmp/hyperglass -COPY .tests/app/setup.sh /tmp/setup.sh -RUN ls -lsah /tmp \ No newline at end of file diff --git a/.tests/container-smoke.sh b/.tests/container-smoke.sh new file mode 100755 index 000000000..ab7d6b047 --- /dev/null +++ b/.tests/container-smoke.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail +image=${1:?Usage: container-smoke.sh IMAGE [CONFIG_DIRECTORY]} +config_directory=${2:-} +smoke_directory=$(mktemp -d) +smoke_name="hyperglass-smoke-$$" +cleanup() { + docker rm -f "$smoke_name" "$smoke_name-redis" >/dev/null 2>&1 || true + docker network rm "$smoke_name" >/dev/null 2>&1 || true + rm -rf "$smoke_directory" +} +trap cleanup EXIT +if [ -n "$config_directory" ]; then + cp "$config_directory"/{devices.yaml,directives.yaml,config.yaml,ssh.key} "$smoke_directory/" + cp "$config_directory/plugins/x4b_bird.py" "$smoke_directory/x4b_bird.py" +else + cat > "$smoke_directory/devices.yaml" <<'YAML' +devices: + - name: Container Test + address: 192.0.2.1 + platform: bird + credential: + username: test + key: /etc/hyperglass/ssh.key + attrs: + source4: 192.0.2.1 + source6: '2001:db8::1' +YAML + echo '{}' > "$smoke_directory/config.yaml" + echo '{}' > "$smoke_directory/directives.yaml" + touch "$smoke_directory/x4b_bird.py" + ssh-keygen -q -t ed25519 -N '' -f "$smoke_directory/ssh.key" +fi +chmod 0400 "$smoke_directory/ssh.key" +docker network create --internal "$smoke_name" >/dev/null +docker run -d --name "$smoke_name-redis" --network "$smoke_name" redis:7.4.7-alpine@sha256:02f2cc4882f8bf87c79a220ac958f58c700bdec0dfb9b9ea61b62fb0e8f1bfcf >/dev/null +docker run -d --name "$smoke_name" --network "$smoke_name" --cpus 3 --memory 2g \ + -e "HYPERGLASS_REDIS_HOST=$smoke_name-redis" \ + -v "$smoke_directory/devices.yaml:/etc/hyperglass/devices.yaml:ro" \ + -v "$smoke_directory/directives.yaml:/etc/hyperglass/directives.yaml:ro" \ + -v "$smoke_directory/config.yaml:/etc/hyperglass/config.yaml:ro" \ + -v "$smoke_directory/ssh.key:/etc/hyperglass/ssh.key:ro" \ + -v "$smoke_directory/x4b_bird.py:/etc/hyperglass/plugins/x4b_bird.py:ro" \ + "$image" >/dev/null +wait_ready() { + for attempt in $(seq 1 120); do + if docker exec "$smoke_name" curl --fail --silent http://localhost:8001/api/info >/dev/null; then + docker exec "$smoke_name" curl --fail --silent http://localhost:8001/ >/dev/null + return + fi + if [ "$(docker inspect --format '{{.State.Running}}' "$smoke_name")" != true ]; then + docker logs --tail 80 "$smoke_name" + return 1 + fi + sleep 10 + done + docker logs --tail 80 "$smoke_name" + return 1 +} +wait_ready +docker exec "$smoke_name" python3 -c 'from pathlib import Path; p=Path("/etc/hyperglass/ssh.key"); assert p.stat().st_mode & 0o777 == 0o400; assert any(Path("/etc/hyperglass/static/ui/_next/static").rglob("*.js"))' +docker restart --time 30 "$smoke_name" >/dev/null +wait_ready +# A missing export must force a rebuild even if the previous config hash matches. +docker exec "$smoke_name" rm -rf /etc/hyperglass/static/ui +docker restart --time 30 "$smoke_name" >/dev/null +wait_ready +docker stop --time 30 "$smoke_name" >/dev/null +test "$(docker inspect --format '{{.State.OOMKilled}}' "$smoke_name")" = false +test "$(docker inspect --format '{{.State.ExitCode}}' "$smoke_name")" != 137 +echo 'Container smoke tests passed (offline startup, assets, key permissions, restart, shutdown).' diff --git a/.tests/devices.yaml b/.tests/devices.yaml new file mode 100644 index 000000000..9f56bb00b --- /dev/null +++ b/.tests/devices.yaml @@ -0,0 +1,12 @@ +--- +routers: + - name: Example Device + address: 127.0.0.1 + group: Test + credential: + username: test + password: test + platform: juniper + directives: + - builtins: false + - juniper_bgp_route diff --git a/.tests/directives.yaml b/.tests/directives.yaml new file mode 100644 index 000000000..029ab8078 --- /dev/null +++ b/.tests/directives.yaml @@ -0,0 +1,17 @@ +juniper_bgp_route: + name: BGP Route + groups: + - Global + rules: + - condition: '1.0.0.1/32' + action: deny + - condition: '0.0.0.0/0' + ge: 8 + le: 25 + command: 'show route protocol bgp table inet.0 {target} detail' + - condition: '::/0' + ge: 16 + le: 64 + command: 'show route protocol bgp table inet6.0 {target} detail' + field: + description: IP Address or Prefix diff --git a/.tests/ga-backend-app.sh b/.tests/ga-backend-app.sh deleted file mode 100755 index 70179140c..000000000 --- a/.tests/ga-backend-app.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash - -mkdir -p $HOME/hyperglass - -LOG_FILE="$HOME/hyperglass-ci.log" - -export POETRY_HYPERGLASS_UI_BUILD_TIMEOUT="600" -echo "[INFO] Set build timeout to $POETRY_HYPERGLASS_UI_BUILD_TIMEOUT seconds" - -echo "[INFO] Starting setup..." -poetry run hyperglass setup -d &> $LOG_FILE -echo "[SUCCESS] Setup completed." -sleep 2 - -echo "[INFO] Copying devices.yaml file..." -cp ./hyperglass/examples/devices.yaml $HOME/hyperglass/devices.yaml - -echo "[INFO] Setting listen_address..." -echo "listen_address: 127.0.0.1" >> $HOME/hyperglass/hyperglass.yaml - -echo "[INFO] Starting UI build." -poetry run hyperglass build-ui &> $LOG_FILE - -if [[ ! $? == 0 ]]; then - echo "[ERROR] Failed to build hyperglass ui." - cat /tmp/hyperglass.log - cat $LOG_FILE - exit 1 -else - echo "[SUCCESS] UI build completed." -fi - -echo "[INFO] Starting hyperglass..." -poetry run hyperglass start &> $LOG_FILE & -sleep 30 - -if [[ ! $? == 0 ]]; then - echo "[ERROR] Failed to start hyperglass." - cat /tmp/hyperglass.log - cat $LOG_FILE - exit 1 -else - echo "[SUCCESS] Started hyperglass." -fi - -echo "[INFO] Running HTTP test..." - -STATUS=$(timeout 10s curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8001) - -echo "[INFO] Status code: $STATUS" - -if [[ ! $? == 0 ]]; then - echo "[ERROR] HTTP test failed." - exit 1 -elif [[ ! "$STATUS" == "200" ]]; then - echo "[ERROR] HTTP test failed." - cat /tmp/hyperglass.log - cat $LOG_FILE - exit 1 -fi - -echo "[SUCCESS] Tests ran successfully." -exit 0 diff --git a/.tests/install/ubuntu/Dockerfile b/.tests/install/ubuntu/Dockerfile deleted file mode 100644 index 2afb0573f..000000000 --- a/.tests/install/ubuntu/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM ubuntu:bionic as base -WORKDIR /tmp -COPY .tests/install/ubuntu/setup.sh /tmp/init.sh -COPY ./install.sh /tmp/install.sh -RUN bash /tmp/init.sh \ No newline at end of file diff --git a/.tests/install/ubuntu/setup.sh b/.tests/install/ubuntu/setup.sh deleted file mode 100755 index 23b9fbe8f..000000000 --- a/.tests/install/ubuntu/setup.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -echo "[INFO] Disabling multiverse repos..." -sed -i -e '/multiverse/s/^#*/#\ /g' /etc/apt/sources.list -cat /etc/apt/sources.list - -echo "[INFO] Updating package repos..." -apt-get update &> /dev/null - -echo "[INFO] Installing apt-utils..." -apt-get install -y apt-utils > /dev/null - -echo "[INFO] Installing base dependencies..." -apt-get install -y curl git gnupg dialog build-essential > /dev/null - -echo '[SUCCESS] Completed build' -exit 0 \ No newline at end of file diff --git a/.tests/pre-commit-frontend.sh b/.tests/pre-commit-frontend.sh deleted file mode 100755 index e23a4347c..000000000 --- a/.tests/pre-commit-frontend.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash - -UI_DIR="$(pwd)/hyperglass/ui" - -check_typescript () { - cd $UI_DIR - node_modules/.bin/tsc --noEmit -} - -check_eslint () { - cd $UI_DIR - node_modules/.bin/eslint . --ext .ts --ext .tsx -} - -check_prettier () { - cd $UI_DIR - node_modules/.bin/prettier -c . -} - -for arg in "$@" -do - if [ "$arg" == "--typescript" ] - then - check_typescript - exit $? - elif [ "$arg" == "--eslint" ] - then - check_eslint - exit $? - elif [ "$arg" == "--prettier" ] - then - check_prettier - exit $? - else - echo "Arguments --typescript, --eslint, or --prettier required." - exit 1 - fi -done diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..797e52f71 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "yaml.format.singleQuote": true, + "python.linting.mypyEnabled": false, + "python.linting.enabled": false, + "biome.lspBin": "./hyperglass/ui/node_modules/.bin/biome", + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "always" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 53dc9774b..be4445dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,105 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# 1.0.4 - 2021-07-03 +## [Unreleased] + +### Fixed +- [#280](https://github.com/thatmattlove/hyperglass/issues/280): Fix: `condition: None` caused error in directive @Jimmy01240397 +- [#306](https://github.com/thatmattlove/hyperglass/issues/306): Fix: allow integer values in ext_community_list_raw field for Arista BGP - @cooperwinser +- [#311](https://github.com/thatmattlove/hyperglass/issues/311): Fix: device and directive errors. +- [#315](https://github.com/thatmattlove/hyperglass/issues/315): Impossibile to use command "BGP Route" with Huawei NetEngine 8000 +- [#315](https://github.com/thatmattlove/hyperglass/issues/187): Error in bgp_router query on Huawei +- [#325](https://github.com/thatmattlove/hyperglass/pull/325): Fix code block padding in the documentation - @jagardaniel +- [#332](https://github.com/thatmattlove/hyperglass/pull/332): Fix custom proxy port support in SSH proxy tunnels @jessiebryan + +### Updated + +- [#245](https://github.com/thatmattlove/hyperglass/issues/245): v2.0.0 Vyos version platforms - Moved to latest LTS command set. - @ServerForge +- [#292](https://github.com/thatmattlove/hyperglass/pull/292): Updates Mikrotik BGP route command so supernets are selected as well as exact matches. - @GrandArcher + +### Added + +- [#304](https://github.com/thatmattlove/hyperglass/pull/304): Add FRR structured output for BGP Routes - @chriswiggins + +## 2.0.4 - 2024-06-30 + +### Fixed + +- [#264](https://github.com/thatmattlove/hyperglass/issues/264): Fixed issue where IPv6 traceroutes fail on Juniper devices due to `traceroute: wait must be >1 sec.` error. Thanks @renatoornelas! +- [#267](https://github.com/thatmattlove/hyperglass/issues/267): Fixed issue where responses were incorrectly cached, resulting in no data being shown in the AS Path viewer. +- [#268](https://github.com/thatmattlove/hyperglass/issues/268): Fixed issue where some Mikrotik commands failed to execute properly. +- [#269](https://github.com/thatmattlove/hyperglass/issues/269): Updated documentation regarding `structured.rpki.mode`. +- Removed unnecessary logging statements which caused logging errors. +- Fixed issue where validation of structured BGP route data may have failed under certain conditions. + +### Changed +- Error responses are no longer cached. + +## 2.0.3 - 2024-06-16 + +### Fixed + +- [#262](https://github.com/thatmattlove/hyperglass/issues/262): Fix issue where Mikrotik output was improperly parsed and displayed an error as a result. +- Fixed issue where incorrect error styles were displayed. +- Fixed issue where 'results' accordion component did not re-open when closed. +- Fixed issue where pattern-based directive rules failed validation. + +### Changed + +- Set default logo width (back) to 50%, adjusted how the `web.logo.width` setting is handled in the UI. + +## 2.0.2 - 2024-06-01 + +### Fixed + +- [#257](https://github.com/thatmattlove/hyperglass/issues/257): Fix issue where if `web.location_display_mode` is set to `dropdown` (automatically or otherwise), the menu would remain open but become detached from the main element because the Query Type element came into view. +- [#253](https://github.com/thatmattlove/hyperglass/issues/253): _Actually_ fix issue where configuration values were improperly prepended with the `HYPERGLASS_APP_PATH` value. +- [#258](https://github.com/thatmattlove/hyperglass/issues/258): Center logo alignment on small screens. +- Fix broken license link in default credit menu. + +### Added + +- Added license to docs. +- [#254](https://github.com/thatmattlove/hyperglass/issues/254): Users may specify their own DNS over HTTPS provider if desired. + +## 2.0.1 - 2024-05-31 + +### Fixed +- [#244](https://github.com/thatmattlove/hyperglass/issues/244): Fix issue with UI build where UI build directory already existed and therefore could not be created. +- [#249](https://github.com/thatmattlove/hyperglass/issues/249): Fix issue where configuration values were improperly prepended with the `HYPERGLASS_APP_PATH` value. +- [#251](https://github.com/thatmattlove/hyperglass/issues/251): Fix issue where browser-based DNS resolution did not show, causing FQDN queries to fail due to validation. +- Fix issue where logo was improperly sized on small screens. + +## 2.0.0 - 2024-05-28 + +_v2.0.0 is a major release of hyperglass. Many things have changed, and it is likely best to redeploy hyperglass in a new environment to migrate to v2._ + +### Added + +- Commands are now defined as [directives](https://hyperglass.dev/configuration/directives), which is a configuration definition of one or more commands to run on a device. A directive defines: + - What command (or commands) to run on the device + - Type of UI field, text input or select + - If the field can accept multiple values + - Help information to show about the directive + - Validation rules +- hyperglass now supports Docker, and using Docker is the default and recommended method for deployment. +- The list of locations (devices) is displayed as a gallery when the number of devices is 5 or less. This is a default value and is configurable. +- hyperglass now supports custom [input or output plugins](https://hyperglass.dev/plugins). + - Input Plugins: Apply custom validation logic or transform user input before the query is sent to a device. + - Output Plugins: Interact with the output from a device before it's displayed to the user. +- [#206](https://github.com/thatmattlove/hyperglass/issues/206): OpenBGPD is natively supported by hyperglass. +- [#176](https://github.com/thatmattlove/hyperglass/issues/176): Custom javascript or HTML can be injected into the web page (for tracking applications such as Google Analytics). +- [#173](https://github.com/thatmattlove/hyperglass/issues/173): Any output, such as BGP Communities, can be highlighted in the UI by defining [highlight patterns](https://hyperglass.dev/configuration/config/web-ui#highlighting). +- [#155](https://github.com/thatmattlove/hyperglass/issues/155): A user can now use the "My IP" button to insert their own IP into the query target field. +- [#143](https://github.com/thatmattlove/hyperglass/issues/143): Any HTTP endpoint may be configured as device from which to collect output. + +### Fixed +- [#229](https://github.com/thatmattlove/hyperglass/issues/229): Fixed an issue where the logo was not visible when using Firefox. +- [#180](https://github.com/thatmattlove/hyperglass/issues/180): Fixed an issue where certain FQDNs were considered invalid. +- [#178](https://github.com/thatmattlove/hyperglass/issues/178): Fixed an issue where parsing of Arista EOS routes failed if MED is unset. +- [#145](https://github.com/thatmattlove/hyperglass/issues/145): Fixed an issue where menu links were improperly generated. + +## 1.0.4 - 2021-07-03 ### Fixed - [#148](https://github.com/thatmattlove/hyperglass/issues/148): Update Debian/Ubuntu Python package name in installer and documentation. @@ -14,21 +112,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Improve handling of Junos XML errors. When a Junos device returns an error in the XML output, it will be displayed in the UI. - Improve `hyperglass system-info` output. NodeJS version is now included in the output. -# 1.0.3 - 2021-06-23 +## 1.0.3 - 2021-06-23 _1.0.3 is a cosmetic release to factor in code-level changes related to the repository name change from checktheroads to thatmattlove._ -# 1.0.2 - 2021-06-18 +## 1.0.2 - 2021-06-18 ### Fixed - [#150](https://github.com/thatmattlove/hyperglass/issues/150): Fix handling of BIRD AS_PATH/Community targets. -# 1.0.1 - 2021-06-17 +## 1.0.1 - 2021-06-17 ### Fixed - UI: fix body overflow issue -# 1.0.0 - 2021-05-30 +## 1.0.0 - 2021-05-30 ### BREAKING CHANGES - The `external_link`, `help`, and `terms` parameters no longer exist and have been replaced with generic `links` and `menus` options. @@ -43,7 +141,7 @@ _1.0.3 is a cosmetic release to factor in code-level changes related to the repo ### Added - [#140](https://github.com/thatmattlove/hyperglass/issues/140): Genericize links and menus so that multiple links and/or menus can be defined and fully customized. -# 1.0.0-beta.82 - 2021-04-22 +## 1.0.0-beta.82 - 2021-04-22 ### BREAKING CHANGE **NodeJS 14.15 or later is required**. See [the docs](https://hyperglass.dev/docs/getting-started) for installation instructions. @@ -60,7 +158,7 @@ _1.0.3 is a cosmetic release to factor in code-level changes related to the repo ### Added - The driver for devices can now be overridden with the `driver` parameter. -# 1.0.0-beta.81 - 2021-04-10 +## 1.0.0-beta.81 - 2021-04-10 ### Fixed - [#124](https://github.com/thatmattlove/hyperglass/issues/124): Fix an issue where networks weren't always sorted alphabetically. @@ -69,13 +167,13 @@ _1.0.3 is a cosmetic release to factor in code-level changes related to the repo - [#133](https://github.com/thatmattlove/hyperglass/issues/133): Use body styles for background/foreground color, allowing the user to override the `light` and `dark` colors per the docs. - Fix an issue with select menu list style. -## 1.0.0-beta.80 - 2021-03-03 +### 1.0.0-beta.80 - 2021-03-03 ### Fixed - Fix an issue where the UI did not properly filter and detect the correct Query VRF when only one was defined. - [#121](https://github.com/thatmattlove/hyperglass/issues/121): Fix issue with select menu styling in light mode. -## 1.0.0-beta.79 - 2021-02-26 +### 1.0.0-beta.79 - 2021-02-26 ### BREAKING CHANGE **Major changes have been made to how VRFs are defined and handled.** Previously, you would signal to hyperglass that a VRF was the "default" VRF (meaning, a VRF does not need to be specified in any commands) by setting `name: default` in the VRF block. This limitation meant that a VRF named `default` _had_ to be defined, and that any users who keep their global routing table in a non-default VRF must define it separately. @@ -90,7 +188,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as - Migrate to palette-by-numbers for theming. - Update UI dependencies. -## 1.0.0-beta.78 - 2021-02-12 +### 1.0.0-beta.78 - 2021-02-12 ### Added - Experimental table output/structured data support for Arista EOS. @@ -101,7 +199,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as ### Changed - Caught fetch errors now display the HTTP status text in the UI, instead of the caught error message. -## 1.0.0-beta.77 - 2021-02-10 +### 1.0.0-beta.77 - 2021-02-10 **POTENTIALLY BREAKING CHANGE**: The device `display_name` field is being deprecated, in favor of a single `name` field, which will be displayed to the end user. The `display_name` field still works, but you should migrate away from it as soon as possible. @@ -111,7 +209,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as ### Changed - Removed `display_name` field from device model. The `name` field will be used in the UI. If a `display_name` is defined, it will be used, for backwards compatibility. -## 1.0.0-beta.76 - 2021-02-06 +### 1.0.0-beta.76 - 2021-02-06 **NOTICE**: *[hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent) will be deprecated soon. Use `frr_ssh` or `bird_ssh` for SSH connectivity in the meantime.* @@ -125,13 +223,13 @@ Moving forward, the `name` field is only used to define the name of the VRF **as - AS Path graph view now uses [dagre](https://github.com/dagrejs/dagre) to properly arrange each AS. - Added timeout argument to `hyperglass start --build` - fixes issue where running a UI build in this way failed due to a missing timeout argument error. -## 1.0.0-beta.75 - 2021-01-28 +### 1.0.0-beta.75 - 2021-01-28 ### Changed - Default UI build timeout is now 180 seconds. - The hyperglass `build-ui` CLI command now accepts a `--timeout` argument to override the UI build timeout. -## 1.0.0-beta.74 - 2021-01-25 +### 1.0.0-beta.74 - 2021-01-25 ### Changed - The Scrapli driver no longer specifically ignores the system's SSH config file. @@ -140,7 +238,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as ### Fixed - [#109](https://github.com/thatmattlove/hyperglass/issues/109): Remove the custom error page, because it doesn't work and doesn't really add much. -## 1.0.0-beta.73 - 2021-01-18 +### 1.0.0-beta.73 - 2021-01-18 ### Added - [#106](https://github.com/thatmattlove/hyperglass/issues/106): Add built-in support for Nokia SR OS (thanks @paunadeu!). @@ -152,7 +250,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as ### Fixed - [#107](https://github.com/thatmattlove/hyperglass/issues/107): Fix footer menu styling so it doesn't overflow the viewport, especially on mobile. -## 1.0.0-beta.72 - 2021-01-16 +### 1.0.0-beta.72 - 2021-01-16 ### Fixed - [#104](https://github.com/thatmattlove/hyperglass/issues/104): Handle the usage of `juniper_junos` as a NOS. `juniper_junos` will now automatically be mapped to `juniper`. @@ -161,7 +259,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as ### Changed - **BREAKING**: The installer no longer generates a Systemd service file. While this was likely convenient for most, it introduced significant complexity and caused most installations using `~/hyperglass` as the app path to fail, with no clear way to resolve it. Further, while Systemd is arguably the most common, it is not the *only* process manager available. As such, the docs will be updated with a Systemd example, much like the current reverse proxy documentation. -## 1.0.0-beta.71 - 2021-01-10 +### 1.0.0-beta.71 - 2021-01-10 ### Added - Added Google Analytics Support. Use the `google_analytics` field for the tracking ID in `hyperglass.yaml`. @@ -169,7 +267,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as ### Changed - Minor frontend code improvements. -## 1.0.0-beta.70 - 2021-01-05 +### 1.0.0-beta.70 - 2021-01-05 ### Fixed @@ -178,7 +276,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as ### Changed - Query results now automatically cancel when each result panel unmounts (e.g. when one clicks the back button). -## 1.0.0-beta.69 - 2021-01-03 +### 1.0.0-beta.69 - 2021-01-03 ### Fixed @@ -189,14 +287,14 @@ Moving forward, the `name` field is only used to define the name of the VRF **as - Setup no longer adds example files -## 1.0.0-beta.67 - 2021-01-02 +### 1.0.0-beta.67 - 2021-01-02 ### Fixed - Fix handling of `web.theme.default_color_mode`. Starting in 1.0.0-beta.65, it was completely ignored and used the library's default of `light`. Now, it's handled properly. - Fix table output layout issues, particularly on mobile. -## 1.0.0-beta.66 - 2021-01-02 +### 1.0.0-beta.66 - 2021-01-02 ### Fixed @@ -207,7 +305,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as - `web.theme.colors.black` and `web.theme.colors.white` are now `web.theme.colors.dark` and `web.theme.colors.light respectively` -## 1.0.0-beta.65 - 2021-01-01 +### 1.0.0-beta.65 - 2021-01-01 ### Added @@ -222,7 +320,7 @@ Moving forward, the `name` field is only used to define the name of the VRF **as - `web.text.title` and `web.text.subtitle` now carry a 32 character limit for simpler styling. - Various UI layout, styling improvements, and stability improvements. -## 1.0.0-beta.63 - 2020-10-18 +### 1.0.0-beta.63 - 2020-10-18 ### Added @@ -232,13 +330,13 @@ Moving forward, the `name` field is only used to define the name of the VRF **as - Fix an issue causing hyperglass custom exceptions to not be properly raised, which caused more generic error messages in the UI/API. -## 1.0.0-beta.62 - 2020-10-17 +### 1.0.0-beta.62 - 2020-10-17 ### Fixed - Fix an issue causing exceptions not to be logged to the log file (but logged to stdout). -## 1.0.0-beta.61 - 2020-10-11 +### 1.0.0-beta.61 - 2020-10-11 ### POTENTIALLY BREAKING CHANGE @@ -252,7 +350,7 @@ When hyperglass starts up, it will check to see if `~/hyperglass` or `/etc/hyper - [#81](https://github.com/thatmattlove/hyperglass/issues/81): Add support for SSH key authentication. See [the docs](https://hyperglass.dev/docs/adding-devices#credential) for more details. -## 1.0.0-beta.60 - 2020-10-10 +### 1.0.0-beta.60 - 2020-10-10 ### Fixed @@ -274,7 +372,7 @@ This would correspond with the following stanza in the Redis configuration file: requirepass examplepassword ``` -## 1.0.0-beta.59 - 2020-10-05 +### 1.0.0-beta.59 - 2020-10-05 ### Added @@ -286,7 +384,7 @@ requirepass examplepassword - Improve output parsing scalability - parsers can now be defined on a per-NOS basis regardless of whether or not structured-data is used. - Restructure model locations & importing to remove some complexities. -## 1.0.0-beta.58 - 2020-09-28 +### 1.0.0-beta.58 - 2020-09-28 ### Changed @@ -303,7 +401,7 @@ requirepass examplepassword - [#77](https://github.com/thatmattlove/hyperglass/issues/77): Allow dashes in FQDN validation pattern. - [#83](https://github.com/thatmattlove/hyperglass/issues/83): Fix lack of support for `protocol-nh` field in Juniper XML BGP table. -## 1.0.0-beta.57 - 2020-07-30 +### 1.0.0-beta.57 - 2020-07-30 ### BREAKING CHANGE @@ -321,7 +419,7 @@ $ hyperglass-agent send-certificate - Refactored device, query, proxy models to no longer scrub unsupported characters from the device name for the purposes of Python class attribute accessing. - Updated hyperglass-agent docs. -## 1.0.0-beta.56 - 2020-07-28 +### 1.0.0-beta.56 - 2020-07-28 ### Changed @@ -332,19 +430,19 @@ $ hyperglass-agent send-certificate - [#56](https://github.com/thatmattlove/hyperglass/issues/56): Fix a silent Redis connection error if the Redis server was anything other than `localhost`, preventing hyperglass from starting. -## 1.0.0-beta.55 - 2020-07-27 +### 1.0.0-beta.55 - 2020-07-27 ### Changed - Removed JS favicon build process in favor of native Python implementation ([favicons](https://github/thatmattlove/favicons)) -## 1.0.0-beta.54 - 2020-07-25 +### 1.0.0-beta.54 - 2020-07-25 ### Fixed - Queries to hyperglass-agent devices failed due to the error `AttributeError: 'AgentConnection' object has no attribute 'collect'` -## 1.0.0-beta.53 - 2020-07-23 +### 1.0.0-beta.53 - 2020-07-23 ### Added @@ -360,7 +458,7 @@ $ hyperglass-agent send-certificate - UI: Error messages couldn't be copied with the copy button -## 1.0.0-beta.52 - 2020-07-19 +### 1.0.0-beta.52 - 2020-07-19 ### Added @@ -384,7 +482,7 @@ $ hyperglass-agent send-certificate - Improve command customization docs. - [#61](https://github.com/thatmattlove/hyperglass/issues/61): Fixed copy output for table data. Output is now a bulleted list of parsed data. -## 1.0.0-beta.51 - 2020-07-13 +### 1.0.0-beta.51 - 2020-07-13 ### Changed @@ -396,7 +494,7 @@ $ hyperglass-agent send-certificate - [#54](https://github.com/thatmattlove/hyperglass/issues/54): A Junos parsing error caused routes with no communities to raise an error. - Pre-validated config files are no longer logged on startup unless debugging is enabled. -## 1.0.0-beta.50 - 2020-07-12 +### 1.0.0-beta.50 - 2020-07-12 ### Added @@ -416,7 +514,7 @@ $ hyperglass-agent send-certificate - [#54](https://github.com/thatmattlove/hyperglass/issues/54): A Junos structured/table output parsing error caused routes with multiple next-hops to raise an error. - RPKI validation no longer occurs twice (once on serialization of the output, once on validation of the API response). -## 1.0.0-beta.49 - 2020-07-05 +### 1.0.0-beta.49 - 2020-07-05 ### Changed @@ -428,7 +526,7 @@ $ hyperglass-agent send-certificate - Route lookups for private (RFC 1918) addresses failed due to an unnecessary lookup to [bgp.tools](https://bgp.tools) -## 1.0.0-beta.48 - 2020-07-04 +### 1.0.0-beta.48 - 2020-07-04 ### Added @@ -440,7 +538,7 @@ $ hyperglass-agent send-certificate - When copying the opengraph image, the copied image was not deleted. - Default traceroute help link now _actually_ points to the new docs site. -## 1.0.0-beta.47 - 2020-07-04 +### 1.0.0-beta.47 - 2020-07-04 ### Added @@ -461,13 +559,13 @@ $ hyperglass-agent send-certificate - Generated favicon manifest files now go to the correct directory. - Various docs site fixes -## 1.0.0-beta.46 - 2020-06-28 +### 1.0.0-beta.46 - 2020-06-28 ### Added - Support for hyperglass-agent [0.1.5](https://github.com/thatmattlove/hyperglass-agent) -## 1.0.0-beta.45 - 2020-06-27 +### 1.0.0-beta.45 - 2020-06-27 ### Changed @@ -478,7 +576,7 @@ $ hyperglass-agent send-certificate - Webhook construction bugs that caused webhooks not to send - Empty response handling for table output -## 1.0.0-beta.44 - 2020-06-26 +### 1.0.0-beta.44 - 2020-06-26 ### Added @@ -488,13 +586,13 @@ $ hyperglass-agent send-certificate - If webhooks were enabled, a hung test connection to RIPEStat would cause the query to time out -## 1.0.0-beta.43 - 2020-06-22 +### 1.0.0-beta.43 - 2020-06-22 ### Fixed - Logo path handling in UI -## 1.0.0-beta.42 - 2020-06-21 +### 1.0.0-beta.42 - 2020-06-21 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f060ade64..1e7d8fc12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,29 +1,18 @@ -hyperglass is primarily maintained by me, [Matt Love](https://github.com/thatmattlove). This is my first ever open source application, and as such, it's kind of my "baby". When I first started writing hyperglass, I knew _nothing_ about development, Python, Javascript, or Github. I was a network engineer trying to solve a problem and learn a few things while I was at it. +hyperglass is primarily maintained by me, [Matt Love](https://github.com/thatmattlove). This was my first ever open source application, and as such, it's kind of my "baby". When I first started writing hyperglass, I knew _nothing_ about development, Python, JavaScript/TypeScript, or GitHub. I was a network engineer trying to solve a problem and learn a few things while I was at it. -Because I've been solo-maintaining and building hyperglass since around April 2019, I've become pretty particular about things that might seem trivial to someone just trying to help out. While I **absolutely welcome development contributions**, please don't be offended if pull requests are denied, or if I request things to be done a certain way. To help understand why, here are some of the development design goals for hyperglass: +Because I've been solo-maintaining and building hyperglass since around April 2019, I've become pretty particular about things that might seem trivial to someone just trying to help out. While I welcome development contributions, please don't be offended if pull requests are denied, if I request things to be done a certain way, or if I integrate something similar to your changes separately from your PR. To help understand why, here are some of the development design goals for hyperglass: - **Pristine code quality** - - [Black](https://github.com/python/black) formatting for Python - - Strict adherence to ESLint/Prettier configs for Javascript/React - - _ZERO_ linting errors + - [Black](https://github.com/python/black) formatting for Python. + - Strict adherence to ESLint/Prettier configs for frontend code. + - _ZERO_ linting errors. - Linting exceptions only used when there is _no other way_, and should be accompanied with comments about why there is no other way. - **No hard-coding** - Anything visible to the end-user _must_ be customizable by the administrator. If it's not, or can't be, leave code or PR comments as to why. - This includes things like timeouts, error messages, etc. - **Mobile & Accessible** - - All UI element must be available on both desktop and mobile devices - - UI must achieve a 100 Lighthouse/PageInsights score for accessibility + - All UI element must be available on both desktop and mobile devices. + - UI must achieve a 100 Lighthouse/PageInsights score for accessibility. - **IPv6 Support** - - Any new device support must include IPv6 commands - - All frontend and backend code must support IPv6, both for running the application and processing queries - -## Branches - -The following are the primary branches used for development and release management: - -| Branch Name | Function | -| :---------- | :--------------------- | -| `main` | Tagged Stable Releases | -| `develop` | Ongoing Development | - -Pull requests should be made against the `develop` branch. + - Any new device support must include IPv6 commands. + - All frontend and backend code must support IPv6, both for running the application and processing queries. diff --git a/Dockerfile b/Dockerfile index 02e56d5a1..2bde7752e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,64 +1,37 @@ -ARG HYPERGLASS_PATH=/opt/hyperglass -FROM python:3.10 AS base - -# TODO nodejs and yarn are required during "build-ui", but also at runtime (in "start") -# To my understanding the runtime requirement should be removed, because it should only be used in "build-ui" -# Once fixed, this RUN can be moved to the builder stage instead -RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ - && curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null \ - && echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | tee /etc/apt/sources.list.d/yarn.list \ - && apt update \ - && apt install -y nodejs yarn libjpeg-dev zlib1g-dev \ - && rm -rf /var/lib/apt/lists/* - -FROM base AS builder -ARG HYPERGLASS_PATH - -RUN curl -sSL https://install.python-poetry.org | python3 - -RUN mkdir -p /usr/local/src/hyperglass ${HYPERGLASS_PATH} -# TODO Only COPY the files that are required for the build -# Keep .dockerignore in mind too -COPY . /usr/local/src/hyperglass -WORKDIR /usr/local/src/hyperglass -RUN /root/.poetry/bin/poetry build -RUN cd ./dist/ && pip install hyperglass*.whl -RUN hyperglass setup -# TODO "build-ui" needs a devices.yaml. -# The "build-ui" requirement for devices.yaml should be removed, because to my understanding it's only needed in "start" -# Once fixed, this COPY can be moved to the app stage instead -COPY ./hyperglass/examples/devices.yaml ${HYPERGLASS_PATH} -RUN hyperglass build-ui - -FROM base AS app -ARG HYPERGLASS_PATH - -COPY --from=builder /usr/local/src/hyperglass/dist/ /tmp/build -RUN cd /tmp/build/ && pip install hyperglass*.whl && \ - useradd -s /usr/sbin/nologin hyperglass && \ - pip3 install -Iv asyncssh==2.8.1 - -COPY --from=builder --chown=hyperglass:hyperglass /usr/local/lib/python3.8/site-packages/hyperglass/ui/node_modules /usr/local/lib/python3.8/site-packages/hyperglass/ui/node_modules -COPY --from=builder --chown=hyperglass:hyperglass ${HYPERGLASS_PATH} ${HYPERGLASS_PATH} -COPY --chown=hyperglass:hyperglass ./hyperglass/examples/hyperglass.docker.yaml ${HYPERGLASS_PATH}/hyperglass.yaml -COPY hyperglass_start /hyperglass_start -# TODO Log to stderr by default instead of to /tmp/hyperglass.log -# RUN ln -sf /dev/stderr /tmp/hyperglass.log -# ^ Won't work because stderr isn't seekable - -# TODO hyperglass needs to run as root, i.e. because -# "EACCES: permission denied, mkdir '/usr/local/lib/python3.9/site-packages/hyperglass/ui/node_modules'" -# This is undesired, uncomment the next line once fixed -# USER hyperglass - -FROM python:3.10 - -COPY --from=app / / - -ARG HYPERGLASS_PATH - -ENV HYPERGLASS_PATH ${HYPERGLASS_PATH} - -RUN hyperglass build-ui - +FROM node:22.22.0-bookworm-slim@sha256:dd9d21971ec4395903fa6143c2b9267d048ae01ca6d3ea96f16cb30df6187d94 AS node + +FROM python:3.12.12-slim-bookworm@sha256:593bd06efe90efa80dc4eee3948be7c0fde4134606dd40d8dd8dbcade98e669c AS dependencies +WORKDIR /opt/hyperglass +COPY --from=node /usr/local/ /usr/local/ +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential pkg-config libcairo2-dev libffi-dev curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* +RUN npm install --global pnpm@10.11.0 +ENV HYPERGLASS_APP_PATH=/etc/hyperglass \ + HYPERGLASS_HOST=0.0.0.0 \ + HYPERGLASS_PORT=8001 \ + HYPERGLASS_DEBUG=false \ + HYPERGLASS_DEV_MODE=false \ + HYPERGLASS_REDIS_HOST=localhost \ + HYPERGLASS_DISABLE_UI=false \ + HYPERGLASS_CONTAINER=true \ + NEXT_TELEMETRY_DISABLED=1 \ + CI=true +RUN mkdir -p /etc/hyperglass/static/images /etc/hyperglass/plugins +COPY hyperglass/ui/package.json hyperglass/ui/pnpm-lock.yaml hyperglass/ui/pnpm-workspace.yaml ./hyperglass/ui/ +RUN pnpm --dir hyperglass/ui install --frozen-lockfile +COPY . . +RUN pip install --no-cache-dir hatchling==1.24.2 editables==0.5 setuptools==69.2.0 wheel==0.43.0 \ + && pip install --no-cache-dir --no-deps --no-build-isolation -r requirements.lock \ + && pip check + +FROM dependencies AS test +RUN pip install --no-cache-dir --no-deps --no-build-isolation -r requirements-dev.lock + +FROM dependencies AS app +ARG APPLICATION_REVISION=unknown +LABEL org.opencontainers.image.source="https://github.com/X4BNet/hyperglass" \ + org.opencontainers.image.revision=$APPLICATION_REVISION \ + io.x4b.hyperglass.upstream="fd34bda03fe3382cb14a00dc9ec76cf282bc3e0a" EXPOSE 8001 -CMD ["/hyperglass_start"] +CMD ["python3", "-m", "hyperglass.console", "start", "--workers", "2"] diff --git a/LICENSE b/LICENSE index 0e59931de..3f48246e2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The Clear BSD License -Copyright (c) 2021 Matthew Love +Copyright (c) 2024 Matthew Love All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 53474eff3..abd4c7d8c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +> **X4B fork:** See [UPGRADE.md](UPGRADE.md) for the v2 container, compatibility choices, checks, and coordinated Kubernetes rollout. +

@@ -13,13 +15,8 @@ [**Documentation**](https://hyperglass.dev)   |   [**Live Demo**](https://demo.hyperglass.dev/) -[![PyPI](https://img.shields.io/pypi/v/hyperglass?style=for-the-badge)](https://pypi.org/project/hyperglass/) -![PyPI - Downloads](https://img.shields.io/pypi/dm/hyperglass?color=%2340798C&style=for-the-badge) -[![GitHub Contributors](https://img.shields.io/github/contributors/thatmattlove/hyperglass?color=40798C&style=for-the-badge)](https://github.com/thatmattlove/hyperglass) - -[![Frontend Tests](https://img.shields.io/github/workflow/status/thatmattlove/hyperglass/Frontend%20Testing?label=Frontend%20Tests&style=for-the-badge)](https://github.com/thatmattlove/hyperglass/actions?query=workflow%3A%Frontend+Testing%22) -[![Backend Tests](https://img.shields.io/github/workflow/status/thatmattlove/hyperglass/Backend%20Testing?label=Backend%20Tests&style=for-the-badge)](https://github.com/thatmattlove/hyperglass/actions?query=workflow%3A%Backend+Testing%22) -[![Installer Tests](https://img.shields.io/github/workflow/status/thatmattlove/hyperglass/Installer%20Testing?label=Installer%20Tests&style=for-the-badge)](https://github.com/thatmattlove/hyperglass/actions?query=workflow%3A%Installer+Testing%22) +[![Frontend Tests](https://img.shields.io/github/actions/workflow/status/thatmattlove/hyperglass/frontend.yml?label=Frontend%20Tests&style=for-the-badge)](https://github.com/thatmattlove/hyperglass/actions/workflows/frontend.yml) +[![Backend Tests](https://img.shields.io/github/actions/workflow/status/thatmattlove/hyperglass/backend.yml?label=Backend%20Tests&style=for-the-badge)](https://github.com/thatmattlove/hyperglass/actions/workflows/backend.yml)
@@ -27,30 +24,30 @@ hyperglass is intended to make implementing a looking glass too easy not to do,
-### [Changelog](https://github.com/thatmattlove/hyperglass/blob/v1.0.0/CHANGELOG.md) +### [Changelog](https://hyperglass.dev/changelog) ## Features -- BGP Route, BGP Community, BGP AS Path, Ping, & Traceroute +- BGP Route, BGP Community, BGP AS Path, Ping, & Traceroute, or [add your own commands](https://hyperglass.dev/configuration/directives). - Full IPv6 support - Customizable everything: features, theme, UI/API text, error messages, commands -- Built in support for: - - Arista EOS - - BIRD - - Cisco IOS-XR - - Cisco IOS/IOS-XE - - Cisco NX-OS - - FRRouting - - Huawei - - Juniper JunOS - - Mikrotik - - Nokia SR OS - - TNSR - - VyOS -- Configurable support for any other [supported platform](https://hyperglass.dev/docs/platforms) +- Built-in support for: + - Arista EOS + - BIRD + - Cisco IOS + - Cisco NX-OS + - Cisco IOS-XR + - FRRouting + - Huawei VRP + - Juniper Junos + - Mikrotik + - Nokia SR OS + - OpenBGPD + - TNSR + - VyOS +- Configurable support for any other [supported platform](https://hyperglass.dev/platforms) - Optionally access devices via an SSH proxy/jump server -- VRF support -- Access List/prefix-list style query control to whitelist or blacklist query targets on a per-VRF basis +- Access-list/prefix-list style query control to whitelist or blacklist query targets - REST API with automatic, configurable OpenAPI documentation - Modern, responsive UI built on [ReactJS](https://reactjs.org/), with [NextJS](https://nextjs.org/) & [Chakra UI](https://chakra-ui.com/), written in [TypeScript](https://www.typescriptlang.org/) - Query multiple devices simultaneously @@ -58,7 +55,7 @@ hyperglass is intended to make implementing a looking glass too easy not to do, *To request support for a specific platform, please [submit a Github Issue](https://github.com/thatmattlove/hyperglass/issues/new) with the **feature** label.* -### [Get Started →](https://hyperglass.dev/docs/introduction) +### [Get Started →](https://hyperglass.dev/installation) ## Community @@ -71,35 +68,15 @@ Any users, potential users, or contributors of hyperglass are welcome to join an *However, if you're feeling particularly helpful or generous, small donations are welcome.* -## Docker - -### User: General use - -- Copy [docker-compose.yaml](docker-compose.yaml) to your destination and edit as desired -- Create a valid `devices.yaml` - - I.e. based on [hyperglass/examples/devices.yaml](hyperglass/examples/devices.yaml) -- Create a valid `hyperglass.yaml` - - I.e. based on [hyperglass/examples/hyperglass.docker.yaml](hyperglass/examples/hyperglass.docker.yaml) - - If redis runs on docker make sure `cache.host` is set to `redis.hyperglass` -- Run `docker-compose up -d` -- Run `docker-compose ps`, to wait till it's up - -### Developer: Build and publish - -- Copy [docker-compose.build.yaml](docker-compose.build.yaml) and [docker-compose.yaml](docker-compose.yaml) to your destination and edit as desired -- `docker-compose -f docker-compose.yaml -f docker-compose.build.yaml build` -- `docker push ...` - [![Donate](https://img.shields.io/badge/Donate-blue.svg?logo=paypal&style=for-the-badge)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ZQFH3BB2B5M3E&source=url) ## Acknowledgements hyperglass is built entirely on open-source software. Here are some of the awesome libraries used, check them out too! -- [FastAPI](https://fastapi.tiangolo.com/) - [Netmiko](https://github.com/ktbyers/netmiko) -- [Scrapli](https://github.com/carlmontanari/scrapli) -- [Pydantic](https://pydantic-docs.helpmanual.io/) +- [Litestar](https://litestar.dev) +- [Pydantic](https://docs.pydantic.dev/latest/) - [Chakra UI](https://chakra-ui.com/) -[![GitHub](https://img.shields.io/github/license/thatmattlove/hyperglass?color=330036&style=for-the-badge)](https://github.com/thatmattlove/hyperglass/blob/v1.0.0/LICENSE) +[![GitHub](https://img.shields.io/github/license/thatmattlove/hyperglass?color=330036&style=for-the-badge)](https://github.com/thatmattlove/hyperglass/blob/main/LICENSE) diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 000000000..80a01520a --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,33 @@ +# X4B hyperglass v2 + +This fork merges upstream `fd34bda03fe3382cb14a00dc9ec76cf282bc3e0a` (v2.0.4 plus unreleased fixes) into the v1 fork without rewriting its history. + +## Build and check + +The container pins Python 3.12.12, Node 22.22.0, pnpm 10.11.0, and base-image digests. Python requirements use upstream's checked-in locks; the UI uses its pnpm lock. Application startup builds UI assets from the mounted configuration, using the image's offline dependency store. Next.js build concurrency is limited for the existing 3 CPU / 2 GiB pod budget. UI dependencies and generated asset directories must remain writable. + +```sh +docker build --target test -t hyperglass:test . +docker build --build-arg APPLICATION_REVISION="$(git rev-parse HEAD)" -t hyperglass:local . +.tests/container-smoke.sh hyperglass:local +../k8s-hyperglass/scripts/test.sh hyperglass:test +.tests/container-smoke.sh hyperglass:local ../k8s-hyperglass/config +``` + +The migration test command uses isolated Redis and mocks router access and containing-prefix lookup. The smoke test uses a network without internet access, checks mounted-key permissions and static assets, then verifies restart and graceful shutdown. It performs no router queries. + +CI runs the backend suite/Ruff and frontend formatting/Biome/TypeScript/Vitest before building and smoke-testing the application image. Only trusted pushes to `main` or `v*` tags publish `x4bnet/hyperglass:git-`; `main` also publishes `latest`. Deploy using the reported digest. PRs never publish images or deploy. + +## Configuration and deployment + +The coordinated change in `X4BNet/k8s-hyperglass` owns devices, directives, the X4B BIRD plugin, and rollout tooling. Mount its `devices.yaml`, `directives.yaml`, `config.yaml`, `plugins/x4b_bird.py`, and the SSH key under `/etc/hyperglass`; give the key mode `0400`. Use per-file mounts so hyperglass can write generated assets alongside them. + +Runtime defaults bind `0.0.0.0:8001`, use local Redis, keep the UI enabled, and run two workers. Compose overrides Redis to the service hostname. The old `HYPERGLASS_PATH`, `hyperglass.yaml`, `commands.yaml`, and VRF configuration are replaced by upstream v2 settings and directives. The API accepts device and directive IDs; no v1 API compatibility layer is included. + +See the deployment repository's `UPGRADE.md` for deployment, rollback, and the validation record. The upgrade updates the existing `hyperglass` StatefulSet, Service, and configuration Secret. Trusted configuration pushes deploy development; production changes use the existing GitOps release process. + +## Deliberate compatibility choices + +The old validator never enforced configured prefix-length limits. The migrated directives therefore retain host/prefix queries, RFC1918 denials, and reserved/loopback/unspecified rejection. They preserve the public-host containing-prefix lookup and BIRD AS-path/community formatting through a v2 input plugin. Dual-stack community/AS-path queries execute both `birdc` and `birdc6`; Brazil uses only IPv4 commands. + +Obsolete v1 Redis, asyncssh, installer, and frontend patches were retired in favor of upstream implementations. Remaining application changes concern reproducible container startup, bounded UI build concurrency, build-retry correctness, and SSH collection outside the event loop with per-request async timeouts. diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..6329765b4 --- /dev/null +++ b/biome.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "organizeImports": { + "enabled": true + }, + "files": { + "ignore": [ + "node_modules", + "dist", + ".next/", + "out/", + "favicon-formats.ts", + "custom.*[js, html]", + "hyperglass.json" + ] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noUselessTypeConstraint": "off", + "noBannedTypes": "off" + }, + "style": { + "noInferrableTypes": "off", + "noNonNullAssertion": "off" + }, + "correctness": { + "useExhaustiveDependencies": "off" + } + } + }, + "formatter": { + "indentStyle": "space", + "lineWidth": 100, + "indentWidth": 2 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "bracketSpacing": true, + "semicolons": "always", + "arrowParentheses": "asNeeded", + "trailingComma": "all" + } + } +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..d4f50c929 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,22 @@ +services: + redis: + image: redis:7.4.7-alpine@sha256:02f2cc4882f8bf87c79a220ac958f58c700bdec0dfb9b9ea61b62fb0e8f1bfcf + healthcheck: + test: [CMD, redis-cli, ping] + interval: 5s + timeout: 3s + retries: 12 + hyperglass: + build: . + depends_on: + redis: + condition: service_healthy + environment: + HYPERGLASS_REDIS_HOST: redis + HYPERGLASS_HOST: 0.0.0.0 + HYPERGLASS_PORT: 8001 + HYPERGLASS_DISABLE_UI: 'false' + ports: + - '8001:8001' + volumes: + - ${HYPERGLASS_APP_PATH:-/etc/hyperglass}:/etc/hyperglass diff --git a/docker-compose.build.yaml b/docker-compose.build.yaml deleted file mode 100644 index ec531d606..000000000 --- a/docker-compose.build.yaml +++ /dev/null @@ -1,7 +0,0 @@ -%YAML 1.1 ---- -version: '3.7' -services: - hyperglass: - build: - context: . diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 83992e392..000000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,37 +0,0 @@ -%YAML 1.1 ---- -version: '3.7' -services: - hyperglass: - image: checktheroads/hyperglass:latest - networks: - - hyperglass - # restart: unless-stopped - # ports: - # - 8001:8001 - # volumes: - # - ./devices.yaml:/opt/hyperglass/devices.yaml - # - ./hyperglass.yaml:/opt/hyperglass/hyperglass.yaml - # - ./commands.yaml:/opt/hyperglass/commands.yaml - depends_on: - - redis - healthcheck: - test: test "$$(curl -o /dev/null -s --user-agent docker-healthcheck http://localhost:8001 --write-out %{http_code})" = "200" - interval: 5s - timeout: 5s - retries: 20 - redis: - image: redis:latest - networks: - - hyperglass - # restart: unless-stopped - command: [redis-server, --appendonly, 'yes'] - volumes: - - redis-data:/data - -volumes: - redis-data: - -networks: - hyperglass: - name: hyperglass diff --git a/docs/.gitignore b/docs/.gitignore old mode 100755 new mode 100644 index 68475e42a..8ade9b27b --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,23 +1,10 @@ -# Project -theme-og - -# Dependencies -/node_modules - -# Production -/build - -# Generated files -.docusaurus -.cache-loader - -# Misc .DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* +.env* +*.tsbuildinfo +*.tmp* +*.log +node_modules/ +fonts/ +.next +out +pages/changelog.mdx diff --git a/docs/README.md b/docs/README.md deleted file mode 100755 index ee0ccc9d1..000000000 --- a/docs/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Website - -This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. - -### Installation - -``` -$ yarn -``` - -### Local Development - -``` -$ yarn start -``` - -This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. - -### Build - -``` -$ yarn build -``` - -This command generates static content into the `build` directory and can be served using any static contents hosting service. - -### Deployment - -``` -$ GIT_USER= USE_SSH=true yarn deploy -``` - -If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/docs/components/color.tsx b/docs/components/color.tsx new file mode 100644 index 000000000..0234cba37 --- /dev/null +++ b/docs/components/color.tsx @@ -0,0 +1,29 @@ +type ColorProps = { + hex: string; + noText?: boolean; +}; + +export const Color = (props: ColorProps) => { + const { hex, noText = false } = props; + return ( + + + {noText ? "" : hex} + + + ); +}; diff --git a/docs/components/docs-button.tsx b/docs/components/docs-button.tsx new file mode 100644 index 000000000..c06c17730 --- /dev/null +++ b/docs/components/docs-button.tsx @@ -0,0 +1,36 @@ +import NextLink from "next/link"; +import { Button } from "nextra/components"; + +const DocsIcon = () => ( + + Docs + + +); + +export interface DocsButtonProps extends React.ComponentProps<"button"> { + href: string; + side?: "left" | "right"; +} + +export const DocsButton = (props: DocsButtonProps) => { + const { href, side = "left", ...rest } = props; + return ( + + + + ); +}; diff --git a/docs/components/not-supported-icon.tsx b/docs/components/not-supported-icon.tsx new file mode 100644 index 000000000..5176aa28e --- /dev/null +++ b/docs/components/not-supported-icon.tsx @@ -0,0 +1,14 @@ +export const NotSupported = (props: React.ComponentProps<"svg">) => ( + + Not Supported + + + +); diff --git a/docs/components/platforms.tsx b/docs/components/platforms.tsx new file mode 100644 index 000000000..dda7d708a --- /dev/null +++ b/docs/components/platforms.tsx @@ -0,0 +1,44 @@ +import { Code, Table, Td, Th, Tr } from "nextra/components"; +import platforms from "~/platforms.json"; +import { NotSupported } from "./not-supported-icon"; +import { Supported } from "./supported-icon"; + +export const SupportedPlatforms = () => ( +
    + {platforms.reduce((final, platform) => { + if (platform.native) { + const element = ( +
  • + + {platform.name} +
  • + ); + final.push(element); + } + return final; + }, [])} +
+); + +export const PlatformTable = () => ( + + + + + + + {platforms.map((spec) => ( + + + + + ))} + +
Platform KeysNatively Supported
+ {spec.keys.map((key) => ( + + {key} + + ))} + {spec.native ? : }
+); diff --git a/docs/components/supported-icon.tsx b/docs/components/supported-icon.tsx new file mode 100644 index 000000000..f053c38f4 --- /dev/null +++ b/docs/components/supported-icon.tsx @@ -0,0 +1,13 @@ +export const Supported = (props: React.ComponentProps<"svg">) => ( + + Supported + + +); diff --git a/docs/docs/.gitignore b/docs/docs/.gitignore deleted file mode 100755 index b2d6de306..000000000 --- a/docs/docs/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -# Dependencies -/node_modules - -# Production -/build - -# Generated files -.docusaurus -.cache-loader - -# Misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/docs/docs/adding-devices.mdx b/docs/docs/adding-devices.mdx deleted file mode 100644 index ea9a5d647..000000000 --- a/docs/docs/adding-devices.mdx +++ /dev/null @@ -1,393 +0,0 @@ ---- -id: adding-devices -title: Adding Devices -sidebar_label: Adding Devices -keywords: - [hyperglass, authentication, ssl, proxy, access list, prefix list, acl, help] -description: Adding devices to hyperglass ---- - -import Link from "@docusaurus/Link"; -import R from "../src/components/Required"; -import MiniNote from "../src/components/MiniNote"; -import Code from "../src/components/JSXCode"; - -
- -## Adding Devices - -To add, as an example, a Cisco router, add the following to your `devices.yaml`, with the relevant details changed for your device: - -```yaml title="devices.yaml" -routers: - - name: New York City, NY - address: 10.0.0.1 - network: - name: production - display_name: AS65000 - credential: - username: username - password: password - port: 22 - nos: cisco_ios - vrfs: - - name: global - default: true - ipv4: - source_address: 192.0.2.1 - ipv6: - source_address: 2001:db8::1 -``` - -## All Device Parameters - -| Parameter | Type | Description | -| :------------------ | :-----: | :---------------------------------------------------------------------------------------------------------------- | -| `name` | String | Device's user-facing name. | -| `address` | String | Device management hostname or IP address. | -| `network` | String | [Network Configuration](#network) | -| `port` | Integer | TCP port used to connect to the device. `22` by default. | -| `nos` | String | Network Operating System. Must be a supported platform. | -| `structured_output` | Boolean | Disabled output parsing to structured data. | -| `driver` | String | Override the device driver. Must be 'scrapli' or 'netmiko'. | -| `credential` | | [Device Credential Configuration](#credential) | -| `vrfs` | | [Device VRF Configuration](#vrfs) | -| `proxy` | | [SSH Proxy Configuration](#proxy) | -| `ssl` | | [SSL Configuration](#ssl) for devices using [hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent). | - -### `network` - -A network is only used for grouping. For example, if your network contains more than one ASN, you might want to group each device separately. Currently, you must define a network on each device. - -| Parameter | Type | Description | -| :------------------ | :----: | :------------------------------------------------------------------- | -| `name` | String | The network's name. This name is only used by hyperglass internally. | -| `display_name` | String | The network's use-facing name. | - -### `proxy` - -Any device that uses SSH (see [platforms](platforms) for breakdown) can be accessed through an intermediary SSH "proxy". The process is nearly identical to using local SSH tunneling, e.g. `ssh -L local_port:remote_device:remote_port username@proxy_server -p proxy_port`. - -| Parameter | Type | Default | Description | -| :---------------- | :-----: | :------------ | :--------------------------------------------------------------------------------------------------------------- | -| `name` | String | | Proxy hostname. | -| `address` | String | | Proxy management hostname or IP address. | -| `credential` | | | [Proxy Credential Configuration](#credential) | -| `nos` | String | `'linux_ssh'` | Proxy's network operating system. Must be a supported platform. | -| `port` | Integer | `22` | TCP port user to connect to the proxy. | - -:::important -Currently, only `linux_ssh` has been tested and validated for use as an SSH proxy. -::: - -### `credential` - -While all devices require a credential mapping, the credential values themselves may be used in different ways depending on the device NOS. For SSH devices, the credential is used as a typical SSH username and password. - -For HTTP devices (i.e. devices using [hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent)), the username is ignored and the password is used as a secret for [JSON Web Token](https://tools.ietf.org/html/rfc7519) encoding/decoding. - -| Parameter | Type | Description | -| :-------------- | :----- | :----------------------------------------------------------- | -| `username` | String | Username | -| `password` | String | Password Passwords will never be logged | -| `key` | Path | Path to SSH Private Key | - -To use SSH key authentication, simply specify the path to the SSH private key with `key:`. If the key is encrypted, set the private key's password to the with the `password:` field, and hyperglass will use it to decrypt the SSH key. - -### `ssl` - -HTTP devices may optionally use SSL for the connection between hyperglass and the device. This is **disabled** by default, which means devices will use unencrypted HTTP by default. - -If SSL is enabled, the public key of the device must be provided in the form of an accessible absolute file path. With SSL enabled and a valid certificate specified, every connection to the device will use HTTPS in addition to payload encoding with [JSON Web Tokens](https://tools.ietf.org/html/rfc7519). - -| Parameter | Type | Description | -| :---------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `enable` | Boolean | Enable or disable the use of SSL. If enabled, a certificate file must be specified (hyperglass does not support connecting to a device over an unverified SSL session). | -| `cert` | String | Absolute path to agent's public RSA key. | - -### `vrfs` - -The VRFs section is a list of available VRFs for a given device. Each VRF may be configured with the following fields: - -| Parameter | Type | Description | -| :------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | String | The VRF's name, as known **by the device**. hyperglass sends this field to the device for queries, so it needs to match the device's configuration. If this is the default/global routing table on the device, this value isn't sent to the device. | -| `display_name` | String | The VRF's user-facing name. This field's value is visible in the UI. If this is not specified, hyperglass will try to create a "pretty" display name based on the `name` field. | -| `default` | Boolean | Indicate that this VRF is the device's default/global routing table (more specifically, that it does not require specifying the VRF name when running commands).If this is set to `true`, the `ipv4_default`/`ipv6_default` [command sets](commands.mdx). If set to `false`, the `ipv4_vpn`/`ipv6_vpn` command sets will be used. | -| `info` | | [Per-VRF Contextual Help Configuration](#info) | -| `ipv4` | | [VRF's IPv4 Configuration](#ipv4) | -| `ipv6` | | [VRF's IPv6 Configuration](#ipv6) | - -#### `ipv4` - -May be set to `null` to disable IPv4 for this VRF, on the parent device. - -| Parameter | Type | Description | -| :-------------------- | :------ | :-------------------------------------------------------------------- | -| `source_address` | String | Device's source IPv4 address for directed queries (ping, traceroute). | -| `force_cidr` | Boolean | `true` | Convert IP host queries to actual advertised containing prefix length | -| `access_list` | | IPv4 Access List Configuration | - -#### `ipv6` - -May be set to `null` to disable IPv6 for this VRF, on the parent device. - -| Parameter | Type | Default | Description | -| :-------------------- | :------ | :------ | :-------------------------------------------------------------------------------------------------------------------------------------- | -| `source_address` | String | | Device's source IPv6 address for directed queries (ping, traceroute). This address must be surrounded by quotes. Ex. "0000:0000:0000::" | -| `force_cidr` | Boolean | `true` | Convert IP host queries to actual advertised containing prefix length | -| `access_list` | | | [IPv6 Access List Configuration](#access_list) | - -:::note -The `force_cidr` option will ensure that a **BGP Route** query for an IP host (/32 IPv4, /128 IPv6) is converted to its containing prefix. For example, a query for `1.1.1.1` would be converted to a query for `1.1.1.0/24`. This is because not all platforms support a BGP lookup for a host (this is primary a problem with IPv6, but the option applies to both address families). - -When `force_cidr`is set to `true`, hyperglass will perform a lookup via the [bgp.tools](https://bgp.tools) whois API to get the advertised prefix for an IP host. -::: - -#### `access_list` - -The `access_list` block can be thought of like a prefix-list from Cisco IOS. It is a list of rules, where the first matching rule is the action executed. - -| Parameter | Type | Default | Description | -| :------------- | :------ | :-----------: | :-------------------------------------------------------------------------- | -| `network` | String | | This rule's IPv4 or IPv6 base prefix | -| `action` | String | `'permit'` | This rule's action. Must be `permit` or `deny` | -| `ge` | Integer | `0` | To match this rule, the target prefix must be greater than or equal to `ge` | -| `le` | Integer | `32` \| `128` | To match this rule, the target prefix must be less than or equal to `le` | - -#### `info` - -Each VRF may enable, disable, or customize the contextual help menu for each enabled query type. The following parameters may be defined under any query type: - -| Parameter | Type | Default | Description | -| :-------- | :------ | :-----: | :-------------------------------------------------------------------------------------------------------------------------- | -| `enable` | Boolean | `true` | Enable or disable the help menu for this command. | -| `file` | String | | Path to a plain text or markdown file containing customized help information for this command. | -| `params` | Object | | Any arbitrary key/value pairs where the value will replace any occurrences of the key when wrapped in braces (e.g. `{key}`) | - -For example: - -```yaml title="devices.yaml" -routers: - - name: router01 - vrfs: - - name: demo_vrf - info: - bgp_route: - enable: true - file: /etc/hyperglass/customer_bgp_route.md - params: - vrf_name: Customer A - ping: - enable: false - bgp_community: - enable: true - file: /etc/hyperglass/customer_bgp_community.md - params: - vrf_name: Customer A - community: "65000" -``` - -## Telnet - -Telnet support is provided via the underlying device connection handling framework, [Netmiko](https://github.com/ktbyers/netmiko). To connect to a device via serial, add the suffix `_telnet` to the device's `nos` value and set the `port` value to `23`. - -For example: - -```yaml {3-4} title="devices.yaml" -routers: - - name: router01 - nos: cisco_ios_telnet - port: 23 - ... -``` - -## Full Example - -Below is a full example with nearly every available knob turned: - -```yaml title="devices.yaml" -routers: - # HTTP/hyperglass-agent device - - name: HTTP Router - address: 192.0.2.1 - network: - name: primary - display_name: AS65000 - credential: - username: madeup - password: txeTTIqwhKSJi4V3tefXQASf5AyGZ6cPsycS9nYLpKk - ssl: - enable: true - cert: /etc/hyperglass/certs/router01.pem - port: 8080 - nos: frr - vrfs: - - name: global - default: true - ipv4: - source_address: 192.0.2.1 - access_list: - - network: 10.0.0.0/8 - action: deny - ge: 8 - le: 32 - - network: 0.0.0.0/0 - action: permit - le: 24 # Only allow /24 or smaller prefixes - ipv6: - source_address: 2001:db8::1 - access_list: - - network: ::/0 - action: permit - proxy: null - - # SSH/netmiko device - - name: SSH Router - address: 10.0.0.1 - network: - name: primary - display_name: AS65000 - credential: - username: user - password: pass - port: 22 - nos: cisco_ios - vrfs: - - name: global - default: true - ipv4: - source_address: 192.0.2.2 - access_list: - - network: 10.0.0.0/8 - action: deny - ge: 8 - le: 32 - - network: 0.0.0.0/0 - action: permit - ipv6: - source_address: 2001:db8::2 - access_list: - - network: ::/0 - action: permit - le: 64 # Only allow /64 or smaller prefixes - - name: special_customer - display_name: Customer Name - ipv4: - source_address: 172.16.0.1 - access_list: - - network: 172.16.0.0/16 - action: allow - ipv6: null # disable IPv6 for this VRF - info: - bgp_route: - file: /etc/hyperglass/help/customer_bgp_route.md - params: - customer_name: Customer Name - proxy: - name: jump_server - address: 10.0.1.100 - port: 22 - credential: - username: user - password: pass - nos: linux_ssh -``` - -## YAML Anchors & Aliases - -If you have a lot of devices with shared configuration parameters, you may want to look into **YAML Anchors and Aliases**. If you've never used them before, they can be pretty weird looking at first read. Atlassian [has a pretty decent guide](https://confluence.atlassian.com/bitbucket/yaml-anchors-960154027.html). - -Here's an example of using this to share two sets of credentials among multiple devices: - -```yaml title="devices.yaml" -my_credentials: - - credential: &credential1 - username: madeup1 - password: gY018mR4gx4sVqc0 - - credential: &credential2 - username: madeup2 - password: 0eMEJ4ZpB6ofkiIF - -routers: - - name: router01 - credential: *credential1 - - name: router02 - credential: *credential2 - - name: router03 - credential: *credential1 - - name: router04 - credential: *credential2 -``` - -:::important -Nothing other than the `routers` key is read by hyperglass. In the above example, `my_credentials` is just an arbitrary list of mappings, is completely optional, and can be named whatever you want. -::: - -For a more complex example, here's an example of how to use YAML aliases & anchors to share a common VRF configuration among multiple devices, while overriding key variables such as the `source_address` key: - -```yaml title="devices.yaml" -my_vrfs: - - &default - name: global - default: true - display_name: Global - ipv4: - access_list: &default_ipv4_acl - - network: 10.0.0.0/8 - action: deny - - network: 192.168.0.0/16 - action: deny - - network: 172.16.0.0/12 - action: deny - - network: 0.0.0.0/0 - action: permit - ge: 8 - le: 24 - ipv6: - access_list: &default_ipv6_acl - - network: ::/0 - action: permit - ge: 32 - le: 64 - - &customer_a - name: customer_a - default: false - display_name: Customer A - ipv4: - access_list: &customer_a_ipv4_acl - - network: 10.0.0.0/8 - action: permit - - network: 0.0.0.0/0 - action: deny - ipv6: null - -routers: - - name: router01 - vrfs: - - <<: *default - ipv4: - source_address: 192.0.2.1 - access_list: *default_ipv4_acl - ipv6: - source_address: 2001:db8::1 - access_list: *default_ipv6_acl - - <<: *customer_a - ipv4: - source_address: 10.0.0.1 - access_list: *customer_a_ipv4_acl - - - name: router02 - vrfs: - - <<: *default - ipv4: - source_address: 192.0.2.2 - access_list: *default_ipv4_acl - ipv6: - source_address: 2001:db8::2 - access_list: *default_ipv6_acl - - <<: *customer_a - ipv4: - source_address: 10.0.0.2 - access_list: *customer_a_ipv4_acl -``` diff --git a/docs/docs/agent/installation.mdx b/docs/docs/agent/installation.mdx deleted file mode 100644 index 3585bd740..000000000 --- a/docs/docs/agent/installation.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -id: installation -title: Linux Agent Installation -sidebar_label: Installation -keywords: [configuration, linux, frr, frrouting, bird, agent] -description: Install hyperglass-agent on your system ---- - -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; - -:::caution Deprecation Warning -[hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent) is going to be deprecated soon. See [Issue #143](https://github.com/thatmattlove/hyperglass/issues/143) and [here](platforms.mdx) for more detail. -::: - -## Installation - -### Dependencies - -On the Linux server running [FRRouting](https://frrouting.org/) or [BIRD](https://bird.network.cz/), make sure Python 3.6+ is installed: - - - - - -```shell-session -$ sudo apt install -y python3.6-dev python3-pip -``` - - - - - -```shell-session -$ sudo apt install -y python3 python3-pip libssl-dev -``` - - - - - -You can install python from the CentOS 7 repository: - -```shell-session -$ sudo yum install python3-devel python3-pip -``` - -But you can also use the [SCL repository](https://www.softwarecollections.org/en/scls/rhscl/rh-python36/) - -```shell-session -$ sudo yum install centos-release-scl -$ sudo yum install rh-python36 -``` - - - - - -You can then verify your Python 3 version: - -```shell-session -$ python3 --version -Python 3.6.9 -``` - -### Application - -Now that Python 3.6+ is installed, you can install the hyperglass agent: - -```shell-session -$ pip3 install hyperglass-agent -``` - -:::caution Deprecation Warning -[hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent) is going to be deprecated soon. See [Issue #143](https://github.com/thatmattlove/hyperglass/issues/143) and [here](platforms.mdx) for more detail. -::: diff --git a/docs/docs/agent/parameters.mdx b/docs/docs/agent/parameters.mdx deleted file mode 100644 index 8bdacfaf1..000000000 --- a/docs/docs/agent/parameters.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -id: parameters -title: Agent Configuration Parameters -sidebar_label: Parameters -keywords: [hyperglass, agent, hyperglass-agent, configuration, parameters] -description: hyperglass-agent configuration parameters. ---- - -import M from "../../src/components/MiniNote"; -import PL from "../../src/components/PageLink"; - -:::caution Deprecation Warning -[hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent) is going to be deprecated soon. See [Issue #143](https://github.com/thatmattlove/hyperglass/issues/143) and [here](platforms.mdx) for more detail. -::: - -
- -## Configuration File - -hyperglass-agent configuration consists of one [YAML](https://yaml.org/) configuration files: - -- `config.yaml` - -The configuration file may be located in one of the following directories: - -- `/etc/hyperglass-agent` -- `~/hyperglass-agent` - -## Global Settings - -| Parameter | Type | Default | Description | -| :------------------ | :-----: | :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| `debug` | Boolean | `false` | Enable application-wide debug mode. **This will generate a log of logs!** | -| `listen_address` | String | `'0.0.0.0'` | Local IPv4/IPv6 Address the hyperglass-agent application listens on to respond to hyperglass queries. | -| `listen_port` | Integer | `8080` or `8443` | Local TCP port the hyperglass-agent application listens on to respond to hyperglass queries. | -| `mode` | String | `'frr'` | FRRouting or BIRD agent mode. | -| `secret` | String | Randomly generated | Password used to sign [JWT](https://jwt.io) tokens. The secret must match the password in hyperglass. | -| `valid_duration` | Integer | `60` | Duration in seconds for which any query is valid. Used as the `exp` claim in the JWT. | -| `not_found_message` | String | `'{target} not found. ({afi})'` | Message returned if the response is empty. `{target}` will be replaced with the `query_target` and `{afi}` will be replaced with the address-family. | - -### Subsections - -From the top level, the following subsections may be defined and configured: - -| Section | Description | All Options | -| :-------- | :------------------------------------- | :-----------------------: | -| `ssl` | Redis server & cache timeout settings. | ➡️ | -| `logging` | API documentation settings. | ➡️ | - -## SSL - -| Parameter | Type | Default | Description | -| :-------- | :------: | :------------------------------ | :----------------------------------------- | -| `enable` | Boolean | `true` | Enable or disable the SSL/HTTPS. | -| `cert` | FilePath | `/agent_cert.pem` | Valid path to SSL certificate public key. | -| `key` | FilePath | `/agent_key.pem` | Valid path to SSL certificate private key. | - -While you can use your own SSL certificates, hyperglass-agent will generate self-signed certificates for you by default. They will be generated during the [setup process](setup.mdx), or you can run the `hyperglass-agent certificate` command. - -## Logging - -By default, hyperglass-agent writes all log messages to a log file located at `/tmp/hyperglass-agent.log`. This behavior and other file logging parameters may be overridden if needed: - -| Parameter | Type | Default | Description | -| :---------- | :------: | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `directory` | FilePath | `/tmp/` | Directory at which to write the log file `hyperglass-agent.log`. Use `false` to disable file logging. | -| `format` | String | `'text'` | `'text'` for plain text non-structured logging, `'json'` for JSON structured logging. If JSON is enabled, the log file name will be `hyperglass-agent.log.json` | -| `max_size` | String | `'50MB'` | Maximum log file size before old logs are overwritten. | - -:::caution Deprecation Warning -[hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent) is going to be deprecated soon. See [Issue #143](https://github.com/thatmattlove/hyperglass/issues/143) and [here](platforms.mdx) for more detail. -::: diff --git a/docs/docs/agent/setup.mdx b/docs/docs/agent/setup.mdx deleted file mode 100644 index 18db315a9..000000000 --- a/docs/docs/agent/setup.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -id: setup -title: Linux Agent Setup -sidebar_label: Setup -keywords: [configuration, linux, frr, frrouting, bird, agent] -description: Configure hyperglass-agent ---- - -import M from "../../src/components/MiniNote"; - -:::caution Deprecation Warning -[hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent) is going to be deprecated soon. See [Issue #143](https://github.com/thatmattlove/hyperglass/issues/143) and [here](platforms.mdx) for more detail. -::: - -:::important Time & NTP -Before you get too far, check to make sure your hyperglass server and hyperglass-agent system are both properly synchronized with an NTP server. During the setup process and on every interaction, hyperglass and hyperglass-agent exchange [JWT](https://jwt.io/) tokens with a relatively short window (**60 seconds, by default**) in which to validate the payload. If the system clock on either system is askew by too much, this exchange can fail. -::: - -## Setup - -To automatically create an application directory, generate SSL certificates, generate and symlink a systemd file, and generate a random secret, run: - -```shell-session -$ hyperglass-agent setup -``` - -During the setup process, you'll be prompted to: - -- Select an installation directory.Must be /etc/hyperglass-agent or ~/hyperglass-agent -- Update hyperglass's `devices.yaml` file with a generated secret -- Verify the hostname of the device. The hostname you verify is used to generate the agent's SSL certificate, and needs to be DNS-resolvable by hyperglass. -- Select IP addresses from the device's interfaces that hyperglass would use to communicate with the agent. The IP addresses are added as Subject Alternative Names to the generated SSL certificate, so that you can point hyperglass to the agent by IP address if needed, but still leverage SSL validation. -- Enter the URL of your hyperglass. hyperglass-agent will send the public key of your SSL certificate to hyperglass, so that future communication between hyperglass and hyperglass-agent is authenticated and encrypted. - -After these prompts, the agent's SSL public & private keys and a configuration file will be generated and saved to your installation directory. A systemd service file will also be generated, saved to your installation directory, and symlinked to `/etc/systemd/system/hyperglass-agent.service`. - -:::note -You can also run the setup wizard with certain options disabled, if needed: - -```shell-session -$ hyperglass-agent setup --help -Usage: hyperglass-agent setup [OPTIONS] - - Run the setup wizard - -Options: - --no-config Don't regenerate config file - --no-certs Don't regenerate certificates - --no-systemd Don't generate a systemd file - --no-send Don't send the SSL certificate to hyperglass - --force Force regeneration of config file - -h, --help Show this message and exit. -``` - -::: - -:::important More coming soon -Documentation for [hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent) is in progress! -::: diff --git a/docs/docs/commands.mdx b/docs/docs/commands.mdx deleted file mode 100644 index e66e287cf..000000000 --- a/docs/docs/commands.mdx +++ /dev/null @@ -1,132 +0,0 @@ ---- -id: commands -title: Command Customization -sidebar_label: Command Customization -keywords: - [ - hyperglass, - commands, - customize, - cisco, - ios, - ios-xe, - ios-xr, - juniper, - junos, - arista, - eos, - huawei, - vrp, - ] -description: Custom Commands ---- - -hyperglass comes with built in support for the following platforms: - -- Cisco IOS & IOS-XE -- Cisco IOS-XR -- Juniper JunOS -- Arista EOS -- Huawei VRP -- VyOS - -:::caution VyOS & VRFs -As of `vyos-1.3-rolling-202007050117` which is the latest release VyOS has been tested with hyperglass, VyOS does not support BGP or other dynamic routing protocols in a VRF. As such, the default BGP commands for VyOS **omit the VRF from the command**. -::: - -Default commands for each of these network operating systems are built into hyperglass. However, you may override any of them or even add commands for another Network Operating System (NOS), as long as it's [supported](platforms). - -To define custom commands, add a `commands.yaml` file to your installation directory (`/etc/hyperglass`, -`~/hyperglass`). - -Each command definition carries the following structure: - -```yaml title="commands.yaml" -command_name: - ipv4_default: - bgp_route: ... - bgp_aspath: ... - bgp_community: ... - ping: ... - traceroute: ... - ipv6_default: - bgp_route: ... - bgp_aspath: ... - bgp_community: ... - ping: ... - traceroute: ... - ipv4_vpn: - bgp_route: ... - bgp_aspath: ... - bgp_community: ... - ping: ... - traceroute: ... - ipv6_vpn: - bgp_route: ... - bgp_aspath: ... - bgp_community: ... - ping: ... - traceroute: ... -``` - -`ipv4_default` and `ipv6_default` reference the commands used in the default routing table/VRF, while `ipv4_vpn` and `ipv6_vpn` reference the commands used in **any** configured VRF. Every command will have the following keywords replaced: - -| Keyword | Description | -| :--------- | :--------------------------------------------------------------- | -| `{target}` | Query Target (IP address, community, AS Path). | -| `{vrf}` | If it's a VRF query, the [name of the VRF](adding-devices#vrfs). | - -## Overriding Built-In Commands - -As an example, you could override the default Juniper `bgp_route` command for the default routing table like this: - -```yaml {4} title="commands.yaml" ---- -juniper: - ipv4_default: - bgp_route: "show route protocol bgp {target} terse" -``` - -If the NOS key (`juniper`, in this case) matches the [supported platform key](platforms), only the command you specify will be overridden. - -## Adding a Custom Command Set - -You can define any arbitrary set of commands to use for any supported device. When defining a custom command, **you must define _all_ commands**, even if they're disabled in your [configuration](query-settings) or otherwise unused. - -```yaml title="commands.yaml" ---- -special_commands: - ipv4_default: - bgp_route: "show ip route {target}" - bgp_aspath: "show ip bgp as-path {target}" - bgp_community: "show ip bgp community {target}" - ping: "ping {target}" - traceroute: "traceroute {target}" - ipv6_default: - bgp_route: "show ipv6 route {target}" - bgp_aspath: "show ipv6 bgp as-path {target}" - bgp_community: "show ipv6 bgp community {target}" - ping: "ping6 {target}" - traceroute: "traceroute6 {target}" - ipv4_vpn: - bgp_route: "show ip route {target} vrf {vrf}" - bgp_aspath: "show ip bgp as-path {target} vrf {vrf}" - bgp_community: "show ip bgp community {target} vrf {vrf}" - ping: "ping {target} vrf {vrf}" - traceroute: "traceroute {target} vrf {vrf}" - ipv6_vpn: - bgp_route: "show ipv6 route {target} vrf {vrf}" - bgp_aspath: "show ipv6 bgp as-path {target} vrf {vrf}" - bgp_community: "show ipv6 bgp community {target} vrf {vrf}" - ping: "ping6 {target} vrf {vrf}" - traceroute: "traceroute6 {target} vrf {vrf}" -``` - -After adding the custom command to `commands.yaml`, reference its name under the device's `commands:` key in `devices.yaml`: - -```yaml {4} title="devices.yaml" ---- -routers: - - name: specialrouter01 - commands: special_commands -``` diff --git a/docs/docs/getting-started.mdx b/docs/docs/getting-started.mdx deleted file mode 100755 index a598773a3..000000000 --- a/docs/docs/getting-started.mdx +++ /dev/null @@ -1,139 +0,0 @@ ---- -id: getting-started -title: Getting Started -sidebar_label: Getting Started -keywords: [install, configuration] -description: Getting started with hyperglass ---- - -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; - -## Automatic installation - -If your system runs on: - -- Ubuntu Linux -- ~~CentOS/Red Had Linux~~ -- macOS (requires [homebrew](https://brew.sh)) - -You should be able to proceed with the automatic installation: - -```shell-session -$ curl https://install.hyperglass.dev | sudo bash -``` - -:::caution Piping to bash -You should be very worried when someone asks you to do what I just did. Downloading a bash script from the internet and piping it to `bash` with root privileges is a terrible idea, unless you fully trust the source. Please don't trust me - go [look at the code](https://github.com/thatmattlove/hyperglass/blob/v1.0.0/install.sh) and determine for your self if it's safe to execute. If you feel it's not, please proceed with the manual installation (and [tell me why](https://github.com/thatmattlove/hyperglass/issues), so I can fix it). -::: - -## Manual Installation - -### System Dependencies - -#### Python - -hyperglass is written in Python 3 and requires Python version 3.6 as a minimum dependency. - -If you're confident upgrading your system's version of Python won't break your system (many Linux operating systems rely heavily on Python for package management and other system functions), you can install Python 3.6: - - - - - -```shell-session -$ sudo apt install -y python3-dev python3-pip -``` - - - - - -Documentation for CentOS is still in-progress. However, it's been determined that these dependencies _may_ also be required, depending on the version of CentOS: - -```shell-session -$ sudo yum install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel \ - libxcb-devel ncurses-devel -``` - -Until further testing with CentOS can be done, you should install the above dependencies _before_ installing Python 3. You may have to uninstall and re-install Python 3 if these dependencies weren't already installed. - -You can install python from the CentOS 7 repository: - -```shell-session -$ sudo yum install python3-devel python3-pip -``` - -But you can also use the [SCL repository](https://www.softwarecollections.org/en/scls/rhscl/rh-python36/) - -```shell-session -$ sudo yum install centos-release-scl -$ sudo yum install rh-python36 -``` - - - - - -You can then verify your Python 3 version: - -```shell-session -$ python3 --version -Python 3.6.9 -``` - -#### Other Dependencies - -The hyperglass UI is written in [ReactJS](https://reactjs.org/). As such, some Javascript dependencies are required. hyperglass also relies on [Redis](https://redis.io/) for caching purposes. - - - - - -```shell-session -$ curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - - -$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - -$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - -$ sudo apt update -$ sudo apt install -y nodejs yarn redis-server - -$ sudo systemctl enable redis-server -$ sudo systemctl restart redis-server -``` - - - - - -```shell-session -$ curl -sL https://rpm.nodesource.com/setup_14.x | sudo bash - -$ curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo -$ sudo rpm --import https://dl.yarnpkg.com/rpm/pubkey.gpg - -$ sudo yum -y install gcc-c++ make nodejs yarn redis - -$ sudo systemctl enable redis -$ sudo systemctl restart redis -``` - - - - - -### Application - -```shell-session -$ pip3 install hyperglass -``` diff --git a/docs/docs/introduction.mdx b/docs/docs/introduction.mdx deleted file mode 100644 index c3a94085d..000000000 --- a/docs/docs/introduction.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -id: introduction -title: Introduction -sidebar_label: Introduction -description: Welcome to hyperglass ---- - -import Link from "@docusaurus/Link"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import clsx from "clsx"; -import styles from "./styles.module.css"; - -## What is hyperglass? - -**hyperglass** is an open source network looking glass written by a network engineer for other network engineers. The purpose of a looking glass is to provide customers, peers, and complete strangers with unattended visibility into the an operator's network. - -hyperglass was created with the lofty goal of benefiting the internet community at-large by providing a faster, easier, and more secure way for operators to provide looking glass services to their customers, peers, and other network operators. - -## Features - -- BGP Route, BGP Community, BGP AS Path, Ping, & Traceroute -- Full IPv6 support -- Customizable everything: features, theme, UI/API text, error messages, commands -- Built in support for: - - Arista EOS - - BIRD - - Cisco IOS-XR - - Cisco IOS/IOS-XE - - Cisco NX-OS - - FRRouting - - Huawei - - Juniper JunOS - - Mikrotik - - Nokia SR OS - - TNSR - - VyOS -- Configurable support for any other [supported platform](platforms.mdx) -- Optionally access devices via an SSH proxy/jump server -- VRF support -- Access List/prefix-list style query control to whitelist or blacklist query targets on a per-VRF basis -- REST API with automatic, configurable OpenAPI documentation -- Modern, responsive UI built on [ReactJS](https://reactjs.org/), with [NextJS](https://nextjs.org/) & [Chakra UI](https://chakra-ui.com/) -- Query multiple devices simultaneously -- Browser-based DNS-over-HTTPS resolution of FQDN queries - - - Get Started - diff --git a/docs/docs/license.mdx b/docs/docs/license.mdx deleted file mode 100644 index fee0996ea..000000000 --- a/docs/docs/license.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -id: license -title: License -sidebar_label: License -keywords: [hyperglass, license, open source] -description: hyperglass License ---- - -import Datetime from "../src/components/Datetime"; -import styles from "./styles.module.css"; - -### The Clear BSD License - -Copyright © Matthew Love - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met: - -- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -- Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/docs/logging.mdx b/docs/docs/logging.mdx deleted file mode 100644 index 61e8e7117..000000000 --- a/docs/docs/logging.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -id: logging -title: Logging & Webhooks -sidebar_label: Logging & Webhooks -keywords: [hyperglass, logging, webhook, hooks, syslog] -description: hyperglass Logging ---- - -hyperglass supports multiple types of logging, for both application troubleshooting and general reporting. - -
- -## File Logging - -By default, hyperglass writes all log messages to a log file located at `/tmp/hyperglass.log`. This behavior and other file logging parameters may be overridden if needed: - -| Parameter | Type | Default | Description | -| :---------- | :----: | :------- | :------------------------------------------------- | -| `directory` | String | `'/tmp'` | Valid directory where the log file can be written. | -| `format` | String | `'text'` | Log format - may be either `'text'` or `'json'`. | -| `max_size` | String | `'50MB'` | Maximum log file size before logs are rotated. | - -The `logging` subsection contains additional subsections of its own for configuring other logging methods: - -| Section | Description | All Options | -| :------- | :-------------------- | :------------------------------------: | -| `syslog` | Syslog settings | ➡️ | -| `http` | HTTP webhook settings | ➡️ | - -:::note -You do not have to set `enable: false` to disable syslogging or webhooks - if there is no configuration under the `syslog` or `http` subsections, it the option is disabled by default. The `enable` option exists for easy toggling without having to delete the other settings. -::: - -## Syslog - -If syslogging is enabled, all of the same log messages written to the file and/or stdout will be forwarded to the syslog server. - -| Parameter | Type | Default | Description | -| :-------- | :-----: | :------ | :------------------------------------------------ | -| `enable` | Boolean | `true` | Optionally disable syslogging even if configured. | -| `host` | String | | Syslog target IP address or hostname. | -| `port` | Integer | `514` | Syslog target UDP port number. | - -## Webhooks - -If http logging is enabled, an HTTP POST will be sent to the configured target every time a query is submitted, _after_ it is validated. - -| Parameter | Type | Default | Description | -| :----------- | :-----: | :---------- | :-------------------------------------------------------------------------------------------------------- | -| `enable` | Boolean | `true` | Optionally disable webhooks even if configured. | -| `host` | String | | HTTP URL to webhook target. | -| `headers` | Mapping | | Any arbitrary mappings, which will be sent as HTTP headers. | -| `params` | Mapping | | Any arbitrary mappings, which will be sent as URL parameters (e.g. `http://example.com/log?param=value`). | -| `verify_ssl` | Boolean | `true` | Verify SSL certificate of target. | -| `timeout` | Integer | `5` | Time in seconds before request times out. | -| `provider` | String | `'generic'` | Webhook provider. | - -### Supported Providers - -| Provider | Parameter Value | -| :--------------------------------------------------------------------------------------------------- | --------------: | -| Generic | `'generic'` | -| [Microsoft Teams](https://www.microsoft.com/en-us/microsoft-365/microsoft-teams/group-chat-software) | `'msteams'` | -| [Slack](https://slack.com/) | `'slack'` | - -### Authentication - -Basic and API key authentication are supported. - -| Parameter | Type | Default | Description | -| :--------- | :----: | :-------- | :------------------------------------------------------------------------ | -| `mode` | String | `'basic'` | Authentication mode. Must be `'basic'` or `'api_key'` | -| `username` | String | | Username for basic authentication. | -| `password` | String | | Password for basic authentication, or API Key for API key authentication. | - -:::important -If `api_key` is used, the header `X-API-Key: {key}` is added to the request, where `{key}` is the password. -::: - -### Webhook Data Structure - -If the `provider` field is set to `'generic'`, the webhook will POST JSON data in the following format: - -```json -{ - "query_location": "router01", - "query_type": "bgp_route", - "query_vrf": "default", - "query_target": "1.1.1.0/24", - "headers": { - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36", - "referer": "http://lg.example.com/", - "accept-encoding": "gzip, deflate, br", - "accept-language": "en-US,en;q=0.9,fr;q=0.8,lb;q=0.7,la;q=0.6" - }, - "source": "192.0.2.1", - "network": { - "prefix": "192.0.2.0/24", - "asns": ["64496"] - } -} -``` - -## Full example - -```yaml title="hyperglass.yaml" -logging: - directory: /var/log - format: json - max_size: 10 MB - syslog: - host: syslog.example.com - http: - host: "https://example.com/logs" - authentication: - mode: basic - username: your username - password: your password - headers: - X-Special-Header: your header value - params: - app: hyperglass # URL would be https://example.com/logs?app=hyperglass - verify_ssl: false -``` diff --git a/docs/docs/messages.mdx b/docs/docs/messages.mdx deleted file mode 100644 index d2c6aa9be..000000000 --- a/docs/docs/messages.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -id: messages -title: Messages -sidebar_label: Messages -keywords: [configuration, messages, text, ui, customization] -description: hyperglass messages configuration ---- - -All user-facing status messages can be customized if needed. - -
- -| Parameter | Type | Default | Description | -| :--------------------- | :----: | :-------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `acl_denied` | String | `'"{target} is a member of {denied_network}, which is not allowed."'` | Displayed when a query target is explicitly denied by a matched VRF's ACL entry. `{target}` and `{denied_network}` will be replaced with the denied query target and the ACL entry that caused it to be denied. | -| `acl_not_allowed` | String | `'{target} is not allowed.'` | Displayed when a query target is implicitly denied by a matched VRF's ACL. `{target}` will be replaced with the denied query target. | -| `authentication_error` | String | `'Authentication error occurred.'` | Displayed when hyperglass is unable to authenticate to a configured device. Usually, this indicates a configuration error. `{device_name}` and `{error}` will be replaced with the device in question and the specific connection error. | -| `connection_error` | String | `'Error connecting to {device_name}: {error}'` | Displayed when hyperglass is unable to connect to a configured device. Usually, this indicates a configuration error. `{device_name}` and `{error}` will be replaced with the device in question and the specific connection error. | -| `feature_not_enabled` | String | `'{feature} is not enabled for {device_name}.'` | Displayed when a query type is submitted that is not supported or disabled. The UI performs validation of supported query types prior to submitting any requests, so this is primarily relevant to the hyperglass API. `{feature}` and `{device_name}` will be replaced with the disabled feature and the selected device/location. | -| `general` | String | `'Something went wrong.'` | Displayed when generalized errors occur. Seeing this error message may indicate a bug in hyperglass, as most other errors produced are highly contextual. If you see this in the wild, try enabling [`debug`](parameters.mdx#global-settings) mode and review the logs to pinpoint the source of the error. | -| `invalid_field` | String | `'{input} is an invalid {field}.'` | Displayed when a query field contains an invalid or unsupported value. `{input}` and `{field}` will be replaced with the invalid input value and corresponding field name. | -| `invalid_input` | String | `'{target} is not a valid {query_type} target.'` | Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` and `{query_type}` will be replaced with the invalid target and corresponding query type. | -| `no_input` | String | `'{field} must be specified.'` | Displayed when no a required field is not specified. `{field}` will be replaced with the `display_name` of the field that was omitted. | -| `no_output` | String | `'The query completed, but no matching results were found.'` | Displayed when hyperglass can connect to a device and execute a query, but the response is empty. | -| `no_response` | String | `'No response.'` | Displayed when hyperglass can connect to a device, but no output is able to be read. Seeing this error may indicate a bug in hyperglass or one of its dependencies. If you see this in the wild, try enabling [`debug`](parameters.mdx#global-settings) mode and review the logs to pinpoint the source of the error. | -| `parsing_error` | String | `'An error occurred while parsing the query output.'` | Displayed when hyperglass can connect to a device and execute a query, but the response cannot be parsed. | -| `request_timeout` | String | `'Request timed out.'` | Displayed when the [`request_timeout`](parameters.mdx#global-settings) time expires. | -| `vrf_not_associated` | String | `'VRF {vrf_name} is not associated with {device_name}.'` | Displayed when a query request's VRF field value contains a VRF that is not configured or associated with the corresponding location/device. The UI automatically filters out VRFs that are not configured on a selected device, so this error is most likely to appear when using the hyperglass API. `{vrf_name}` and `{device_name}` will be replaced with the VRF in question and corresponding device. | -| `vrf_not_found` | String | `'VRF {vrf_name} is not defined.'` | Displayed when a query VRF is not configured on any devices. The UI only shows configured VRFs, so this error is most likely to appear when using the hyperglass API. `{vrf_name}` will be replaced with the VRF in question. | - -## Example - -```yaml title="hyperglass.yaml" -messages: - acl_denied: "{target} is a member of {denied_network}, which is not allowed." - acl_not_allowed: "{target} is not allowed." - authentication_error: Authentication error occurred. - connection_error: "Error connecting to {device_name}: {error}" - feature_not_enabled: "{feature} is not enabled for {device_name}." - general: Something went wrong. - invalid_field: "{input} is an invalid {field}." - invalid_input: "{target} is not a valid {query_type} target." - no_input: "{field} must be specified." - no_output: The query completed, but no matching results were found. - no_response: No response. - parsing_error: An error occurred while parsing the query output. - request_timeout: Request timed out. - vrf_not_associated: VRF {vrf_name} is not associated with {device_name}. - vrf_not_found: VRF {vrf_name} is not defined. -``` diff --git a/docs/docs/parameters.mdx b/docs/docs/parameters.mdx deleted file mode 100644 index 834e555eb..000000000 --- a/docs/docs/parameters.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -id: parameters -title: Configuration Parameters -sidebar_label: Parameters -keywords: [hyperglass, configuration, parameters] -description: hyperglass configuration parameters. ---- - -import MiniNote from "../src/components/MiniNote"; -import PageLink from "../src/components/PageLink"; - -hyperglass is meant to be _extremely_ customizable, but with reasonable defaults that most operators won't need to override. Even though there are a _lot_ of configuration options, all of them can be left untouched and hyperglass will work (although, it's recommended to at least set the organization name). - -:::tip Validation -On the back end, hyperglass uses strict configuration schema validation, so it's virtually impossible to configure hyperglass incorrectly without receiving a contextual warning about what's missing or incorrect about a configuration. -::: - -## Configuration Files - -hyperglass configuration consists of three separate [YAML](https://yaml.org/) configuration files: - -- `hyperglass.yaml` - - Defines configuration parameters for the application and UI - This file is not required for hyperglass to run -- `devices.yaml` - - Defines devices (routers), SSH proxy servers, and credentials - - This file is required for hyperglass to run - -- `commands.yaml` - - Defines commands for custom network operating systems, or overrides default hyperglass command sets. - This file is not required for hyperglass to run - -Configuration files may be located in one of the following directories: - -- `/etc/hyperglass` -- `~/hyperglass` - -## Global Settings - -The following global settings can be set in `hyperglass.yaml`: - -| Parameter | Type | Default | Description | -| :--------------------- | :--------------: | :----------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `debug` | Boolean | `false` | Enable application-wide debug mode. **This will generate a lot of logs!** | -| `developer_mode` | Boolean | `false` | If enabled, the hyperglass backend (Python) and frontend (React/Javascript) applications are "unlinked", so that React tools can be used for front end development. A `` convenience component is also displayed in the UI for easier UI development.' | -| `primary_asn` | String | `'65000'` | Your network's primary ASN. This field is used to set some useful defaults such as the subtitle and PeeringDB URL. | -| `org_name` | String | `'Beloved Hyperglass User'` | Your organization's name. This field is used in the UI & API documentation to set fields such as `` HTML tags for SEO and the terms & conditions footer component. | -| `site_title` | String | `'hyperglass'` | The name of your hyperglass site. This field is used in the UI & API documentation to set fields such as the `` HTML tag, and the terms & conditions footer component. | -| `site_description` | String | `'{org_name} Network Looking Glass'` | A short description of your hyperglass site. This field is used in th UI & API documentation to set the `<meta name="description"/>` tag. `{org_name}` may be used to insert the value of the `org_name` field. | -| `site_keywords` | List | | Keywords pertaining to your hyperglass site. This field is used to generate `<meta name="keywords"/>` HTML tags, which helps tremendously with [SEO](https://support.google.com/webmasters/answer/7451184). | -| `request_timeout` | Integer | `90` | Global timeout in seconds for all requests. The UI uses this field's exact value when submitting queries. The backend uses this field's value, minus one second, for its own timeout handling. This is to ensure a contextual timeout error is presented to the end user in the event of a backend application timeout. | -| `listen_address` | String | `'localhost'` | Local IPv4/IPv6 Address the hyperglass application listens on to serve web traffic. | -| `listen_port` | Integer | `8001` | Local TCP port the hyperglass application listens on to serve web traffic. | -| `cors_origins` | List | `[]` | Allowed [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) hosts. By default, no CORS hosts are allowed. | -| `netmiko_delay_factor` | Integer \| Float | `0.1` | Override the [Netmiko global delay factor](https://ktbyers.github.io/netmiko/docs/netmiko/index.html). | -| `google_analytics` | String | | Google Analytics Tracking ID | - -:::note -The `netmiko_delay_factor` parameter should only be used if you're experiencing strange SSH connection issues. By default, Netmiko uses a `global_delay_factor` of `1`, which tends to be a bit slow for running a simple show command. hyperglass overrides this to `0.1` by default, but you can override this to whatever value suits your environment if needed. -::: - -### Subsections - -From the top level, the following subsections may be defined and configured: - -| Section | Description | All Options | -| :----------- | :-------------------------------------------------- | :-------------------------------------------: | -| `cache` | Redis server & cache timeout settings. | <PageLink to="response-caching">➡️</PageLink> | -| `docs` | API documentation settings. | <PageLink to="rest-api">➡️</PageLink> | -| `logging` | File, syslog, and webhook settings. | <PageLink to="logging">➡️</PageLink> | -| `messages` | Customize almost all user-facing UI & API messages. | <PageLink to="messages">➡️</PageLink> | -| `queries` | Enable, disable, or configure query types. | <PageLink to="query-settings">➡️</PageLink> | -| `structured` | Configure structured data features. | <PageLink to="table-output">➡️</PageLink> | -| `web` | Web UI & branding settings. | <PageLink to="ui/configuration">➡️</PageLink> | - -### Example - -```yaml title="hyperglass.yaml" -debug: false -developer_mode: false -org_name: Beloved Hyperglass User -site_title: hyperglass -site_description: "{org_name} Network Looking Glass" -site_keywords: [hyperglass, looking glass, routing, bgp] -request_timeout: 30 -listen_address: "127.0.0.1" -listen_port: 8001 -log_file: /tmp/hyperglass.log -cors_origins: [lg.example.com, 192.0.2.1] -``` diff --git a/docs/docs/platforms.mdx b/docs/docs/platforms.mdx deleted file mode 100644 index 8038eeeb2..000000000 --- a/docs/docs/platforms.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -id: platforms -title: Supported Platforms -sidebar_label: Supported Platforms -description: Platforms supported by hyperglass ---- - -import Native from "../src/components/Native"; - -<div class="table--full-width" /> - -## SSH - -The following platforms use [Netmiko](https://github.com/ktbyers/netmiko) or [Scrapli](https://github.com/carlmontanari/scrapli) for connection handling. When configuring the `nos` property of a device, use the value in the **Key** column. - -Platforms in <Native>green</Native> are natively supported, which means hyperglass has predefined commands built in. Other platforms should also work, you'll just need to [add a custom command profile](commands.mdx). - -If needed, for troubleshooting purposes or otherwise, the driver for a given device [can be overridden](adding-devices#all-device-paramters) with the `driver` parameter. Generally, Netmiko is more stable, supports more platforms, but is slower; while Scrapli can be less reliable, supports fewer platforms, but is significantly faster. - -| Name | Key | Default Driver | -| :--------------------------------- | :-------------------- | :------------- | -| A10 | `a10` | Netmiko | -| Accedian | `accedian` | Netmiko | -| Alcatel AOS | `alcatel_aos` | Netmiko | -| Alcatel SROS | `alcatel_sros` | Netmiko | -| Apresia Systems AEOS | `apresia_aeos` | Netmiko | -| <Native>Arista vEOS</Native> | `arista_eos` | Netmiko | -| Aruba | `aruba_os` | Netmiko | -| Avaya ERS | `avaya_ers` | Netmiko | -| Avaya VSP | `avaya_vsp` | Netmiko | -| <Native>BIRD</Native> | `bird` | Scrapli | -| Calix B6 | `calix_b6` | Netmiko | -| Check Point GAiA | `checkpoint_gaia` | Netmiko | -| Ciena SAOS | `ciena_saos` | Netmiko | -| Cisco ASA | `cisco_asa` | Netmiko | -| <Native>Cisco IOS</Native> | `cisco_ios` | Scrapli | -| <Native>Cisco NX-OS</Native> | `cisco_nxos` | Scrapli | -| Cisco SG-300 | `cisco_s300` | Netmiko | -| <Native>Cisco IOS-XE</Native> | `cisco_xe` | Scrapli | -| <Native>Cisco IOS-XR</Native> | `cisco_xr` | Scrapli | -| Citrix Netscaler | `netscaler` | Netmiko | -| CloudGenix ION | `cloudgenix_ion` | Netmiko | -| Coriant | `coriant` | Netmiko | -| Dell OS6 | `dell_os6` | Netmiko | -| Dell OS9 | `dell_os9` | Netmiko | -| Dell OS10 | `dell_os10` | Netmiko | -| Dell PowerConnect | `dell_powerconnect` | Netmiko | -| Endace | `endace` | Netmiko | -| Eltex | `eltex` | Netmiko | -| Eltex ESR | `eltex_esr` | Netmiko | -| Enterasys | `enterasys` | Netmiko | -| Extreme | `extreme` | Netmiko | -| Extreme ERS | `extreme_ers` | Netmiko | -| Extreme EXOS | `extreme_exos` | Netmiko | -| Extreme/Brocade NetIron | `extreme_netiron` | Netmiko | -| Extreme NOS | `extreme_nos` | Netmiko | -| Extreme/Brocade SLX | `extreme_slx` | Netmiko | -| Extreme/Brocade VDX | `extreme_vdx` | Netmiko | -| Extreme VSP | `extreme_vsp` | Netmiko | -| Extreme Wing | `extreme_wing` | Netmiko | -| F5 LTM | `f5_ltm` | Netmiko | -| F5 TMSH | `f5_tmsh` | Netmiko | -| F5 Linux | `f5_linux` | Netmiko | -| Flex VNF | `flexvnf` | Netmiko | -| Fortinet | `fortinet` | Netmiko | -| <Native>FRRouting</Native> | `frr` | Scrapli | -| Generic Terminal Server | `generic_termserver` | Netmiko | -| HPE/3COM Comware | `hp_comware` | Netmiko | -| HPE ProCurve | `hp_procurve` | Netmiko | -| <Native>Huawei</Native> | `huawei` | Netmiko | -| Huawei VRPv8 | `huawei_vrpv8` | Netmiko | -| IPInfusion OcNOS | `ipinfusion_ocnos` | Netmiko | -| <Native>Juniper</Native> | `juniper` | Scrapli | -| <Native>Juniper JunOS</Native> | `juniper_junos` | Scrapli | -| Juniper ScreenOS | `juniper_screenos` | Netmiko | -| Keymile | `keymile` | Netmiko | -| Keymile NOS | `keymile_nos` | Netmiko | -| Linux | `linux` | Netmiko | -| <Native>Mikrotik RouterOS</Native> | `mikrotik_routeros` | Netmiko | -| <Native>Mikrotik SwitchOS</Native> | `mikrotik_switchos` | Netmiko | -| Mellanox | `mellanox` | Netmiko | -| Mellanox MLNX-OS | `mellanox_mlnxos` | Netmiko | -| MRV LX | `mrv_lx` | Netmiko | -| MRV Optiswitch | `mrv_optiswitch` | Netmiko | -| <Native>Nokia SROS</Native> | `nokia_sros` | Netmiko | -| OneAccess OneOS | `oneaccess_oneos` | Netmiko | -| OVS Linux | `ovs_linux` | Netmiko | -| Palo Alto Networks PAN-OS | `paloalto_panos` | Netmiko | -| Pluribus | `pluribus` | Netmiko | -| Quanta Mesh | `quanta_mesh` | Netmiko | -| RAD ETX | `rad_etx` | Netmiko | -| Ruckus/Brocade FastIron | `ruckus_fastiron` | Netmiko | -| Ruijie OS | `ruijie_os` | Netmiko | -| <Native>TNSR</Native> | `tnsr` | Scrapli | -| Ubuiquiti EdgeRouter | `ubiquiti_edge` | Netmiko | -| Ubuiquiti EdgeSwitch | `ubiquiti_edgeswitch` | Netmiko | -| Vyatta VyOS | `vyatta_vyos` | Netmiko | -| <Native>VyOS</Native> | `vyos` | Netmiko | - -## HTTP - -The following platforms use [hyperglass-agent](agent/installation.mdx) for connection handling. When configuring the `nos` property of a device, use the value in the **Key** column. - -:::caution hyperglass agent is being deprecated -While hyperglass-agent was a cool idea, maintaining the two codebases and protocol sets has proven cumbersome, with very little benefit. As of **v1.0.0**, `frr_legacy` and `bird_legacy` are temporarily available to mimic previous behavior with hyperglass-agent. -::: - -| Name | Key | -| :-------- | :------------ | -| BIRD | `bird_legacy` | -| FRRouting | `frr_legacy` | - -## Caveats - -### BIRD - -If using BIRD, more specifically [`bird_ssh`](platforms.mdx), you'll more than likely need to provide hyperglass with credentials that have root privileges. This is because, to my knowledge, the socket `birdc` uses is created by root. Therefore, if hyperglass runs the `birdc` commands as an unprivileged user, they will fail with a permissions error. - -### FRRouting - -The user account you provide hyperglass in the `credential` field needs to be a member of the `frrvty` group. See [the FRR docs](http://docs.frrouting.org/en/latest/vtysh.html) for more detail. - -### VyOS - -As of `vyos-1.3-rolling-202007050117` which is the latest release VyOS has been tested with hyperglass, VyOS does not support BGP or other dynamic routing protocols in a VRF. As such, the default BGP commands for VyOS **omit the VRF from the command**. diff --git a/docs/docs/production.mdx b/docs/docs/production.mdx deleted file mode 100644 index bf65c327e..000000000 --- a/docs/docs/production.mdx +++ /dev/null @@ -1,166 +0,0 @@ ---- -id: production -title: Production -sidebar_label: Production -description: Running hyperglass in production ---- - -## System Requirements - -### CPU - -In order to function properly in a production environment, hyperglass leverages [Gunicorn](https://gunicorn.org/) as an application-layer HTTP server. You don't really need to know anything about Gunicorn to use hyperglass, but there is one important factor: each Gunicorn "worker" (a process, or thread, in essence) directly maps to the number of CPU cores on your hyperglass system. Per the [Gunicorn docs](https://docs.gunicorn.org/en/latest/settings.html#workers), hyperglass uses the conservative value of 2x workers per CPU core. - -To determine the number of CPU cores on the system, Python's [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) library, and the number of cores returned _does_ factor in hyperthreading. For example, if your system has 4 cores provisioned, and the processors support hyperthreading, hyperglass will see this as 8 cores, and will provision 2 workers per core, for a final result of 16 workers. - -#### Why does this matter? - -While hyperglass is, to the extent possible, fully [asynchronous](https://docs.python.org/3/library/asyncio.html) (which means tasks may be run while waiting on other tasks to complete), this asynchronism is currently only applicable to **each request**. This means that with a single worker process, while one request is being processed, a second request must wait until the first request completes. If the first request is long-running for whatever reason, the second request may time out (this also applies to running multiple queries at the same time, in the same session). - -To combat this, hyperglass uses the above worker strategy. **Ultimately, it's important to provision the appropriate number of CPU cores, corresponding to the number of concurrent sessions you might expect to have in your environment** (keeping in mind that if your system supports hyperthreading, each core equates to two workers). - -:::note -When [debug](parameters.mdx#global-settings) is set to `true`, the number of workers is set to 1. -::: - -### Memory - -Testing shows that hyperglass is extremely memory efficient at runtime. For example, running 4 simulations BGP Route queries, with two devices utilizing [hyperglass-agent](https://github.com/thatmattlove/hyperglass-agent), and two devices utilizing SSH, the server increased RAM utilization by about 20MB during execution, and went back down afterwards. - -However, at build time, there are some fairly memory-intensive tasks which _will_ time out or cause strange errors without the proper amount of RAM. Testing suggests **2GB of RAM is sufficient**, however **4GB is the ideal minimum amount of RAM**. - -### Storage - -At **build**, hyperglass consumes approximately **196 MB** of storage. 194 MB of this is front-end dependencies, which are downloaded and installed when running a UI build. The other 2 MB is the hyperglass code itself. Once again, the minimum system requirements for most Linux distributions should be sufficient. - -## systemd - -More than likely, you'll want to run hyperglass as a background system service. [systemd](https://systemd.io/) is one of the most common ways of running services on Linux. To run hyperglass as a systemd service, create a file named `hyperglass.service` in your [installation directory](setup.mdx#installation-directory) and add following to it: - -```ini -[Unit] -Description=hyperglass -After=network.target -Requires=redis-server -# Replace the above with Requires=redis for CentOS - -[Service] -User=<user or root> -Group=<user or root> -ExecStart=<path to hyperglass> start - -[Install] -WantedBy=multi-user.target -``` - -Replace `<user or root>` with whichever user you're running hyperglass as. For example, if you're running hyperglass as a non-root user, you probably used `pip3 install hyperglass` (without `sudo`) to install hyperglass, and you're probably using `~/hyperglass` as your installation directory. However, if you're running hyperglass as root, you probably used `sudo pip3 install hyperglass` to install hyperglass, and you're probably using `/etc/hyperglass` as your installation directory. - -Systemd requires an absolute path for executables. This means you can't just put `hyperglass start` in the `ExecStart` field, it needs to be the full path. The easiest way to get this is to run `which hyperglass`, which will output the full path. It should look something like `/home/username/.local/bin/hyperglass` or `/usr/local/bin/hyperglass`. - -After adding the file, run the following: - -```bash -# Replace <systemd file> with the path to the systemd file you just created. -sudo ln -s <systemd file> /etc/systemd/system/hyperglass.service - -# Tell systemd to re-look at its service files, since you just added one. -sudo systemctl daemon-reload - -# Tell systemd to run hyperglass on system startup. -sudo systemctl enable hyperglass - -# Start the hyperglass service. -sudo systemctl start hyperglass - -# Check the status of the hyperglass service. -sudo systemctl status hyperglass -``` - -:::important Checking the status -The first time hyperglass starts up, it will run through a UI build process, which will take a little time. You may have to wait a couple of minutes in between each check on hyperglass's status. -::: - -## Reverse Proxy - -You'll want to run hyperglass behind a reverse proxy in production to serve the static files more efficiently and offload SSL. Any reverse proxy should work, but hyperglass has been specifically tested with [Caddy](https://caddyserver.com/) and [NGINX](https://www.nginx.com/). Sample configs for both can be found below. - -### Caddy - -The following file can be placed anywhere, and referenced at runtime with `sudo caddy run -config <file name>`. The highlighted lines should be replaced with your installation's specific variables. - -```text {1,2,4,5,8,11} title="Caddy" -lg.example.com:443 { - tls person@example.com - file_server { - root /etc/hyperglass/static/ui - index /etc/hyperglass/static/ui/index.html - } - file_server /custom { - root /etc/hyperglass/static/custom - } - file_server /images { - root /etc/hyperglass/static/images - } - reverse_proxy localhost:8001 -} -``` - -:::tip -The `tls` directive will tell Caddy to automatically use Let's Encrypt to generate SSL certificates for hyperglass. -::: - -### NGINX - -The following file can be placed at `/etc/nginx/sites-enabled/hyperglass`. It supports IPv6, and will automatically redirect to HTTPS. The highlighted lines should be replaced with your installation's specific variables. - -```nginx {4,9,10,17,19} title="NGINX" -server { - listen 80; - listen [::]:80; - server_name lg.example.com; - return 301 https://$host$request_uri; -} -server { - listen [::]:443 ssl ipv6only=on; - listen 443 ssl; - ssl_certificate <path to cert chain> - ssl_certificate_key <path to key> - - client_max_body_size 2M; - - server_name lg.example.com; - - root /etc/hyperglass/static; - - location / { - try_files $uri $uri/ /ui /ui/$uri =404; - index /ui/index.html; - } - - location /openapi.json { - try_files $uri @proxy_to_app; - } - - location /custom/ { - try_files $uri $uri/ /custom; - } - - location /images/ { - try_files $uri $uri/ /images; - } - - location /api { - try_files $uri @proxy_to_app; - } - - location @proxy_to_app { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $http_host; - proxy_redirect off; - proxy_pass http://[::1]:8001; - } - -} -``` diff --git a/docs/docs/query-settings.mdx b/docs/docs/query-settings.mdx deleted file mode 100644 index df9c88985..000000000 --- a/docs/docs/query-settings.mdx +++ /dev/null @@ -1,162 +0,0 @@ ---- -id: query-settings -title: Query Settings -sidebar_label: Query Settings -keywords: [hyperglass, queries] -description: hyperglass query types ---- - -import Code from "../src/components/JSXCode"; -import MiniNote from "../src/components/MiniNote"; -import RP from "../src/components/RegexPattern"; - -<div class="table--full-width" /> - -Each query type may be disabled, enabled, or customized. The `display_name` parameter defines how the query type will be displayed in the UI's Query Type select element. - -## `bgp_route` - -| Parameter | Type | Default | Description | -| :------------- | :-----: | :------------ | :----------------------------------------------------- | -| `enable` | Boolean | `true` | Enable or disable the BGP Route query type. | -| `display_name` | String | `'BGP Route'` | Text displayed for the BGP Route query type in the UI. | - -#### Example - -<!-- prettier-ignore --> -```yaml title="hyperglass.yaml" -queries: - bgp_route: - display_name: BGP Route - enable: true -``` - -## `bgp_community` - -| Parameter | Type | Default | Description | -| :------------- | :-----: | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `enable` | Boolean | `true` | Enable or disable the BGP Community query type. | -| `display_name` | String | `'BGP Community'` | Text displayed for the BGP Community query type in the UI. | -| `mode` | String | `'input'` | `input` mode requires the user to type the community value in the target element. `select` mode allows the user to select a community from a preconfigured list of communities. | -| `communities` | | | [BGP Community Definitions](#community-definitions) for `select` mode. | -| `pattern` | | | [BGP Community Regular Expression Patterns](#community-patterns) for `input` mode. | - -### Community Definitions - -If using `select` mode, you may define a list of communities the users can choose from. Each community definition uses the following schema: - -| Parameter | Type | Description | Example | -| :------------- | :----: | :--------------------- | :----------------------- | -| `community` | String | Community value | `'64496:1001'` | -| `display_name` | String | Community display name | `'64496:1001'` | -| `description` | String | Community description | `'North America Routes'` | - -#### Example - -```yaml title="hyperglass.yaml" -queries: - bgp_community: - enable: true - mode: select - communities: - - community: 64496:1001 - display_name: 64496:1001 - description: North America Routes - - community: 64496:1002 - display_name: 64496:1002 - description: Europe Routes -``` - -### Community Patterns - -If using `input` mode, hyperglass allows you to override the default regular expression patterns used to validate UI and API queries. hyperglass supports [Decimal (well known)](https://tools.ietf.org/html/rfc1997) communities, [Extended AS](https://tools.ietf.org/html/rfc4360) communities, and [Large](https://tools.ietf.org/html/rfc8092) communities. - -| Parameter | Type | Default | Description | -| :------------ | :----: | :--------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | -| `decimal` | String | <RP pattern="community_decimal"/> | Regular expression pattern for validating decimal type BGP Community strings. | -| `extended_as` | String | <RP pattern="community_extended"/> | Regular expression pattern for validating extended AS type BGP Community strings, e.g. `65000:1` | -| `large` | String | <RP pattern="community_large"/> | Regular expression pattern for validating [large community](http://largebgpcommunities.net/) strings, e.g. `65000:65001:65002` | - -#### Example - -<!-- prettier-ignore --> -```yaml title="hyperglass.yaml" -queries: - bgp_community: - enable: true - mode: input - pattern: - decimal: '^[0-9]{1,10}$' - extended_as: '^([0-9]{0,5})\:([0-9]{1,5})$' - large: '^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$' -``` - -:::caution -Regular expression patterns must be enclosed in single quotes, e.g. `'^.*$'` -::: - -## `bgp_aspath` - -| Parameter | Type | Default | Description | -| :------------- | :-----: | :-------------- | :---------------------------------------------------------------------- | -| `enable` | Boolean | `true` | Enable or disable the BGP AS Path query type. | -| `display_name` | String | `'BGP AS Path'` | Text displayed for the BGP AS Path query type in the UI. | -| `pattern` | | | [BGP AS Path Settings & Regular Expression Patterns](#as-path-patterns) | - -#### Example - -<!-- prettier-ignore --> -```yaml title="hyperglass.yaml" -queries: - bgp_aspath: - display_name: BGP AS Path - enable: true - pattern: - asdot: '^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$' - asplain: '^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$' - mode: asplain -``` - -### AS Path Patterns - -AS Path regular expression patterns may also be customized, should you wish to more granularly control what your network considers a valid AS Path pattern. hyperglass makes two "modes" available for validation - [**`asplain`** and **`asdot`**](https://tools.ietf.org/html/rfc5396). - -| Parameter | Type | Default | Description | -| :-------- | :----: | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `mode` | String | `'asplain'` | Set ASN display mode. This field is dependent on how your network devices are configured. <MiniNote>Must be <Code>asplain</Code> or <Code>asdot</Code></MiniNote> | -| `asplain` | String | <RP pattern="aspath_asplain"/> | Regular expression pattern for validating **asplain** type BGP AS Path queries. | -| `asdot` | String | <RP pattern="aspath_asdot"/> | Regular expression pattern for validating **asdot** type BGP AS Path queries. | - -## `ping` - -| Parameter | Type | Default | Description | -| :------------- | :-----: | :------- | :------------------------------------------------ | -| `enable` | Boolean | `true` | Enable or disable the Ping query type. | -| `display_name` | String | `'Ping'` | Text displayed for the Ping query type in the UI. | - -#### Example - -<!-- prettier-ignore --> -```yaml title="hyperglass.yaml" -queries: - ping: - display_name: Ping - enable: true -``` - -## `traceroute` - -| Parameter | Type | Default | Description | -| :------------- | :-----: | :------------- | :------------------------------------------------------ | -| `enable` | Boolean | `true` | Enable or disable the Traceroute query type. | -| `display_name` | String | `'Traceroute'` | Text displayed for the Traceroute query type in the UI. | - -#### Example - -<!-- prettier-ignore --> -```yaml title="hyperglass.yaml" -queries: - traceroute: - display_name: Traceroute - enable: true -``` diff --git a/docs/docs/response-caching.mdx b/docs/docs/response-caching.mdx deleted file mode 100644 index cb1ae628c..000000000 --- a/docs/docs/response-caching.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -id: response-caching -title: Response Caching -sidebar_label: Response Caching -keywords: [configuration, cache, timeout] -description: hyperglass caching configuration ---- - -hyperglass uses [Redis](https://redis.io/) for cache storage. Every query is cached and automatically expired from the cache after a configurable timeout period. - -Common Redis parameters are configurable, in case you already have a dedicated Redis server you'd prefer to use, instead of running it on the same server as hyperglass: - -| Parameter | Type | Default | Description | -| :---------- | :-----: | :------------ | :----------------------------------------------------------------------------------------------------- | -| `host` | String | `'localhost'` | Redis server IP address or hostname. | -| `port` | Integer | `6379` | Redis server TCP port. | -| `database` | Integer | `1` | Database ID for hyperglass. | -| `password` | String | `None` | Password for [Redis password authentication](https://redis.io/topics/security#authentication-feature). | -| `timeout` | Integer | `120` | Time in seconds query output will be kept in the Redis cache. | -| `show_text` | Boolean | `true` | Show the [cache message](ui/text) in the hyperglass UI. | - -:::important Caching -hyperglass caches every query response to a Redis database, and always responds to a request with the cached value. If hyperglass receives a query for which it has no matching cached entry, the query parameters are used to created a new cache entry, hyperglass executes the request normally, writes the response to the cache, and then returns the response to the end user. -::: - -## Example - -```yaml title="hyperglass.yaml" -cache: - database: 0 - host: localhost - port: 6379 - show_text: true - timeout: 120 -``` diff --git a/docs/docs/rest-api.mdx b/docs/docs/rest-api.mdx deleted file mode 100644 index 9dd84820c..000000000 --- a/docs/docs/rest-api.mdx +++ /dev/null @@ -1,85 +0,0 @@ ---- -id: rest-api -title: REST API -sidebar_label: REST API -keywords: [configuration, api, rest] -description: Configure the REST API ---- - -import MiniNote from "../src/components/MiniNote"; -import Code from "../src/components/JSXCode"; -import PageLink from "../src/components/PageLink"; - -The hyperglass API is based on the powerful [FastAPI](https://fastapi.tiangolo.com/) framework, which includes automatic API documentation using the [OpenAPI](https://www.openapis.org/) specification. If you plan to provide your users with API access to your hyperglass instance, it is highly recommended to leave this enabled. - -FastAPI provides built in support for both [Swagger UI](https://swagger.io/tools/swagger-ui/) and [Redoc](https://redocly.github.io/redoc/), which are open source projects that create a pretty web UI from an OpenAPI specification. - -## Settings - -| Parameter | Type | Default | Description | -| :------------ | :-----: | :--------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | -| `enable` | Boolean | `true` | Enable or disable the API documentation. | -| `title` | String | `'{site_title} API Documentation'` | API documentation title. `{site_title}` will be replaced with the [`site_title`](parameters.mdx#global-settings) parameter. | -| `description` | String | | API documentation description appearing below the title. | -| `base_url` | String | `'https://lg.example.net'` | The base URL for your hyperglass site. Used by OpenAPI for dynamically creating hyperlinks. | -| `mode` | String | `'redoc'` | OpenAPI UI library to use for the hyperglass API docs. <MiniNote>Must be <Code>redoc</Code> or <Code>swagger</Code></MiniNote> | -| `uri` | String | `'/api/docs'` | HTTP URI/path where API documentation can be accessed. | -| `openapi_uri` | String | `'/openapi.json'` | Path to the automatically generated `openapi.json` file. | -| `queries` | | | `/queries` endpoint settings <PageLink to="#queries">➡️</PageLink> | -| `query` | | | `/query` endpoint settings <PageLink to="#query">➡️</PageLink> | -| `devices` | | | `/devices` endpoint settings <PageLink to="#devices">➡️</PageLink> | - -### `queries` - -| Parameter | Type | Default | Description | -| :------------ | :----: | :--------------------------------- | :----------------------------------------------------------- | -| `title` | String | `'Supported Queries'` | Displayed as the header text above the API endpoint section. | -| `description` | String | `'List of supported query types.'` | Displayed inside each API endpoint section. | -| `summary` | String | `'Query Types'` | Displayed beside the API endpoint URI. | - -### `query` - -| Parameter | Type | Default | Description | -| :------------ | :----: | :----------------------------------------- | :----------------------------------------------------------- | -| `title` | String | `'Submit Query'` | Displayed as the header text above the API endpoint section. | -| `description` | String | `'Request a query response per-location.'` | Displayed inside each API endpoint section. | -| `summary` | String | `'Query the Looking Glass'` | Displayed beside the API endpoint URI. | - -### `devices` - -| Parameter | Type | Default | Description | -| :------------ | :----: | :---------------------------------------------------------------------------------------------- | :----------------------------------------------------------- | -| `title` | String | `'Devices'` | Displayed as the header text above the API endpoint section. | -| `description` | String | `'List of all devices/locations with associated identifiers, display names, networks, & VRFs.'` | Displayed inside each API endpoint section. | -| `summary` | String | `'Devices List'` | Displayed beside the API endpoint URI. | - -## Example - -```yaml title="hyperglass.yaml" -docs: - base_url: https://lg.example.net - description: "" - devices: - description: - List of all devices/locations with associated identifiers, display - names, networks, & VRFs. - summary: Devices List - title: Devices - enable: true - mode: redoc - openapi_uri: /openapi.json - queries: - description: List of supported query types. - summary: Query Types - title: Supported Queries - query: - description: Request a query response per-location. - summary: Query the Looking Glass - title: Submit Query - title: "{site_title} API Documentation" - uri: /api/docs -``` - -:::note From the developer -I'm partial to Redoc, partially because I find it to be more aesthetically pleasing, and partially because it's written in [ReactJS](https://reactjs.org/), just like the hyperglass UI. At some point, I plan to migrate away from the built-in Redoc page and integrate Redoc directly with hyperglass. -::: diff --git a/docs/docs/setup.mdx b/docs/docs/setup.mdx deleted file mode 100644 index 4b2a2603e..000000000 --- a/docs/docs/setup.mdx +++ /dev/null @@ -1,74 +0,0 @@ ---- -id: setup -title: Setup -sidebar_label: Setup -keywords: [install, setup] -description: hyperglass Setup ---- - -import Link from "@docusaurus/Link"; - -After hyperglass is [installed](getting-started.mdx), you can run the `hyperglass --help` command to see the available options: - -```shell-session -$ hyperglass --help -Usage: hyperglass [OPTIONS] COMMAND [ARGS]... - - hyperglass Command Line Interface - -Options: - -v, --version 🔢 hyperglass version - -h, --help 🙏 Show this help message - -Commands: - build-ui 🦋 Create a new UI build - clear-cache 🧼 Clear the Redis cache - secret 🔒 Generate agent secret - setup 🧰 Run the setup wizard - start 🚀 Start web server -``` - -## Setup Wizard - -To start the setup wizard, run `hyperglass setup`. - -You'll receive the following prompts: - -### Installation Directory - -```shell-session -[?] Choose a directory for hyperglass: /Users/ml/hyperglass - > /home/user/hyperglass - /etc/hyperglass -``` - -hyperglass requires a directory on your system to store configuration files, the web UI, logos, etc. You may choose between the current user's home directory or `/etc` (use the up/down arrow keys). - -## UI Build - -After running the setup, the `build-ui` command needs to be run for the first time. This command creates the required file structure for the UI, initializes frontend dependencies, and generates favicons. - -:::caution -If your system's compute resources are too low, the `build-ui` step will likely fail. You can use the `--timeout` flag to increase the default timeout of 180 seconds. You can also set the environment variable `HYPERGLASS_UI_BUILD_TIMEOUT` for the same result. See the [system requirements](production.mdx) section for more information about system resources. -::: - -```shell-session -$ hyperglass build-ui -✅ Completed UI build in production mode -``` - -## Startup - -To start hyperglass from the console run `hyperglass start`. - -:::important -hyperglass won't start without a `devices.yaml` file. See the [Adding Devices](adding-devices.mdx) section for instructions on how add devices. -::: - -```shell-session -$ hyperglass start -``` - -:::tip Production -Remember to check the [Production](production.mdx) section for instructions on how to run hyperglass in production. It includes instructions and examples for setting up a Reverse Proxy (such as NGINX or Caddy), and running hyperglass as a system service with systemd. -::: diff --git a/docs/docs/styles.module.css b/docs/docs/styles.module.css deleted file mode 100644 index 359aa5e67..000000000 --- a/docs/docs/styles.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.getStarted[class][class]:not(.button--active):not(:hover) { - color: var(--ifm-color-secondary); -} - -.getStarted[class][class]:not(.button--active):hover { - color: var(--ifm-color-white); -} - -.year { - color: var(--ifm-color-primary); -} diff --git a/docs/docs/table-output.mdx b/docs/docs/table-output.mdx deleted file mode 100644 index 305a37473..000000000 --- a/docs/docs/table-output.mdx +++ /dev/null @@ -1,74 +0,0 @@ ---- -id: table-output -title: Table Output -sidebar_label: Table Output -description: Configure structured data parameters ---- - -Devices that support responding to a query with structured or easily parsable data can have their response data placed into an easier to read table (or JSON, when using the [REST API](rest-api)). Currently, the following platforms have structured data supported in hyperglass: - -- Juniper Junos -- Arista EOS - -:::note -Unlike with standard text output, devices with structured data enabled do not support customization of their commands. This is because the response must be something hyperglass is preconfigured to understand and parse, so hyperglass must maintain control over the specific command to run for a given platform. -::: - -<div class="table--full-width" /> - -The `structured` subsection contains multiple subsections of its own: - -| Section | Description | All Options | -| :------------ | :--------------------------------- | :---------------------------------------: | -| `communities` | Include or exclude BGP communities | <PageLink to="#communities">➡️</PageLink> | -| `rpki` | Configure RPKI validation | <PageLink to="#rpki">➡️</PageLink> | - -## `communities` - -| Parameter | Type | Default | Description | -| :-------- | :----: | :------: | :------------------------------------------------------------------------------------------------------------------------------------ | -| `mode` | String | `'deny'` | `'deny'` denies any matching patterns and permits anything else. `'permit'` only permits matching patterns and denies everything else | -| `items` | List | `[]` | List of regular expression patterns to match | - -:::note Regular Expressions with YAML -If you're using any regex patterns with special characters, you'll probably need to wrap the patterns in single quotes, to denote a raw string. For example: - -```yaml -normal_string: Normal String -raw_string: '^Raw\sString$' -``` - -::: - -## `rpki` - -| Parameter | Type | Default | -| :-------- | :----: | :--------: | -| `mode` | String | `'router'` | - -### `router` mode - -`router` mode uses the RPKI state from the perspective of the router. This means if your network is not using Origin Validation, the RPKI state will be shown as "Not Verified". Otherwise, the corresponding RPKI state will be shown. - -### `external` mode - -`external` mode takes each prefix, the last ASN in the `AS_PATH` and requests the RPKI validation state via the [Cloudflare RPKI Portal](https://rpki.cloudflare.com/). - -## Example - -```yaml title="hyperglass.yaml" -structured: - communities: - mode: permit - items: - - '65000:\d+' - - '65[1-4]00:\d+' - rpki: - mode: external -``` - -## Caveats - -### Arista EOS - -For whatever reason, Arista EOS does not supply certain details about routes in its JSON output when running commands `show ip bgp regex <pattern>` or `show ip bgp community <community>`. Specifically, the the route's timestamp and any attached communities are not supplied. When these commands are used with Arista EOS, hyperglass sets the timestamp to the current time, and the community to an empty list. diff --git a/docs/docs/ui/configuration.mdx b/docs/docs/ui/configuration.mdx deleted file mode 100644 index 2ceba0e6a..000000000 --- a/docs/docs/ui/configuration.mdx +++ /dev/null @@ -1,139 +0,0 @@ ---- -id: configuration -title: UI Configuration -sidebar_label: Configuration -keywords: [hyperglass, looking glass, web ui, gui, theme, colors, branding] -description: Customize the Web UI ---- - -import R from "../../src/components/Required"; -import MiniNote from "../../src/components/MiniNote"; -import Code from "../../src/components/JSXCode"; -import PageLink from "../../src/components/PageLink"; - -## Build - -hyperglass is build with [NextJS](https://nextjs.org/), a [React](https://reactjs.org/)-based UI framework that supports server-side rendering and static exporting, which contribute to hyperglass's speed and SEO-friendliness. At startup, hyperglass creates a new "UI build", which is a static export of the site and includes some elements of the [configuration](parameters.mdx). - -This UI build process can be run manually via the hyperglass CLI: - -```shell-session -$ hyperglass build-ui -✅ Completed UI build in production mode -``` - -Or with the `--build` CLI flag on startup: - -```shell-session -$ hyperglass start --build -``` - -The UI build will run automatically any time the hyperglass configuration or version changes. - -## Sections - -<div class="table--full-width" /> - -The `web` subsection contains multiple subsections of its own, should you wish to customize various aspects of the UI: - -| Section | Description | All Options | -| :-------------- | :----------------------------- | :-----------------------------------------: | -| `credit` | Developer credit & GitHub Link | <PageLink to="#credit">➡️</PageLink> | -| `dns_provider` | DNS over HTTPS Provider | <PageLink to="#dns_provider">➡️</PageLink> | -| `external_link` | Link to external site | <PageLink to="#external_link">➡️</PageLink> | -| `greeting` | Greeting Modal | <PageLink to="#greeting">➡️</PageLink> | -| `logo` | Logo & Favicons | <PageLink to="logo">➡️</PageLink> | -| `opengraph` | [OpenGraph](https://ogp.me/) | <PageLink to="#opengraph">➡️</PageLink> | -| `links` | Footer Links | <PageLink to="#links">➡️</PageLink> | -| `menus` | Footer Menus | <PageLink to="#menus">➡️</PageLink> | -| `text` | Text, title, & names | <PageLink to="text">➡️</PageLink> | -| `theme` | Colors & Fonts | <PageLink to="theme">➡️</PageLink> | - -### `credit` - -| Parameter | Type | Default | Description | -| :-------- | :-----: | :-----: | :--------------------------------------------------------------------------------------- | -| `enable` | Boolean | `true` | Enable or disable the display of developer credit & link to hyperglass GitHub repository | - -:::note From the developer -If your organization's policy allows, and you don't mind, I request that you keep `credit` enabled. Remember: my goal for this project is get more networks to use looking glasses to make all of our lives easier. Because it's primarily other network operators who will use this tool to begin with, I'd love for any operators that use your looking glass to know where they can get their own. -::: - -### `dns_provider` - -| Parameter | Type | Default | Description | -| :-------- | :----: | :------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | String | `'cloudflare'` | DNS over HTTPS provider for in-browser DNS resolution. Cloudflare & Google supported. <MiniNote>Must be <Code>cloudflare</Code> or <Code>google</Code></MiniNote> | - -### `links` - -Specify an array/list of links to show in the footer. By default, a link to your ASN's PeeringDB page is used. - -| Parameter | Type | Default | Description | -| :---------- | :-----: | :---------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | -| `title` | String | `'PeeringDB'` | Link title/label | -| `url` | String | `'https://www.peeringdb.com/asn/{primary_asn}'` | Target URL. `{primary_asn}` will be replaced with the `primary_asn` value from [Global Settings](parameters.mdx#global-settings) | -| `show_icon` | Boolean | `true` | Show an icon on the right side of the link indicating that the link will take the user away from the hyperglass page. | -| `side` | String | `'left'` | Show the link on the `'left'` or `'right'` side of the footer. | -| `order` | Integer | `0` | Specify the order of the links (left to right). During rendering, `links` and [`menus`](#menus) are merged, so the order is used to sort both. | - -#### Example - -```yaml title="hyperglass.yaml" -web: - links: - - title: Website - url: https://www.example.com - side: right - order: 5 - - title: PeeringDB - url: https://www.peeringdb.com/asn/{primary_asn} - side: left - order: 2 -``` - -### `menus` - -Specify an array/list of menus to show in the footer. A menu's content can be plain text or markdown. By default, generic help and terms & conditions menus are shown. - -| Parameter | Type | Default | Description | -| :-------- | :-----: | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | -| `title` | String | | Menu title/label | -| `content` | String | | Menu content. Can be any string content, or a path to a plain text or markdown file. | -| `side` | String | `'left'` | Show the menu on the `'left'` or `'right'` side of the footer. | -| `order` | Integer | `0` | Specify the order of the links (left to right). During rendering, [`links`](#links) and `menus` are merged, so the order is used to sort both. | - -#### Example - -```yaml title="hyperglass.yaml" -web: - menus: - - title: Help - content: "Please contact [support@example.com](mailto:support@example.com) to get support." - - title: Terms & Conditions - content: /etc/hyperglass/terms-and-conditions.md - side: right -``` - -### `greeting` - -| Parameter | Type | Default | Description | -| :--------- | :-----: | :----------- | :------------------------------------------------------------------------------------------- | -| `enable` | Boolean | `false` | Enable or disable the greeting modal. | -| `file` | String | | Path to a [markdown](https://www.markdownguide.org/) file containing the modal body content. | -| `title` | String | `'Welcome'` | Modal title. | -| `button` | String | `'Continue'` | Button text. | -| `required` | Boolean | `false` | If `true` the user must click the button in order to submit a query. | - -### `opengraph` - -If you're not familiar with [OpenGraph](https://ogp.me/), it's the thing that generates the pretty pictures, titles, and descriptions for links when you post them to sites/tools such as Facebook, Twitter, Slack, etc. - -By default, [this Opengraph image](/opengraph.jpg) is set. If you define one with `image`, hyperglass will automatically generate an Opengraph image using the following methodology: - -- If the image is already 1200x630, it will be used -- If the image is or is not 1200x630, but contains transparency (like in a `.png` formatted image), it will be converted to a JPEG file with the [`black`](theme#colors) used as the background color. - -| Parameter | Type | Description | -| :-------- | :----: | :---------------------- | -| `image` | String | Path to opengraph image | diff --git a/docs/docs/ui/example.mdx b/docs/docs/ui/example.mdx deleted file mode 100644 index 25c1c6ca3..000000000 --- a/docs/docs/ui/example.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -id: example -title: Example UI Configuration -sidebar_label: Example -keywords: - [hyperglass, looking glass, web ui, gui, theme, colors, branding, example] -description: See how to the hyperglass UI should be configured. ---- - -This is an example of every UI configuration option. You don't need to specify any of these for hyperglass to work; all parameters are optional. - -```yaml title="hyperglass.yaml" -web: - credit: - enable: true - dns_provider: - name: cloudflare - url: https://cloudflare-dns.com/dns-query - greeting: - button: Continue - enable: true - file: /etc/hyperglass/greeting.md - required: true - title: Welcome - links: - - order: 0 - show_icon: true - side: left - title: PeeringDB - url: https://www.peeringdb.com/asn/{primary_asn} - logo: - dark: /Users/ml/dev/hyperglass/hyperglass/images/hyperglass-dark.svg - favicon: /Users/ml/dev/hyperglass/hyperglass/images/hyperglass-icon.svg - height: null - light: /Users/ml/dev/hyperglass/hyperglass/images/hyperglass-light.svg - width: 100% - menus: - - content: /etc/hyperglass/terms.md - order: 0 - side: left - title: Terms - - content: /etc/hyperglass/help.md - order: 0 - side: left - title: Help - opengraph: - image: /etc/hyperglass/opengraph.png - text: - cache_icon: Cached from {time} UTC - cache_prefix: "Results cached for " - complete_time: Completed in {seconds} - fqdn_error: Unable to resolve {fqdn} - fqdn_error_button: Try Again - fqdn_message: Your browser has resolved {fqdn} to - fqdn_tooltip: Use {protocol} - no_communities: No Communities - query_location: Location - query_target: Target - query_type: Query Type - query_vrf: Routing Table - rpki_invalid: Invalid - rpki_unknown: No ROAs Exist - rpki_unverified: Not Verified - rpki_valid: Valid - subtitle: Network Looking Glass - title: hyperglass - title_mode: logo_subtitle - theme: - colors: - black: "#000" - blue: "#314cb6" - cyan: "#118ab2" - danger: "#d84b4b" - dark: "#010101" - error: "#ff6b35" - gray: "#c1c7cc" - green: "#35b246" - light: "#f5f6f7" - orange: "#ff6b35" - pink: "#f2607d" - primary: "#118ab2" - purple: "#8d30b5" - red: "#d84b4b" - secondary: "#314cb6" - success: "#35b246" - teal: "#35b299" - warning: "#edae49" - white: "#fff" - yellow: "#edae49" - default_color_mode: null - fonts: - body: Nunito - mono: Fira Code -``` diff --git a/docs/docs/ui/logo.mdx b/docs/docs/ui/logo.mdx deleted file mode 100644 index 34dfa4617..000000000 --- a/docs/docs/ui/logo.mdx +++ /dev/null @@ -1,99 +0,0 @@ ---- -id: logo -title: Logo -sidebar_label: Logo -keywords: [hyperglass, looking glass, web ui, gui, branding, logo] -description: Customize the Logo & Favicon ---- - -## `logo` - -| Parameter | Type | Default | Description | -| :-------- | :-----: | :-------------- | :-------------------------------------------- | -| `light` | String | hyperglass logo | Path to logo that will be used in light mode | -| `dark` | String | hyperglass logo | Path to logo that will be used in dark mode | -| `favicon` | String | hyperglass icon | Path to logo that will be used as the favicon | -| `width` | String | `100%` | Maximum logo width as a percentage | -| `height` | Integer | | Maximum logo height as a percentage | - -### Favicon - -It's very easy to customize the favicon imaged used by hyperglass. If you're not familiar with the term, the favicon is the icon used by the browser in tabs, windows, or bookmarks: - -![](/screenshots/screenshot-favicons.jpg) - -During the [UI build process](setup.mdx#ui-build) hyperglass automatically generates favicon files in all of the many formats required for cross-platform, cross-browser support. - -#### Supported Formats - -- `.jpg` -- `.jpeg` -- `.png` -- `.svg` - -:::caution Icon File Size -Generating the favicons is very CPU-intensive, and therefore has the tendency of timing out if the icon file is very large. If you receive a timeout error when starting hyperglass or running the `hyperglass build-ui` command, try reducing the size of the icon file. **File sizes of around 200 KB** have been known to succeed without too much overhead. -::: - -The following icons types are generated: - -``` -android-chrome-36x36.png -android-chrome-48x48.png -android-chrome-72x72.png -android-chrome-96x96.png -android-chrome-144x144.png -android-chrome-192x192.png -android-chrome-256x256.png -android-chrome-384x384.png -android-chrome-512x512.png -apple-touch-icon-57x57.png -apple-touch-icon-60x60.png -apple-touch-icon-72x72.png -apple-touch-icon-76x76.png -apple-touch-icon-114x114.png -apple-touch-icon-120x120.png -apple-touch-icon-144x144.png -apple-touch-icon-152x152.png -apple-touch-icon-167x167.png -apple-touch-icon-180x180.png -apple-touch-icon-1024x1024.png -apple-touch-icon-precomposed.png -apple-touch-icon.png -apple-touch-startup-image-640x1136.png -apple-touch-startup-image-750x1334.png -apple-touch-startup-image-828x1792.png -apple-touch-startup-image-1125x2436.png -apple-touch-startup-image-1136x640.png -apple-touch-startup-image-1242x2208.png -apple-touch-startup-image-1242x2688.png -apple-touch-startup-image-1334x750.png -apple-touch-startup-image-1536x2048.png -apple-touch-startup-image-1620x2160.png -apple-touch-startup-image-1668x2224.png -apple-touch-startup-image-1668x2388.png -apple-touch-startup-image-1792x828.png -apple-touch-startup-image-2048x1536.png -apple-touch-startup-image-2048x2732.png -apple-touch-startup-image-2160x1620.png -apple-touch-startup-image-2208x1242.png -apple-touch-startup-image-2224x1668.png -apple-touch-startup-image-2388x1668.png -apple-touch-startup-image-2436x1125.png -apple-touch-startup-image-2688x1242.png -apple-touch-startup-image-2732x2048.png -coast-228x228.png -favicon-16x16.png -favicon-32x32.png -favicon-48x48.png -favicon.ico -firefox_app_60x60.png -firefox_app_128x128.png -firefox_app_512x512.png -mstile-70x70.png -mstile-144x144.png -mstile-150x150.png -mstile-310x150.png -mstile-310x310.png -yandex-browser-50x50.png -``` diff --git a/docs/docs/ui/text.mdx b/docs/docs/ui/text.mdx deleted file mode 100644 index 64aa29c13..000000000 --- a/docs/docs/ui/text.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: text -title: Text -sidebar_label: Text -keywords: [hyperglass, looking glass, web ui, gui, branding, text, messages] -description: Customize the text used in the web UI ---- - -## `text` - -| Parameter | Type | Default | Description | -| :--------------- | :----: | :-------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `cache_prefix` | String | `'Results cached for '` | Text displayed with the cache timeout countdown. | -| `cache_icon` | String | `'Cached Response from {time}'` | Text displayed when a user hovers over the lightning bolt icon, which is displayed when a response from the server was a cached response. `{time}` is replaced with the _original_ query's timestamp. | -| `completed_time` | String | `'Completed in {seconds}'` | Text displayed when a user hovers over the success icon for a query result. `{seconds}` will be replaced with 'n seconds' where n is the time a query took to complete. | -| `fqdn_message` | String | `'Your browser has resolved {fqdn} to'` | Text displayed when prompting a user to select a resolve IPv4 or IPv6 address for an FQDN query. | -| `fqdn_error` | String | `'Unable to resolve {fqdn}'` | Text displayed when an FQDN is not resolvable. | -| `fqdn_button` | String | `'Try Again'` | Button text used when an FQDN is not resolvable. | -| `no_communities` | String | `'No Communities'` | Shown in table output when a route has no communities. | -| `query_location` | String | `'Location'` | Query Location (router) form label. | -| `query_target` | String | `'Target'` | Query Target (IP/hostname/community/AS Path) form label. | -| `query_type` | String | `'Query Type'` | Query Type (BGP Route, Ping, Traceroute, etc.) form label. | -| `query_vrf` | String | `'Routing Table'` | Query VRF form label. | -| `subtitle` | String | `'Network Looking Glass'` | Subtitle text. | -| `title` | String | `'hyperglass'` | Title text. | -| `title_mode` | String | `'text_only'` | Set the title mode. <MiniNote>Must be <Code>text_only</Code>, <Code>logo_only</Code>, <Code>logo_subtitle</Code>, or <Code>all</Code></MiniNote> | - -### Title Mode - -The `title_mode` parameter behaves in the following manner: - -<div className="table--full-width" /> - -| Mode | Behavior | -| --------------- | ----------------------------------------------------------------------- | -| `text_only` | Shows the [`title`](#text) and [`subtitle`](#text) only. | -| `logo_only` | Shows the [`logo`](ui/logo.mdx) only. | -| `logo_subtitle` | Shows the [`logo`](ui/logo.mdx) and [`subtitle`](#text) only. | -| `all` | Shows the [`logo`](ui/logo.mdx), [`title`](#text), [`subtitle`](#text). | diff --git a/docs/docs/ui/theme.mdx b/docs/docs/ui/theme.mdx deleted file mode 100644 index 1eb549286..000000000 --- a/docs/docs/ui/theme.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -id: theme -title: Theme -sidebar_label: Theme -keywords: [hyperglass, looking glass, web ui, gui, branding, theme] -description: Customize Theme ---- - -import Color from "../../src/components/Color"; -import Font from "../../src/components/Font"; -import PageLink from "../../src/components/PageLink"; - -## `theme` - -### Parameters - -| Parameter | Type | Description | -| :------------------- | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------- | -| `default_color_mode` | String | Sets the default color mode. Must be `light` or `dark`. If no default mode is specified, hyperglass will use the browser's preferred color mode. | - -### Subsections - -| Section | Description | All Options | -| :------- | :----------- | :----------------------------------: | -| `colors` | Theme colors | <PageLink to="#colors">➡️</PageLink> | -| `fonts` | Theme fonts | <PageLink to="#fonts">➡️</PageLink> | - -#### `colors` - -| Parameter | Type | Default | Description | -| :---------- | :----: | :--------------------- | :------------------------------------- | -| `primary` | String | | Primary accent color | -| `secondary` | String | | Secondary accent color | -| `success` | String | | Success message/status color | -| `warning` | String | | Warning message/status color | -| `error` | String | | Error message/status color | -| `danger` | String | | Danger message/status color | -| `dark` | String | <Color hex="#121212"/> | Used as background color in dark mode | -| `light` | String | <Color hex="#f5f6f7"/> | Used as background color in light mode | -| `red` | String | <Color hex="#d84b4b"/> | Used as `danger` color if undefined | -| `orange` | String | <Color hex="#ff6b35"/> | Used as `error` color if undefined | -| `yellow` | String | <Color hex="#edae49"/> | Used as `warning` color if undefined | -| `green` | String | <Color hex="#35b246"/> | Used as `success` color if undefined | -| `cyan` | String | <Color hex="#118ab2"/> | Used as `primary` color if undefined | -| `blue` | String | <Color hex="#314cb6"/> | Used as `secondary` color if undefined | -| `teal` | String | <Color hex="#35b299"/> | | -| `pink` | String | <Color hex="#f2607d"/> | | -| `purple` | String | <Color hex="#8d30b5"/> | | -| `gray` | String | <Color hex="#c1c7cc"/> | | - -#### `fonts` - -Currently, only [Google Fonts](https://fonts.google.com/) are supported. - -| Parameter | Type | Default | Description | -| :-------- | :----: | :----------------------- | :-------------------------------------- | -| `body` | String | <Font name="Nunito"/> | Main body font | -| `mono` | String | <Font name="Fira Code"/> | Monospace font, used for command output | diff --git a/docs/docs/upgrading.mdx b/docs/docs/upgrading.mdx deleted file mode 100644 index 1cf0dde05..000000000 --- a/docs/docs/upgrading.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -id: upgrading -title: Upgrading hyperglass -sidebar_label: Upgrading -keywords: [hyperglass, looking glass, upgrade, upgrading, version] -description: How to upgrade hyperglass ---- - -## Upgrade Process - -To upgrade hyperglass, stop the currently-running service and upgrade via PyPI: - -```shell-session -$ sudo systemctl stop hyperglass -$ sudo pip3 install -U hyperglass -``` - -## Check Current version - -To check the current version of hyperglass, run the following command: - -```shell-session -$ sudo hyperglass --version -hyperglass version: 1.0.0-beta.19 -``` diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js deleted file mode 100755 index a04fb45c7..000000000 --- a/docs/docusaurus.config.js +++ /dev/null @@ -1,103 +0,0 @@ -const githubURL = "https://github.com/thatmattlove/hyperglass"; - -const { googleTrackingId, algoliaKey } = process.env; - -const docusaurusConfig = { - title: "hyperglass", - tagline: "the network looking glass that tries to make the internet better.", - url: "https://hyperglass.dev", - baseUrl: "/", - favicon: "img/favicon.ico", - organizationName: "thatmattlove", - projectName: "hyperglass", - themeConfig: { - image: "opengraph.jpg", - algolia: { - apiKey: algoliaKey || "dev", - indexName: "hyperglass", - }, - prism: { - additionalLanguages: ["shell-session", "ini", "nginx", "yaml"], - theme: require("./src/prism-dracula"), - }, - navbar: { - items: [ - { to: "docs/introduction", label: "Docs", position: "left" }, - { - href: "https://demo.hyperglass.dev", - label: "Demo", - position: "left", - }, - { - href: githubURL, - position: "right", - className: "header-github-link", - "aria-label": "GitHub Repository", - }, - ], - }, - footer: { - links: [ - { - title: "Docs", - items: [ - { - label: "Introduction", - to: "docs/introduction", - }, - { - label: "Getting Started", - to: "docs/getting-started", - }, - { - label: "Configuration", - to: "docs/parameters", - }, - ], - }, - { - title: "Community", - items: [ - { - label: "Slack", - href: "https://netdev.chat", - }, - { - label: "Telegram", - href: "https://t.me/hyperglasslg", - }, - ], - }, - { - title: "Social", - items: [ - { - label: "GitHub", - href: githubURL, - }, - { - label: "Twitter", - href: "https://twitter.com/thatmattlove", - }, - ], - }, - ], - }, - }, - presets: [ - [ - "@docusaurus/preset-classic", - { - docs: { - sidebarPath: require.resolve("./sidebars.js"), - editUrl: githubURL + "/edit/v1.0.0/docs/", - }, - theme: { - customCss: [require.resolve("./src/css/custom.css")], - }, - }, - ], - ], -}; - -module.exports = docusaurusConfig; diff --git a/docs/favicon-formats.ts b/docs/favicon-formats.ts new file mode 100644 index 000000000..1ac62332c --- /dev/null +++ b/docs/favicon-formats.ts @@ -0,0 +1,80 @@ +interface Favicon { + rel: string | null; + dimensions: [number, number]; + image_format: string; + prefix: string; +} + +export default [ + { dimensions: [48, 48], image_format: "ico", prefix: "favicon", rel: null }, + { dimensions: [16, 16], image_format: "png", prefix: "favicon", rel: "icon" }, + { dimensions: [32, 32], image_format: "png", prefix: "favicon", rel: "icon" }, + { dimensions: [64, 64], image_format: "png", prefix: "favicon", rel: "icon" }, + { dimensions: [96, 96], image_format: "png", prefix: "favicon", rel: "icon" }, + { dimensions: [180, 180], image_format: "png", prefix: "favicon", rel: "icon" }, + { + dimensions: [57, 57], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { + dimensions: [60, 60], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { + dimensions: [72, 72], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { + dimensions: [76, 76], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { + dimensions: [114, 114], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { + dimensions: [120, 120], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { + dimensions: [144, 144], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { + dimensions: [152, 152], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { + dimensions: [167, 167], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { + dimensions: [180, 180], + image_format: "png", + prefix: "apple-touch-icon", + rel: "apple-touch-icon", + }, + { dimensions: [70, 70], image_format: "png", prefix: "mstile", rel: null }, + { dimensions: [270, 270], image_format: "png", prefix: "mstile", rel: null }, + { dimensions: [310, 310], image_format: "png", prefix: "mstile", rel: null }, + { dimensions: [310, 150], image_format: "png", prefix: "mstile", rel: null }, + { dimensions: [196, 196], image_format: "png", prefix: "favicon", rel: "shortcut icon" }, +] as Favicon[]; diff --git a/docs/generateSitemap.js b/docs/generateSitemap.js deleted file mode 100644 index cf6b1b87a..000000000 --- a/docs/generateSitemap.js +++ /dev/null @@ -1,34 +0,0 @@ -const fs = require("fs"); - -const globby = require("globby"); -const prettier = require("prettier"); - -(async () => { - const pages = await globby(["docs/**/*{.js,.mdx}"]); - const sitemap = ` - <?xml version="1.0" encoding="UTF-8"?> - <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> - ${pages - .map((page) => { - const path = page - .replace("pages", "") - .replace(".js", "") - .replace(".mdx", ""); - const route = path === "/index" ? "" : path; - console.log("Added entry to sitemap:", path, route); - return ` - <url> - <loc>${`https://hyperglass.dev/${route}`}</loc> - </url> - `; - }) - .join("")} - </urlset> - `; - - const formatted = prettier.format(sitemap, { - parser: "html", - }); - - fs.writeFileSync("static/sitemap.xml", formatted); -})(); diff --git a/docs/global.d.ts b/docs/global.d.ts new file mode 100644 index 000000000..75facdcf0 --- /dev/null +++ b/docs/global.d.ts @@ -0,0 +1,8 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + NODE_ENV: 'development' | 'production'; + CF_PAGES_BRANCH: string; + } + } +} diff --git a/docs/global.module.css b/docs/global.module.css new file mode 100644 index 000000000..d6df9b3d1 --- /dev/null +++ b/docs/global.module.css @@ -0,0 +1,7 @@ +.logo { + color: #d84b4b; +} + +html[class~=dark] .logo { + color: #fff; +} \ No newline at end of file diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/docs/next-env.d.ts @@ -0,0 +1,5 @@ +/// <reference types="next" /> +/// <reference types="next/image-types/global" /> + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/docs/next.config.mjs b/docs/next.config.mjs new file mode 100644 index 000000000..9223adc8d --- /dev/null +++ b/docs/next.config.mjs @@ -0,0 +1,35 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import withNextra from "nextra"; + +function copyChangelog() { + const dir = path.dirname(fileURLToPath(import.meta.url)); + const src = path.resolve(dir, "..", "CHANGELOG.md"); + const data = fs.readFileSync(src); + const replaced = data.toString().replace("# Changelog\n\n", ""); + const dst = path.resolve(dir, "pages", "changelog.mdx"); + fs.writeFileSync(dst, replaced); +} + +copyChangelog(); + +/** + * @type {import('nextra').NextraConfig} + */ +const nextraConfig = { + theme: "nextra-theme-docs", + themeConfig: "./theme.config.tsx", +}; + +/** + * @type {import('next').NextConfig} + */ +const config = { + images: { + unoptimized: true, + }, + output: "export", +}; + +export default withNextra(nextraConfig)(config); diff --git a/docs/package.json b/docs/package.json old mode 100755 new mode 100644 index a8f916374..a63a8e993 --- a/docs/package.json +++ b/docs/package.json @@ -1,56 +1,24 @@ { "name": "hyperglass-docs", - "version": "1.0.0", + "version": "2.0.0", + "description": "hyperglass documentation", "private": true, "scripts": { - "docusaurus": "docusaurus", - "start": "docusaurus start", - "build": "docusaurus build", - "swizzle": "docusaurus swizzle", - "deploy": "docusaurus deploy", - "clear": "docusaurus clear", - "serve": "docusaurus serve", - "write-translations": "docusaurus write-translations", - "write-heading-ids": "docusaurus write-heading-ids" + "dev": "next dev", + "start": "next start", + "typecheck": "tsc --noEmit" }, + "author": "thatmattlove <matt@hyperglass.dev>", + "license": "BSD-3-Clause-Clear", "dependencies": { - "@docusaurus/core": "^2.0.0-beta.0", - "@docusaurus/preset-classic": "^2.0.0-beta.0", - "@mdx-js/react": "^1.6.22", - "@svgr/webpack": "^5.5.0", - "clsx": "^1.1.1", - "globby": "^11.0.1", - "prismjs": "^1.19.0", - "prop-types": "^15.7.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "url-loader": "^4.1.1", - "use-media": "^1.4.0" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "next": "^14.1.1", + "nextra": "3.0.0-alpha.24", + "nextra-theme-docs": "3.0.0-alpha.24", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "^2.0.0-beta.0", - "@tsconfig/docusaurus": "^1.0.2", - "@types/react": "^17.0.8", - "@types/react-helmet": "^6.1.1", - "@types/react-router-dom": "^5.1.7", - "prettier": "^2.0.5", - "typescript": "^4.3.2" - }, - "prettier": { - "[yaml]": { - "singleQuote": true - } + "@types/node": "^20.11.24", + "typescript": "^5.3.3" } } diff --git a/docs/pages/_meta.tsx b/docs/pages/_meta.tsx new file mode 100644 index 000000000..78bc398e6 --- /dev/null +++ b/docs/pages/_meta.tsx @@ -0,0 +1,42 @@ +export default { + index: { title: "Introduction", theme: { breadcrumb: false } }, + "---": { + type: "separator", + }, + installation: "Installation", + configuration: "Configuration", + platforms: "Platforms", + plugins: "Plugins", + documentation: { + title: "Documentation", + type: "menu", + items: { + installation: { + title: "Installation", + href: "/installation", + }, + configuration: { + title: "Configuration", + href: "/configuration", + }, + plugins: { + title: "Plugins", + href: "/plugins", + }, + changelog: { + title: "Changelog", + href: "/changelog", + }, + license: { + title: "License", + href: "/license", + }, + }, + }, + demo: { + title: "Demo", + type: "page", + href: "https://demo.hyperglass.dev", + newWindow: true, + }, +}; diff --git a/docs/pages/configuration/_meta.tsx b/docs/pages/configuration/_meta.tsx new file mode 100644 index 000000000..713550eb6 --- /dev/null +++ b/docs/pages/configuration/_meta.tsx @@ -0,0 +1,7 @@ +export default { + overview: "Overview", + config: "Config File", + devices: "Devices File", + directives: "Directives File", + examples: "Examples", +}; diff --git a/docs/pages/configuration/config.mdx b/docs/pages/configuration/config.mdx new file mode 100644 index 000000000..85262afe3 --- /dev/null +++ b/docs/pages/configuration/config.mdx @@ -0,0 +1,44 @@ +--- +title: Configuration File +description: hyperglass config.yaml file reference +--- + +The `config.yaml` file is broken into multiple sections: + +## Top Level Parameters + +| Parameter | Type | Default Value | Description | +| :----------------- | :-------------- | :------------------------------- | :------------------------------------------------------------ | +| `org_name` | String | Beloved Hyperglass User | Your organization's name. | +| `plugins` | List of Strings | | List of hyperglass [plugins](/plugins) to load. | +| `primary_asn` | String | 65000 | Your organization's primary ASN. Used to set default UI text. | +| `request_timeout` | Number | 90 | Global timeout in seconds for all requests. | +| `site_description` | String | `org_name` Network Looking Glass | `<meta>` description, also used in the API documentation. | +| `site_title` | String | `org_name` | Browser title, also used in the default terms & conditions. | + +#### Example with Defaults + +```yaml filename="config.yaml" +org_name: Beloved Hyperglass User +plugins: [] +primary_asn: 65000 +request_timeout: 90 +site_description: Beloved Hyperglass User Network Looking Glass +site_title: Beloved Hyperglass User +``` + +## Other Configuration Sections + +| Parameter | Docs | Description | +| :----------- | :--------------------------------------------------------------------- | :--------------------------------------------------------------- | +| `cache` | [Caching Docs](/configuration/config/caching.mdx) | Customize how hyperglass caches responses. | +| `logging` | [Logging Docs](/configuration/config/logging.mdx) | Customize file logging, syslog, webhooks, etc. | +| `messages` | [Messages Docs](/configuration/config/messages.mdx) | Customize messages shown to users. | +| `structured` | [Structured Output Docs](/configuration/config/structured-ouptput.mdx) | Customize how hyperglass handles structured output from devices. | +| `web` | [Web UI Docs](/configuration/config/web-ui.mdx) | Customize the look and feel of hyperglass's web UI. | + +## Caveats + +### Arista EOS + +For whatever reason, the tested version of Arista EOS does not supply certain details about routes in its JSON output when running commands `show ip bgp regex <pattern>` or `show ip bgp community <community>`. Specifically, the the route's timestamp and any attached communities are not supplied. When these commands are used with Arista EOS, hyperglass sets the timestamp to the current time, and the community to an empty list. diff --git a/docs/pages/configuration/config/_meta.tsx b/docs/pages/configuration/config/_meta.tsx new file mode 100644 index 000000000..c91b46169 --- /dev/null +++ b/docs/pages/configuration/config/_meta.tsx @@ -0,0 +1,8 @@ +export default { + "api-docs": "API Docs", + caching: "Caching", + logging: "Logging & Webhooks", + messages: "Messages", + "structured-output": "Structured Output", + "web-ui": "Web UI", +}; diff --git a/docs/pages/configuration/config/api-docs.mdx b/docs/pages/configuration/config/api-docs.mdx new file mode 100644 index 000000000..a88dd4398 --- /dev/null +++ b/docs/pages/configuration/config/api-docs.mdx @@ -0,0 +1,72 @@ +## API Docs + +hyperglass automatically generates documentation for the REST API. The `docs` section allows users to customize the look, feel, and text used for the REST API documentation. + +| Parameter | Type | Default Value | Description | +| :----------------- | :------ | :----------------------------- | :---------------------------------------------------------------------------------------------- | +| `docs.base_url` | String | https://lg.example.com | Used for REST API samples. See the [demo](https://demo.hyperglass.dev/api/docs) for an example. | +| `docs.enable` | Boolean | `true` | Enable or disable the REST API documentation. | +| `docs.path` | String | /api/docs | Path to the REST API documentation. | +| `docs.title` | String | `site_title` API Documentation | API docs title. Uses the `site_title` parameter from the [global](#global) parameters. | +| `docs.description` | String | | API docs description. Appears below the title. | + +The documentation for API endpoints follow a common schema: + +- `devices` +- `info` +- `queries` +- `query` + +### Schema + +| Parameter | Type | Description | +| :------------ | :----- | :------------------------------------------------------------------------------- | +| `title` | String | API endpoint title, displayed as the header text above the API endpoint section. | +| `description` | String | API endpoint description, displayed inside each API endpoint section. | +| `summary` | String | API endpoint summary, displayed beside the API endpoint path. | + +### Parameters + +| Parameter | Default Value | +| :------------------------- | :------------------------------------------------------------------------------------------ | +| `docs.devices.title` | Devices | +| `docs.devices.description` | List of all devices/locations with associated identifiers, display names, networks, & VRFs. | +| `docs.devices.summary` | Devices List | +| `docs.info.title` | System Information | +| `docs.info.description` | General information about this looking glass. | +| `docs.info.summary` | System Information | +| `docs.queries.title` | Supported Queries | +| `docs.queries.description` | List of supported query types. | +| `docs.queries.summary` | Query Types | +| `docs.query.title` | Supported Query | +| `docs.query.description` | Request a query response per-location. | +| `docs.query.summary` | Query the Looking Glass | + +#### Example with Defaults + +```yaml filename="config.yaml" +docs: + base_url: https://lg.example.com + enable: true + mode: redoc + path: /api/docs + title: Beloved Hyperglass User Looking Glass API Documentation + description: null + # API Endpoints ↓ + devices: + title: Devices + description: List of all devices/locations with associated identifiers, display names, networks, & VRFs. + summary: Devices List + info: + title: System Information + description: General information about this looking glass. + summary: System Information + queries: + title: Supported Queries + description: List of supported query types. + summary: Query Types + query: + title: Supported Query + description: Request a query response per-location. + summary: Query the Looking Glass +``` diff --git a/docs/pages/configuration/config/caching.mdx b/docs/pages/configuration/config/caching.mdx new file mode 100644 index 000000000..839b8e1cf --- /dev/null +++ b/docs/pages/configuration/config/caching.mdx @@ -0,0 +1,16 @@ +## Cache + +hyperglass automatically caches responses to reduce the number of times devices are queried for the same information. + +| Parameter | Type | Default Value | Description | +| :---------------- | :------ | :------------ | :------------------------------------------------------------------------------ | +| `cache.timeout` | Number | 120 | Number of seconds for which to cache device responses. | +| `cache.show_text` | Boolean | True | If true, an indication that a user is viewing cached information will be shown. | + +### Example with Defaults + +```yaml filename="config.yaml" +cache: + timeout: 120 + show_text: true +``` diff --git a/docs/pages/configuration/config/logging.mdx b/docs/pages/configuration/config/logging.mdx new file mode 100644 index 000000000..5f1e83477 --- /dev/null +++ b/docs/pages/configuration/config/logging.mdx @@ -0,0 +1,90 @@ +## Logging + +Console, file, HTTP, and/or syslog logging configuration. + +| Parameter | Type | Default Value | Description | +| :---------- | :----- | :------------ | :---------------------------------------------- | +| `directory` | String | /tmp | Path to directory where logs will be created. | +| `format` | String | text | Log text format, must be `text` or `json`. | +| `max_size` | String | 50MB | Maximum log file size before being overwritten. | + +### Syslog + +| Parameter | Type | Default Value | Description | +| :-------- | :----- | :------------ | :---------------------- | +| `host` | String | | Syslog target host. | +| `port` | Number | 514 | Syslog target UDP port. | + +##### Syslog Example + +```yaml filename="config.yaml" copy +logging: + syslog: + host: log.example.com + port: 514 +``` + +### HTTP Logging + +If enabled, logs will be sent by HTTP `POST` method. + +| Parameter | Type | Default Value | Description | +| :----------- | :------ | :------------ | :---------------------------------------------- | +| `provider` | String | generic | Must be `generic`, `msteams`, or `slack`. | +| `host` | String | | URL | +| `headers` | Map | | | +| `params` | Map | | | +| `verify_ssl` | Boolean | true | Enable or disable SSL certificate verification. | +| `timeout` | Number | 5 | HTTP connection timeout in seconds. | + +#### Authentication + +Authentication is supported using HTTP basic authentication or an API key. + +| Parameter | Type | Default Value | Description | +| :----------------------------- | :----- | :------------ | :--------------------------------------------------------------------------------------------------------- | +| `http.authentication.mode` | String | basic | Must be `basic` or `api_key`. | +| `http.authentication.username` | String | | Basic authentication username if `mode` is set to `basic`. | +| `http.authentication.password` | String | | Basic authentication password if `mode` is set to `basic`, or API key value if `mode` is set to `api_key`. | +| `http.authentication.header` | String | X-API-Key | Header name if `mode` is set to `api_key`. | + +#### Examples + +##### Generic + +```yaml filename="config.yaml" copy +logging: + http: + provider: generic + host: https://httpbin.org + headers: + x-special-header: super special header value + params: + source: hyperglass + verify_ssl: true + timeout: 5 + authentication: + mode: basic + username: your username + password: super secret password +``` + +In the above example, hyperglass will send a `POST` request to `https://httpbin.org?source=hyperglass` with Basic Authentication headers set. + +##### Microsoft Teams Webhook + +```yaml filename="config.yaml" copy +logging: + http: + provider: msteams + host: <MS Teams webhook URL> +``` + +##### Slack + +```yaml filename="config.yaml" copy +logging: + http: + provider: slack + host: <Slack webhook URL> +``` diff --git a/docs/pages/configuration/config/messages.mdx b/docs/pages/configuration/config/messages.mdx new file mode 100644 index 000000000..135a788b9 --- /dev/null +++ b/docs/pages/configuration/config/messages.mdx @@ -0,0 +1,23 @@ +## Message Customization + +hyperglass provides as much control over user-facing text/messages as possible. The following messages may be adjusted as needed: + +| Parameter | Type | Default Value | Description | +| :------------------------------ | :----- | :------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `messages.authentication_error` | String | Authentication error occurred. | Displayed when hyperglass is unable to authenticate to a device. Usually, this indicates a configuration error. | +| `messages.connection_error` | String | Error connecting to \{device_name\}: \{error\} | Displayed when hyperglass is unable to connect to a device. Usually, this indicates a configuration error. `{device_name}` and `{error}` will be used to display the device in question and the specific connection error. | +| `messages.general` | String | Something went wrong. | Displayed when errors occur that hyperglass didn't anticipate or handle correctly. Seeing this error message may indicate a bug in hyperglass. If you see this in the wild, try enabling [debug mode](#global) and review the logs to pinpoint the source of the error. | +| `messages.invalid_input` | String | \{target\} is not valid. | Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` will be used to display the invalid target. | +| `messages.invalid_query` | String | \{target\} is not a valid \{query_type\} target. | Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` and `{query_type}` may be used to display the invalid target and corresponding query type. | +| `messages.no_input` | String | \{field\} must be specified. | Displayed when a required field is not specified. `{field}` will be used to display the name of the field that was omitted. | +| `messages.no_output` | String | The query completed, but no matching results were found. | Displayed when hyperglass can connect to a device and execute a query, but the response is empty. | +| `messages.not_found` | String | \{type\} '\{name\}' not found. | Displayed when an object property does not exist in the configuration. `{type}` corresponds to a user-friendly name of the object type (for example, 'Device'), `{name}` corresponds to the object name that was not found. | +| `messages.request_timeout` | String | Request timed out. | Displayed when the [`request_timeout`](#global) time expires. | +| `messages.target_not_allowed` | String | \{target\} is not allowed. | Displayed when a query target is implicitly denied by a configured rule. `{target}` will be used to display the denied query target. | + +##### Example + +```yaml filename="config.yaml" +message: + general: Something with wrong. +``` diff --git a/docs/pages/configuration/config/structured-output.mdx b/docs/pages/configuration/config/structured-output.mdx new file mode 100644 index 000000000..df936253f --- /dev/null +++ b/docs/pages/configuration/config/structured-output.mdx @@ -0,0 +1,61 @@ +## Structured + +Devices that support responding to a query with structured or easily parsable data can have their response data placed into an easier to read table (or JSON, when using the REST API). Currently, the following platforms have structured data supported in hyperglass: + +- Arista EOS +- Juniper Junos + +When structured output is available, hyperglass checks the RPKI state of each BGP prefix returned using one of two methods: + +1. From the router's perspective +2. From the perspective of [Cloudflare's RPKI Service](https://rpki.cloudflare.com/) + +Additionally, hyperglass provides the ability to control which BGP communities are shown to the end user. + +| Parameter | Type | Default Value | Description | +| :----------------------------- | :-------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------- | +| `structured.rpki.mode` | String | router | Use `router` to use the router's view of the RPKI state (1 above), or `external` to use Cloudflare's view (2 above). | +| `structured.communities.mode` | String | deny | Use `deny` to deny any communities listed in `structured.communities.items`, or `permit` to _only_ permit communities listed. | +| `structured.communities.items` | List of Strings | | List of communities to match. | + +### RPKI Examples + +#### Show RPKI State from the Device's Perspective + +```yaml filename="config.yaml" copy {2} +structured: + rpki: + mode: router +``` + +#### Show RPKI State from a Public/External Perspective + +```yaml filename="config.yaml" copy {2} +structured: + rpki: + mode: external +``` + +### Community Filtering Examples + +#### Deny Listed Communities by Regex pattern + +```yaml filename="config.yaml" {5-6} +structured: + communities: + mode: deny + items: + - '^65000:1\d+$' # don't show any communities starting with 65000:1. 65000:1234 would be denied, but 65000:4321 would be permitted. + - "65000:2345" # don't show the 65000:2345 community. +``` + +#### Permit only Listed Communities + +```yaml filename="config.yaml" {5-6} +structured: + communities: + mode: permit + items: + - "^65000:.*$" # permit any communities starting with 65000, but no others. + - "1234:1" # permit only the 1234:1 community. +``` diff --git a/docs/pages/configuration/config/web-ui.mdx b/docs/pages/configuration/config/web-ui.mdx new file mode 100644 index 000000000..761344769 --- /dev/null +++ b/docs/pages/configuration/config/web-ui.mdx @@ -0,0 +1,294 @@ +import { Callout } from "nextra/components"; +import { Color } from "~/components/color"; + +## Web UI + +hyperglass provides extensive customization options for the look and feel of the web UI. + +| Parameter | Type | Default Value | Description | +| :-------------------------- | :----- | :------------ | :----------------------------------------------------------------------------------------------------------------------------------------- | +| `web.location_display_mode` | String | auto | Show the locations field as a `dropdown`, `gallery`. When using `auto`, if there are more than 5 location groups, `dropdown` will be used. | +| `web.custom_javascript` | String | | Path to a javascript file that will be loaded with the website. | +| `web.custom_html` | String | | Path to a custom HTML file that will be loaded with the website and rendered beneath the app. | + +### DNS over HTTPS + +[DNS over HTTPS](https://www.rfc-editor.org/rfc/rfc8484) is used to look up an FQDN query target from the perspective of the user's browser. + +| Parameter | Type | Default Value | Description | +| :------------------ | :----- | :------------------------------------- | :-------------------------------------------------------------------------- | +| `dns_provider.name` | String | cloudflare | If `cloudflare` or `google` are provided, no URL is necessary. | +| `dns_provider.url` | String | `https://cloudflare-dns.com/dns-query` | Provide a custom DNS over HTTPS URL if you'd like to use your own resolver. | + +### Logo + +| Parameter | Type | Default Value | Description | +| :----------------- | :--------------- | :------------ | :------------------------------------------------- | +| `web.logo.light` | String | | Path to logo to show in light mode. | +| `web.logo.dark` | String | | Path to logo to show in dark mode. | +| `web.logo.favicon` | String | | Path to icon/logo from which to generate favicons. | +| `web.logo.width` | Number or String | 100% | Width of logo, either as pixels or a percentage. | +| `web.logo.height` | Number or String | | Height of logo, either as pixels or a percentage. | + +##### Example + +```yaml filename="config.yaml" copy +web: + logo: + light: /path/to/light-logo.svg + dark: /path/to/dark-logo.svg + favicon: /path/to/favicon.svg + width: 100% + height: null +``` + +### Greeting + +The greeting is an optional modal/popup window that will present itself to users in the Web UI. It can be used for anything you want; for example: + +- A data privacy acknowledgement that must be acknowledged prior to proceeding +- Information about your company +- Instructions on how to use hyperglass + +| Parameter | Type | Default Value | Description | +| :---------------------- | :------ | :------------ | :--------------------------------------------------------------------------------------------------- | +| `web.greeting.enable` | Boolean | False | Enable or disable the greeting. | +| `web.greeting.file` | String | | Path to markdown or plain text file that contains greeting content. | +| `web.greeting.title` | String | Welcome | Text to be used as the greeting title. | +| `web.greeting.button` | String | Continue | Text to be used for the button that acknowledges/closes the greeting. | +| `web.greeting.required` | Boolean | False | If `true`, users cannot close the modal or run any queries until the greeting has been acknowledged. | + +##### Example + +```yaml filename="config.yaml" copy +web: + greeting: + enable: true + file: /path/to/your/file.md + title: Welcome + button: Continue + required: false +``` + +### OpenGraph + +[OpenGraph](https://ogp.me/) is the thing that generates the pretty pictures, titles, and descriptions for links when you post them to sites/tools such as Facebook, Twitter, Slack, etc. By default, [this Opengraph image](/opengraph.jpg) is used, but you can provide your own image. You don't need to worry about sizing or formatting it properly, hyperglass will handle this for you. + +| Parameter | Type | Default Value | Description | +| :-------------------- | :----- | :------------ | :---------------------------- | +| `web.opengraph.image` | String | | Path to your OpenGraph image. | + +### Highlighting + +hyperglass can highlight special values in your router output and provide users with additional information about the content. For example, your organization's BGP communities or IP address space can be visually highlighted, and a tooltip can be shown when a user hovers over the highlighted value. + +Each value you wish to be highlighted is defined with the following schema: + +| Parameter | Type | Default Value | Description | +| :-------- | :----- | :------------ | :------------------------------------------------------ | +| `pattern` | String | | RegEx pattern or string to match against router output. | +| `label` | String | | Tooltip value when the highlighted text is hovered. | +| `color` | String | | Color name from [`web.theme.colors`](#colors). | + +##### Example + +```yaml filename="config.yaml" copy +web: + highlight: + - pattern: "65000:1234" + label: Special snowflake community that does a thing + color: primary + - pattern: '^192\.0\.2\.[0-9]+$' + label: Magical IP Address + color: blue +``` + +### Menus + +hyperglass can show completely-customizable menus in the footer. Each menu can be configured with the following schema: + +| Parameter | Type | Default Value | Description | +| :-------- | :----- | :------------ | :------------------------------------------------------------------------------------------------------------------------- | +| `title` | String | | Menu title, will be the text that shows on footer. | +| `content` | String | | Plain text or markdown content of the menu or path to a file that contains plain text or markdown content. | +| `side` | String | left | Side of the footer with which the menu will be grouped. | +| `order` | Number | 0 | Optionally specify an order for each menu item. If not specified, menus will be rendered in the order they are configured. | + +##### Example + +```yaml filename="config.yaml" copy +web: + menus: + - title: Terms & Conditions + content: + | **Don't** break stuff! + _please_ + side: right + order: 1 + - title: Help + content: /path/to/help/file.md + side: left + order: 0 +``` + +### Links + +hyperglass can show customizable links to anything you think your users might find helpful. Each link can be configured with the following schema: + +| Parameter | Type | Default Value | Description | +| :---------- | :------ | :------------ | :-------------------------------------------------------------------------------------------------------------------- | +| `title` | String | | Link text. | +| `url` | String | | Link URL. | +| `show_icon` | Boolean | True | If `true`, an icon indicating the link is an external link is shown. | +| `side` | String | left | Side of the footer with which the link will be grouped. | +| `order` | Number | 0 | Optionally specify an order for each link. If not specified, links will be rendered in the order they are configured. | + +##### Example + +```yaml filename="config.yaml" copy +web: + links: + - title: PeeringDB + url: https://www.peeringdb.com/65000 + show_icon: true + side: right + order: 1 + - title: Our Website + url: https://example.com + show_icon: false + side: left + order: 0 +``` + +### Credit + +| Parameter | Type | Default | Description | +| :-------- | :-----: | :-----: | :--------------------------------------------------------------------------------------- | +| `enable` | Boolean | `true` | Enable or disable the display of developer credit & link to hyperglass GitHub repository | + +<Callout emoji="❤️" type="info"> + **Note from the Developer** + <br /> If your organization's policy allows, and you don't mind, I request that you keep `credit` + enabled. Remember: my goal for this project is get more networks to use looking glasses to make all + of our lives easier. Because it's primarily other network operators who will use this tool to begin + with, I'd love for any operators that use your looking glass to know where they can get their own. +</Callout> + +### Text + +Most of the text in the hyperglass UI can be overridden to suit your needs. + +| Parameter | Type | Default Value | Description | +| :--------------------------- | :----- | :------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `web.text.title_mode` | String | logo_only | See [title mode section](#title-mode) | +| `web.text.title` | String | hyperglass | Title text. | +| `web.text.subtitle` | String | Network Looking Glass | Subtitle text. | +| `web.text.query_location` | String | Location | Text used to label the query location (router) field. | +| `web.text.query_type` | String | Query Type | Text used to label the query type (directive) field. | +| `web.text.query_target` | String | Target | Text used to label the query target field. | +| `web.text.fqdn_tooltip` | String | Use \{protocol\} | Tooltip text used when a user hovers over the FQDN tooltip. | +| `web.text.fqdn_message` | String | Your browser has resolved \{fqdn\} to | Text used when prompting a user to select a resolve IPv4 or IPv6 address for an FQDN query. | +| `web.text.fqdn_error` | String | Unable to resolve \{fqdn\} | Text used when an FQDN is not resolvable. | +| `web.text.fqdn_error_button` | String | Try Again | Button text used when an FQDN is not resolvable. | +| `web.text.cache_prefix` | String | Results cached for | Text displayed with the cache timeout countdown. | +| `web.text.cache_icon` | String | Cached from \{time\} UTC | Text used when a user hovers over the cache icon, which is displayed when a response was a cached response. `{time}` is replaced with the _original_ query's timestamp. | +| `web.text.complete_time` | String | Completed in \{seconds\} | Text used when a user hovers over the success icon for a query result. `{seconds}` will be replaced with 'n seconds' where n is the time a query took to complete. | +| `web.text.rpki_invalid` | String | Invalid | Text used in table output when a route's RPKI status is invalid. | +| `web.text.rpki_valid` | String | Valid | Text used in table output when a route's RPKI status is valid. | +| `web.text.rpki_unknown` | String | No ROAs Exist | Text used in table output when a route's RPKI status is unknown. | +| `web.text.rpki_unverified` | String | Not Verified | Text used in table output when a route's RPKI status is unverified. | +| `web.text.no_communities` | String | No Communities | Text used in table output when a route has no communities. | +| `web.text.ip_error` | String | Unable to determine IP Address | Error displayed if hyperglass is unable to determine the user's IP. | +| `web.text.no_ip` | String | No \{protocol\} Address | Text displayed if the user doesn't have an IP address of \{protocol\} (IPv4 or IPv6). | +| `web.text.ip_select` | String | Select an IP Address | Text used to label the IP Address selection for the user's IP. | +| `web.text.ip_button` | String | My IP | Text used for the user IP button. | + +#### Title Mode + +Available title modes are: + +| `title_mode` Value | Action | +| :----------------- | :------------------------------------------------------------------ | +| `logo_only` | Only the logo is displayed, no title or subtitle will be visible. | +| `text_only` | Only the title and subtitle are displayed, no logo will be visible. | +| `logo_subtitle` | Only the logo and subtitle are displayed, no title will be visible. | +| `all` | Logo, title, and subtitle will all be visible. | + +##### Example + +```yaml filename="config.yaml" +web: + text: + title: Our Looking Glass + subtitle: Company Name, Inc. + title_mode: text_only +``` + +### Theme + +hyperglass allows you to customize the colors and fonts used in the Web UI. + +| Parameter | Type | Default Value | Description | +| :----------------------------- | :----- | :------------ | :--------------------------------------------------------------------------------------------------------------- | +| `web.theme.default_color_mode` | String | | Set hyperglass's default color mode. By default, the user's system preference is used. Must be `light` or `dark` | + +#### Colors + +##### Intrinsic Colors + +| Parameter | Default Value | +| :------------------------ | :---------------------- | +| `web.theme.colors.black` | <Color hex="#000000" /> | +| `web.theme.colors.white` | <Color hex="#ffffff" /> | +| `web.theme.colors.dark` | <Color hex="#010101"/> | +| `web.theme.colors.light` | <Color hex="#f5f6f7"/> | +| `web.theme.colors.gray` | <Color hex="#c1c7cc"/> | +| `web.theme.colors.red` | <Color hex="#d84b4b"/> | +| `web.theme.colors.orange` | <Color hex="#ff6b35"/> | +| `web.theme.colors.yellow` | <Color hex="#edae49"/> | +| `web.theme.colors.green` | <Color hex="#35b246"/> | +| `web.theme.colors.blue` | <Color hex="#314cb6"/> | +| `web.theme.colors.teal` | <Color hex="#35b299"/> | +| `web.theme.colors.cyan` | <Color hex="#118ab2"/> | +| `web.theme.colors.pink` | <Color hex="#f2607d"/> | +| `web.theme.colors.purple` | <Color hex="#8d30b5"/> | + +##### Functional Colors + +| Parameter | Default Value | +| :--------------------------- | :--------------------- | +| `web.theme.colors.primary` | <Color hex="#118ab2"/> | +| `web.theme.colors.secondary` | <Color hex="#314cb6"/> | +| `web.theme.colors.success` | <Color hex="#35b246"/> | +| `web.theme.colors.warning` | <Color hex="#edae49"/> | +| `web.theme.colors.error` | <Color hex="#ff6b35"/> | +| `web.theme.colors.danger` | <Color hex="#d84b4b"/> | + +###### Example + +To override hyperglass's primary color, it's recommended to override its mapped intrinsic color. For example, to override the default primary color to <Color hex="#00ffff" noText /> cyan: + +```yaml filename="config.yaml" +web: + theme: + colors: + cyan: "#00ffff" +``` + +#### Fonts + +hyperglass's fonts are loaded from [Google Fonts](https://fonts.google.com/). Any Google font name may be specified to override the default fonts. + +| Parameter | Type | Default Value | Description | +| :--------------------- | :----- | :------------ | :--------------------------------------------------------------------------------------- | +| `web.theme.fonts.body` | String | Nunito | Font for all standard body text, including headings. | +| `web.theme.fonts.mono` | String | Fire Code | Font for all monospace text such as inline code or code blocks, including device output. | + +##### Example + +```yaml filename="config.yaml" +web: + theme: + fonts: + body: Inter +``` diff --git a/docs/pages/configuration/devices.mdx b/docs/pages/configuration/devices.mdx new file mode 100644 index 000000000..f30799a95 --- /dev/null +++ b/docs/pages/configuration/devices.mdx @@ -0,0 +1,135 @@ +import { Callout } from "nextra/components"; +import { SupportedPlatforms } from "~/components/platforms"; +import { DocsButton } from "~/components/docs-button"; + +## Device Configuration Parameters + +Each configured device may have the following parameters: + +| Parameter | Type | Default Value | Description | +| :------------------ | :-------------- | :------------ | :----------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | String | | Display name of the device. | +| `description` | String | | Description of the device, displayed as a subtle label. | +| `avatar` | String | | Path to an avatar/logo image for this site. Used when [`web.location_display_mode`](/configuration/config/web-ui.mdx) is set to `gallery`. | +| `address` | String | | IPv4 address, IPv6 address, or hostname of the device. | +| `group` | String | | Group name, used to visually group devices in the UI. | +| `port` | Number | | TCP port on which to connect to the device. | +| `platform` | String | | Device platform/OS. Must be a [supported platform](/platforms.mdx). | +| `structured_output` | Boolean | True | Disable structured output for a device that supports it. | +| `directives` | List of Strings | | Enable referenced directives configured in the [directives config file](/configuration/directives.mdx). | +| `driver` | String | netmiko | Specify which driver to use for this device. Currently, only `netmiko` is supported. | +| `driver_config` | Mapping | | Mapping/dict of options to pass to the connection driver. | +| `attrs` | Mapping | | Mapping/dict of variables, as referenced in configured directives. | +| `credential` | Mapping | | Mapping/dict of a [credential configuration](/configuration/devices/credentials.mdx). | +| `http` | Mapping | | Mapping/dict of [HTTP client options](/configuration/devices/http-device.mdx), if this device is connected via HTTP. | +| `proxy` | Mapping | | Mapping/dict of [SSH proxy config](/configuration/devices/ssh-proxy.mdx) to use for this device's requests. | + +<Callout type="tip"> + +hyperglass ships with predefined [directives](/configuration/directives.mdx) (commands) for the following [platforms](platforms.mdx): + +<SupportedPlatforms /> + +All built in directives require that the following `attrs` be defined on each device using the directive: + +| Attribute | Value | +| :-------- | :-------------------------------------------------------- | +| `source4` | IPv4 address used to source Ping and Traceroute commands. | +| `source6` | IPv6 address used to source Ping and Traceroute commands. | + +**Example** + +```yaml filename="devices.yaml" {5-7} copy +devices: + - name: New York, NY + address: 192.0.2.1 + platform: cisco_ios + attrs: + source4: 192.0.2.1 + source6: "2001:db8::1" +``` + +</Callout> + +<Callout type="warning"> + If you do not utilize IPv6 in your network, you'll need to create your own directive that only + has IPv4 commands. +</Callout> + +## Examples + +### Simple + +```yaml filename="devices.yaml" copy +devices: + - name: New York, NY + address: 192.0.2.1 + platform: cisco_ios + credential: + username: you + password: your password + - name: San Francisco, CA + address: 192.0.2.2 + platform: juniper + credential: + username: you + password: your password +``` + +<h3 className="_font-semibold _tracking-tight _text-slate-900 dark:_text-slate-100 _mt-8 _text-2xl"> + {" "} + <DocsButton href="/configuration/directives" /> With Directives +</h3> + +In this example, an additional directive `cisco-show-lldp-neighbors` is added to the built-in directives. + +```yaml filename="devices.yaml" copy {8-9} +devices: + - name: New York, NY + address: 192.0.2.1 + platform: cisco_ios + credential: + username: you + password: your password + directives: + - cisco-show-lldp-neighbors +``` + +<h3 className="_font-semibold _tracking-tight _text-slate-900 dark:_text-slate-100 _mt-8 _text-2xl"> + {" "} + <DocsButton href="/configuration/directives" /> Disable Built-in Directives +</h3> + +In this example, _only_ the `cisco-show-lldp-neighbors` directive will be available. Built-in directives are disabled. + +```yaml filename="devices.yaml" copy {8-10} +devices: + - name: New York, NY + address: 192.0.2.1 + platform: cisco_ios + credential: + username: you + password: your password + directives: + - builtin: false + - cisco-show-lldp-neighbors +``` + +<h3 className="_font-semibold _tracking-tight _text-slate-900 dark:_text-slate-100 _mt-8 _text-2xl"> + {" "} + <DocsButton href="/configuration/directives" /> Enable Specifc Built-in Directives +</h3> + +In this example, only specified built-in directives are made available. + +```yaml filename="devices.yaml" copy {8-9} +devices: + - name: New York, NY + address: 192.0.2.1 + platform: cisco_ios + credential: + username: you + password: your password + directives: + - builtin: [bgp_route, traceroute] +``` diff --git a/docs/pages/configuration/devices/_meta.tsx b/docs/pages/configuration/devices/_meta.tsx new file mode 100644 index 000000000..cc954902e --- /dev/null +++ b/docs/pages/configuration/devices/_meta.tsx @@ -0,0 +1,5 @@ +export default { + credentials: "Credentials", + "http-device": "HTTP Device", + "ssh-proxy": "SSH Proxy", +}; diff --git a/docs/pages/configuration/devices/credentials.mdx b/docs/pages/configuration/devices/credentials.mdx new file mode 100644 index 000000000..463ff68f3 --- /dev/null +++ b/docs/pages/configuration/devices/credentials.mdx @@ -0,0 +1,35 @@ +Each device must be configured with credentials with which hyperglass can log into the device and execute commands. + +## Credential Configuration + +| Parameter | Type | Default Value | Description | +| :-------------------- | :----- | :------------ | :----------------------------------------------------- | +| `credential.username` | String | | Username to use for authentication to the device. | +| `credential.password` | String | | Password to use for authentication to the device. | +| `credential.key` | String | | Path to SSH key used for authentication to the device. | + +### Examples + +#### Username & Password + +```yaml filename="devices.yaml" copy {5-7} +devices: + - name: New York, NY + address: 192.0.2.1 + platform: cisco_ios + credential: + username: you + password: your password +``` + +#### SSH Private Key + +```yaml filename="devices.yaml" copy {5-7} +devices: + - name: San Francisco, CA + address: 192.0.2.2 + platform: juniper + credential: + username: you + key: /path/to/your/ssh/key +``` diff --git a/docs/pages/configuration/devices/http-device.mdx b/docs/pages/configuration/devices/http-device.mdx new file mode 100644 index 000000000..ce14abe59 --- /dev/null +++ b/docs/pages/configuration/devices/http-device.mdx @@ -0,0 +1,85 @@ +hyperglass supports collecting output from a generic HTTP endpoint. + +## HTTP Configuration + +| Parameter | Type | Default Value | Description | +| :---------------------- | :------ | :------------ | :--------------------------------------------------------------------------------------------------------------------- | +| `http.attribute_map` | Mapping | | Mapping/dict of hyperglass query fields as keys, and hyperglass query field replacements as values. | +| `http.basic_auth` | Mapping | | If basic authentication is required, provide a mapping/dict containing the basic authentication username and password. | +| `http.body_format` | String | json | Body format, options are `json` `yaml` `xml` `text` | +| `http.follow_redirects` | Boolean | `false` | Follow HTTP redirects from server. | +| `http.headers` | Mapping | | Mapping/dict of http headers to append to requests. | +| `http.method` | String | GET | HTTP method to use for requests. | +| `http.path` | String | / | HTTP URI/Path. | +| `http.query` | Mapping | | Mapping/Dict of URL Query Parameters. | +| `http.retries` | Number | 0 | Number of retries to perform before request failure. | +| `http.scheme` | String | https | HTTP schema, must be `http` or `https` | +| `http.source` | String | | Request source IP address. | +| `http.ssl_ca` | String | | Path to SSL CA certificate file for SSL validation. | +| `http.ssl_client` | String | | Path to client SSL certificates for request. | +| `http.timeout` | Number | 5 | Request timeout in seconds. | +| `http.verify_ssl` | Boolean | `true` | If `false`, invalid certificates for HTTPS hosts will be ignored. | + +### Example + +#### Basic + +The following example will send an HTTP POST request to `https://192.0.2/path/to/query/device` with HTTP basic authentication, and will not verify the SSL certificate. + +```yaml filename="devices.yaml" copy +devices: + - name: New York, NY + address: 192.0.2.1 + http: + path: /path/to/query/device + method: POST + verify_ssl: false + basic_auth: + username: you + password: your password +``` + +Given the following hyperglass query: + +| Field | Value | +| :------------- | :------------------ | +| Query Target | `192.0.2.0/24` | +| Query Location | `your_location` | +| Query Type | `example_directive` | + +The body of the request will be: + +```json +{ + "query_target": "192.0.2.0/24", + "query_location": "your_location", + "query_type": "example_directive" +} +``` + +#### Non-HTTPS Request + +The following example will send an HTTP GET request to `http://192.0.2.1/path/to/query/device`: + +```yaml filename="devices.yaml" {6} copy +devices: + - name: New York, NY + address: 192.0.2.1 + http: + path: /path/to/query/device + scheme: http +``` + +#### Header Authentication + +The following example will send an HTTP GET request to `https://192.0.2.1/path/to/query/device` with an `Authorization` header: + +```yaml filename="devices.yaml" {6-7} copy +devices: + - name: New York, NY + address: 192.0.2.1 + http: + path: /path/to/query/device + headers: + Authorization: your special token +``` diff --git a/docs/pages/configuration/devices/ssh-proxy.mdx b/docs/pages/configuration/devices/ssh-proxy.mdx new file mode 100644 index 000000000..99d19f1ec --- /dev/null +++ b/docs/pages/configuration/devices/ssh-proxy.mdx @@ -0,0 +1,30 @@ +In cases where access to the devices is secured behind a "jump box" or other intermediary server/device, hyperglass can use SSH local port forwarding to SSH to an intermedary device first, and then to the device. + +## SSH Proxy Configuration + +| Parameter | Type | Default Value | Description | +| :----------------- | :------ | :------------ | :----------------------------------------------------------------------------------- | +| `proxy.address` | String | | IPv4 address, IPv6 address, or hostname of SSH proxy. | +| `proxy.port` | Number | 22 | TCP port to use for connecting to the SSH proxy. | +| `proxy.platform` | String | linux_ssh | Currently, only `linux_ssh` is supported. | +| `proxy.credential` | Mapping | | Mapping/dict of a [credential configuration](/configuration/config/credentials.mdx). | + +### Examples + +#### Use an SSH Proxy When Connecting to a Device + +```yaml filename="devices.yaml" copy +devices: + - name: New York, NY + address: 192.0.2.1 + credential: + username: you + password: your password + proxy: + address: 203.0.113.1 + credential: + username: your proxy username + password: your proxy password +``` + +In the above example, the credentials `your proxy username`/`your proxy password` will be used to authenticate from the hyperglass server to the SSH proxy, and the credentials `you`/`your password` will be used to authentiate from the SSH proxy to the device. diff --git a/docs/pages/configuration/directives.mdx b/docs/pages/configuration/directives.mdx new file mode 100644 index 000000000..9da38126e --- /dev/null +++ b/docs/pages/configuration/directives.mdx @@ -0,0 +1,188 @@ +import { Callout } from "nextra/components"; + +## What is a directive? + +<Callout type="tip"> + +A <strong style={{color: "#ff5e5b"}}>directive</strong> is a defined configuration for one or more **commands** to run on a device. For example, a BGP Route query is a built-in directive. A directive defines: + +- What command (or commands) to run on the device +- Type of UI field, text input or select +- If the field can accept multiple values +- Help information to show about the directive +- Validation rules + +</Callout> + +Each directive has the following options: + +| Parameter | Type | Default Value | Description | +| :------------------- | :-------------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | String | | Display name of the directive. | +| `rules` | List of Rules | | List of [rule configs](#rules) | +| `field` | Mapping | | Mapping/dict of [fields config](#fields) | +| `info` | String | | File path to markdown-formatted help information about the directive. | +| `plugins` | List of Strings | | List of plugin names to use with this directive. | +| `groups` | List of Strings | | List of names by which directives are grouped in the UI. | +| `multiple` | Boolean | `false` | Command supports receiving multiple values. For example, Cisco IOS's `show ip bgp community` accepts multiple communities as arguments. | +| `multiple_separator` | String | `" "` | String by which multiple values are separated. For example, a list of values `[65001, 65002, 65003]` would be rendered as `65001 65002 65003` for when the command is run. | + +## Rules + +A rule is a way of saying "if a query target matches the rule's conditions, run this command". + +| Parameter | Type | Default Value | Description | +| :---------- | :-------------- | :------------ | :--------------------------------------------------------------------------------------------- | +| `condition` | String | | Regular expression to match or IP prefix in which the value being evaluated must be contained. | +| `action` | String | permit | `permit` or `deny` the directive target when this rule is matched. | +| `commands` | List of Strings | | Commands to run when this rule matches. `{target}` is replaced with the query target. | + +### IP Rule + +| Parameter | Type | Default Value | Description | +| :------------------ | :------ | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ge` | Number | 0 | Prefix length greater than defined will be matched. | +| `le` | Number | 32,128 | `32` for IPv4 evaluations, `128` for IPv6 evaluations. Prefix length less than defined will be matched. | +| `allow_reserved` | Boolean | `false` | Allow reserved ([RFC1918](https://www.rfc-editor.org/rfc/rfc1918), [RFC5735](https://www.rfc-editor.org/rfc/rfc5735), [RFC5737](https://www.rfc-editor.org/rfc/rfc5737.html), etc.) addresses to pass validation. | +| `allow_unspecified` | Boolean | `false` | Allow unspecified addresses (`0.0.0.0` `::`) to pass validation. | +| `allow_loopback` | Boolean | `false` | Allow [loopback addresses](https://www.rfc-editor.org/rfc/rfc1700.html) (`127.0.0.0/8` `::1`) to pass validation. | + +#### Examples + +##### Require IPv4 Queries between /8 and /24 + +```yaml filename="directives.yaml" {5-6} /{target} {mask}/ copy +your-directive: + name: IP Route + rules: + - condition: 0.0.0.0/0 + ge: 8 + le: 24 + command: "show ip route {target} {mask}" +``` + +Given a query target of 198.18.0.0/15, the command run on the device would be: + +```none /198.18.0.0 255.254.0.0/ +show ip route 198.18.0.0 255.254.0.0 +``` + +##### Deny a Specific Prefix + +```yaml filename="directives.yaml" {5} copy +your directive: + name: BGP Route + rules: + - condition: "192.0.2.0/24" + action: deny + - condition: "0.0.0.0/0" + command: "show ip bgp {target}" +``` + +In this example, a query of any IP address or prefix contained within 192.0.2.0/24 will result in an error. + +##### Run Multiple Commands + +```yaml filename="directives.yaml" /{target}/ {6-7} copy +your-directive: + name: BGP Communities + rules: + - condition: "65000:[0-9]+" + commands: + - "show route table inet.0 community {target} detail" + - "show route table inet6.0 community {target} detail" +``` + +In this example, a query of `65000:1` would result in the following commands being sent to the device: + +```none /65000:1/ +show route table inet.0 community 65000:1 detail +show route table inet6.0 community 65000:1 detail +``` + +The output for both commands will be shown as the query result. + +### Regex Validation + +To validate input by regex pattern, just specify a regex pattern as the `condition` + +```yaml filename="directives.yaml" copy +your-directive: + name: DNS Query + rules: + - condition: '^.+\.yourdomain\.com$' +``` + +### No Validation + +```yaml filename="directives.yaml" /{target}/ copy {4} +your-directive: + name: IP Route + rules: + - condition: null + command: show ip route {target} +``` + +In this example, any query would pass, regardless of query input. For instance, if a user selected this directive/query type and queried `your mom`, the real command sent to the device will be: + +```none /your mom/ +show ip route your mom +``` + +## Fields + +### Text Input + +| Parameter | Type | Default Value | Description | +| :------------ | :----- | :------------ | :---------------------------------------------------- | +| `description` | String | | Field description, displayed as a label or help text. | +| `validation` | String | | Regex pattern to validate text input. | + +### Select + +| Parameter | Type | Default Value | Description | +| :------------ | :-------------- | :------------ | :---------------------------------------------------- | +| `description` | String | | Field description, displayed as a label or help text. | +| `options` | List of Options | | | + +#### Options + +Each select option uses the following schema: + +| Parameter | Type | Default Value | Description | +| :------------ | :----- | :------------ | :------------------------------------------------------- | +| `description` | String | | Field description, displayed as a label or help text. | +| `name` | String | | If specified, will be used as the option's display name. | +| `value` | String | | Option value sent to the device. | + +### Examples + +Example of a text directive expecting a string value matching a regex pattern: + +```yaml filename="directives.yaml" copy {6-7} +your-directive: + name: IP Route + rules: + - condition: null + command: show ip route {target} + field: + description: IP of target + validation: '[0-9a-f\.\:]+' +``` + +Example of a select directive: + +```yaml filename="directives.yaml" copy {6-12} +your-directive: + name: BGP Community + rules: + - condition: null + command: show ip bgp community {target} + field: + description: BGP community to show + options: + - value: "65001:1" + description: Provider A Routes + - value: "65001:2" + description: Provider B Routes +``` diff --git a/docs/pages/configuration/examples/_meta.tsx b/docs/pages/configuration/examples/_meta.tsx new file mode 100644 index 000000000..d4151247d --- /dev/null +++ b/docs/pages/configuration/examples/_meta.tsx @@ -0,0 +1,5 @@ +export default { + "basic-configuration": "Basic Configuration", + "add-your-own-command": "Add Your Own Command", + "customize-the-ui": "Customize the UI", +}; diff --git a/docs/pages/configuration/examples/add-your-own-command.mdx b/docs/pages/configuration/examples/add-your-own-command.mdx new file mode 100644 index 000000000..a8f588ae8 --- /dev/null +++ b/docs/pages/configuration/examples/add-your-own-command.mdx @@ -0,0 +1,60 @@ +import { Steps } from "nextra/components"; + +## Add Your Own Command + +While hyperglass does come with several built-in [directives](/configuration/configuration/directives.mdx) (commands), you can also add your own. For example, say you want to add a command that shows the BGP summary from a device: + +<Steps> + +### Create the Directive + +```yaml filename="directives.yaml" copy +show-bgp-summary: + name: BGP Summary + rules: + - condition: null + command: show bgp all summary + field: null +``` + +### Associate the Directive with the Device + +```yaml filename="devices.yaml" {5-6} copy +devices: + - name: Your Router + address: 192.0.2.1 + platform: cisco_ios + directives: + - show-bgp-summary +``` + +</Steps> + +## Default Directives + +By default, all built-in directives are _also_ enabled. If you wish to _only_ enable directives you specify, you can use `builtins: false` as a directive: + +```yaml filename="devices.yaml" {6-7} copy +devices: + - name: Your Router + address: 192.0.2.1 + platform: cisco_ios + directives: + - builtins: false + - show-bgp-summary +``` + +In the above example, _only_ the `show-bgp-summary` directive will be enabled. + +You can also selectively enable certain built-in directives: + +```yaml filename="devices.yaml" {6} copy +devices: + - name: Your Router + address: 192.0.2.1 + platform: cisco_ios + directives: + - builtins: [bgp_route, traceroute] +``` + +In the above example, _only_ the BGP Route and Traceroute built-in directives will be enabled. diff --git a/docs/pages/configuration/examples/basic-configuration.mdx b/docs/pages/configuration/examples/basic-configuration.mdx new file mode 100644 index 000000000..c7e64c504 --- /dev/null +++ b/docs/pages/configuration/examples/basic-configuration.mdx @@ -0,0 +1,30 @@ +--- +title: Basic Configuration +description: Get started with a basic hyperglass configuration +--- + +import { Callout } from "nextra/components"; + +To get started, hyperglass only needs to know about your devices. + +<Callout type="info"> + **Devices** are your routers, switches, or whatever else you want to call the endpoints + hyperglass will query for information. +</Callout> + +## Simple Device Configuration + +```yaml filename="devices.yaml" +devices: + - name: NYC Router 1 + address: <IP address hyperglass will use to log into the router> + credential: + username: <Username hyperglass will use to log into the router> + password: <Password hyperglass will use to log into the router> + platform: cisco_ios + attrs: + source4: <IPv4 address from which to source ping & traceroute commands> + source6: <IPv4 address from which to source ping & traceroute commands> +``` + +That's it! diff --git a/docs/pages/configuration/examples/customize-the-ui.mdx b/docs/pages/configuration/examples/customize-the-ui.mdx new file mode 100644 index 000000000..24e564abb --- /dev/null +++ b/docs/pages/configuration/examples/customize-the-ui.mdx @@ -0,0 +1,60 @@ +--- +description: Customize hyperglass to fit your needs. +--- + +import { DocsButton } from "~/components/docs-button"; + +<h3 className="_font-semibold _tracking-tight _text-slate-900 dark:_text-slate-100 _mt-8 _text-2xl"> + {" "} + <DocsButton href="/configuration/config" /> Change the Title and Organization Name +</h3> + +```yaml filename="config.yaml" +site_title: Our super neat looking glass +org_name: Cool Company +``` + +<h3 className="_font-semibold _tracking-tight _text-slate-900 dark:_text-slate-100 _mt-8 _text-2xl"> + {" "} + <DocsButton href="/configuration/config/web-ui#logo" /> Change the Logo +</h3> + +```yaml filename="config.yaml" {2-4} copy +web: + logo: + light: <path to logo image file to use in light mode> + dark: <path to logo image file to use in dark mode> +``` + +<h3 className="_font-semibold _tracking-tight _text-slate-900 dark:_text-slate-100 _mt-8 _text-2xl"> + {" "} + <DocsButton href="/configuration/config/web-ui#theme" /> Change the Color Scheme +</h3> + +```yaml filename="config.yaml" copy {3-5} +web: + theme: + colors: + primary: "#d84b4b" + secondary: "#118ab2" +``` + +<h3 className="_font-semibold _tracking-tight _text-slate-900 dark:_text-slate-100 _mt-8 _text-2xl"> + {" "} + <DocsButton href="/configuration/config/web-ui#menus" /> Add a Link to the Footer +</h3> + +```yaml filename="config.yaml" copy +web: + links: + - title: PeeringDB + url: https://www.peeringdb.com/65000 + show_icon: true + side: right + order: 1 + - title: Our Website + url: https://example.com + show_icon: false + side: left + order: 0 +``` diff --git a/docs/pages/configuration/overview.mdx b/docs/pages/configuration/overview.mdx new file mode 100644 index 000000000..c90c4dcf1 --- /dev/null +++ b/docs/pages/configuration/overview.mdx @@ -0,0 +1,76 @@ +import { Code, Table, Td, Th, Tr, Callout } from "nextra/components"; +import { SupportedPlatforms } from "~/components/platforms"; + +Once you've gotten started with a basic configuration, you'll probably want to customize the look and feel of hyperglass by changing the logo or color scheme. Fortunately, there are _a lot_ ways to customize hyperglass. + +## Configuration Files + +| File Name | Docs | Purpose | +| :----------- | :---------------------------------------------------: | :------------------------------------------------------------------------- | +| `config` | [Config File Docs](/configuration/config.mdx) | Application-wide configuration such as logging, web UI customization, etc. | +| `devices` | [Devices File Docs](/configuration/devices.mdx) | Your devices and their associated configurations. | +| `directives` | [Directives File Docs](/configuration/directives.mdx) | Custom directives (commands). | + +<Callout type="info"> + **File Extensions** <br /> + All the examples in the docs are provided in [YAML](https://yaml.org/) format, but [TOML](https://toml.io/), + JSON, and Python files are also supported. +</Callout> + +### Using a Python File + +When using a Python file for a hyperglass configuration, one of the following methods may be used: + +#### Define a Function Named `main` + +```python filename="Example: Using a Python function to define configuration parameters" +def main(): + return { + "org_name": "Your Organization Name", + "web": { + "theme": { + "colors": { + "blue": "#0000ff", + } + } + } + } + +# The main function can also be an async function. +async def main(): + config = await some_function_to_fetch_config() + return config +``` + +#### Define a Dictionary Named `main` + +```python filename="Example: Using a Python dictionary to define configuration parameters" +main = { + "org_name": "Your Organization Name", + "web": { + "theme": { + "colors": { + "blue": "#0000ff", + } + } + } + } +``` + +## Built-in Directives + +hyperglass ships with predefined [directives](/configuration/directives.mdx) for the following [platforms](platforms.mdx): + +<SupportedPlatforms /> + +All built in directives require that the following `attrs` be defined on each device using the directive: + +| Attribute | Value | +| :-------- | :-------------------------------------------------------- | +| `source4` | IPv4 address used to source Ping and Traceroute commands. | +| `source6` | IPv6 address used to source Ping and Traceroute commands. | + +<Callout type="warning"> + If you do not utilize IPv6 in your network, you'll need to create your own directive that only + has IPv4 commands. +</Callout> diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx new file mode 100644 index 000000000..d12892f8c --- /dev/null +++ b/docs/pages/index.mdx @@ -0,0 +1,35 @@ +--- +title: Introduction +description: Get started with hyperglass +--- + +import { Cards } from "nextra/components"; +import { SupportedPlatforms } from "~/components/platforms"; + +## What is hyperglass? + +<strong style={{ color: "#ff5e5b" }}>hyperglass</strong> is an open source network looking glass written +by a network engineer for other network engineers. The purpose of a looking glass is to provide customers, +peers, and complete strangers with unattended visibility into the an operator's network. + +hyperglass was created with the lofty goal of benefiting the internet community at-large by providing a faster, easier, and more secure way for operators to provide looking glass services to their customers, peers, and other network operators. + +## Features + +- BGP Route, BGP Community, BGP AS Path, Ping, & Traceroute +- Full IPv6 support +- Customizable everything: features, theme, UI/API text, error messages, commands +- Built in support for: + <SupportedPlatforms /> +- Configurable support for any other [supported platform](platforms.mdx) +- Optionally access devices via an SSH proxy/jump server +- VRF support +- Access List/prefix-list style query control to whitelist or blacklist query targets on a per-VRF basis +- REST API with automatic, configurable OpenAPI documentation +- Modern, responsive UI +- Query multiple devices simultaneously +- Browser-based DNS-over-HTTPS resolution of FQDN queries + +<Cards> + <Cards.Card title="Get Started" href="installation/docker" arrow /> +</Cards> diff --git a/docs/pages/installation.mdx b/docs/pages/installation.mdx new file mode 100644 index 000000000..0eb736a47 --- /dev/null +++ b/docs/pages/installation.mdx @@ -0,0 +1,11 @@ +import { Cards } from "nextra/components"; + +<Cards> + <Cards.Card href="installation/docker" title="Using Docker" /> + <Cards.Card href="installation/manual" title="Manual Installation" /> +</Cards> + +<Cards> + <Cards.Card href="installation/environment-variables" title="Environment Variables" /> + <Cards.Card href="installation/reverse-proxy" title="Reverse Proxy" /> +</Cards> diff --git a/docs/pages/installation/_meta.tsx b/docs/pages/installation/_meta.tsx new file mode 100644 index 000000000..6f531ec2a --- /dev/null +++ b/docs/pages/installation/_meta.tsx @@ -0,0 +1,7 @@ +export default { + docker: "Using Docker", + manual: "Manual Installation", + "environment-variables": "Environment Variables", + "reverse-proxy": "Reverse Proxy", + upgrading: "Upgrading hyperglass", +}; diff --git a/docs/pages/installation/docker.mdx b/docs/pages/installation/docker.mdx new file mode 100644 index 000000000..cab3e3152 --- /dev/null +++ b/docs/pages/installation/docker.mdx @@ -0,0 +1,60 @@ +--- +title: Using Docker +description: Installing hyperglass with Docker +--- + +import { Cards, Steps, Callout } from "nextra/components"; +// import { Callout } from "nextra-theme-docs"; + +<Callout type="info">**Docker is the recommended method for running hyperglass.**</Callout> + +<Steps> + +### Install Docker + +<Cards> + <Cards.Card + title="Docker Engine Installation Guide" + href="https://docs.docker.com/engine/install/" + target="_blank" + arrow + /> +</Cards> + +### Download hyperglass + +```shell copy +mkdir /etc/hyperglass +cd /opt +git clone https://github.com/thatmattlove/hyperglass.git --depth=1 +cd /opt/hyperglass +``` + +### Optional: Quickstart + +Do this if you just want to see the hyperglass page working with a fake device. + +```shell copy +cp /opt/hyperglass/.samples/sample_devices.yaml /etc/hyperglass/devices.yaml +cd /opt/hyperglass +docker compose up +``` + +Navigate to http://localhost:8001 + +### Create a `systemd` service + +<Callout type="info"> + Before you create and start the hyperglass service, you may want to verify whether or not you + intend to change any [environment variables](environment-variables.mdx) and change them first. +</Callout> + +```shell copy +cp /opt/hyperglass/.samples/hyperglass-docker.service /etc/hyperglass/hyperglass.service +ln -s /etc/hyperglass/hyperglass.service /etc/systemd/system/hyperglass.service +systemctl daemon-reload +systemctl enable hyperglass +systemctl start hyperglass +``` + +</Steps> diff --git a/docs/pages/installation/environment-variables.mdx b/docs/pages/installation/environment-variables.mdx new file mode 100644 index 000000000..64d39157e --- /dev/null +++ b/docs/pages/installation/environment-variables.mdx @@ -0,0 +1,22 @@ +--- +title: Environment Variables +description: hyperglass environment variables +--- + +There are some configuration variables that must be supplied before hyperglass can start or read its configuration files. Most of the time, you should not need to modify these. + +Environment variables may be overridden at the command line, or by placing them in `${HYPERGLASS_APP_PATH}/hyperglass.env`. + +| Variable Name | Type | Default | Description | +| :-------------------------- | :------ | :---------------- | :----------------------------------------------------------------------------------------------------------------- | +| `HYPERGLASS_DEBUG` | boolean | `false` | Enable debug logging | +| `HYPERGLASS_DEV_MODE` | boolean | `false` | Enable developer mode. This should only be used if you are developing hyperglass under specific circumstances. | +| `HYPERGLASS_DISABLE_UI` | boolean | `false` | If set to `true`, the hyperglass UI is not built or served. The only way to access hyperglass is via REST API. | +| `HYPERGLASS_APP_PATH` | string | `/etc/hyperglass` | Directory where hyperglass configuration files and static web UI files are contained. | +| `HYPERGLASS_REDIS_HOST` | string | `localhost` | Host on which Redis is running. | +| `HYPERGLASS_REDIS_PASSWORD` | string | — | Redis password, if any. | +| `HYPERGLASS_REDIS_DB` | number | `1` | Redis database number. | +| `HYPERGLASS_REDIS_DSN` | string | — | Redis DSN. If supplied, overrides `HYPERGLASS_REDIS_HOST`, `HYPERGLASS_REDIS_DB`, and `HYPERGLASS_REDIS_PASSWORD`. | +| `HYPERGLASS_HOST` | string | `[::1]` | Address on which hyperglass listens for requests. | +| `HYPERGLASS_PORT` | number | `8001` | TCP port on which hyperglass listens for requests. | +| `HYPERGLASS_CA_CERT` | string | — | Path to CA certificate file for validating HTTPS certificates. If not supplied, system CAs are used. | diff --git a/docs/pages/installation/manual.mdx b/docs/pages/installation/manual.mdx new file mode 100644 index 000000000..4c3cefc81 --- /dev/null +++ b/docs/pages/installation/manual.mdx @@ -0,0 +1,68 @@ +--- +title: Manual Installation +description: Installing hyperglass manually +--- + +import { Steps, Callout } from "nextra/components"; + +<Steps> + +### Install Dependencies + +To install hyperglass manually, you'll need to install the following dependencies: + +1. [Python 3.11, or 3.12](https://www.python.org/downloads/) and [`pip`](https://pip.pypa.io/en/stable/installation/) +2. [NodeJS 20.14 or later](https://nodejs.org/en/download) +3. [PNPM 9 or later](https://pnpm.io/installation) +4. [Redis 7.2 or later](https://redis.io/download/) + +<Callout type="warning">Make sure the Redis server is started.</Callout> + +### Install hyperglass + +Once these dependencies are installed, install hyperglass via PyPI: + +```shell copy +git clone https://github.com/thatmattlove/hyperglass --depth=1 +cd hyperglass +# optional - switch to the latest stable release +# git switch -c v2.0.4 v2.0.4 +pip3 install -e . +``` + +### Create app directory + +<Callout type="info"> + If you plan on using a different directory, be sure to set the directory you wish to use in your + [environment variables](environment-variables.mdx). +</Callout> + +```shell copy +mkdir /etc/hyperglass +``` + +### Optional: Quickstart + +Do this if you just want to see the hyperglass page working with default settings and a fake device. + +```shell copy +curl -o /etc/hyperglass/devices.yaml https://raw.githubusercontent.com/thatmattlove/hyperglass/main/.samples/sample_devices.yaml +hyperglass start +``` + +### Create a `systemd` service + +```shell copy +curl -o /etc/hyperglass/hyperglass.service https://raw.githubusercontent.com/thatmattlove/hyperglass/main/.samples/hyperglass-manual.service +ln -s /etc/hyperglass/hyperglass.service /etc/systemd/system/hyperglass.service +systemctl daemon-reload +systemctl enable hyperglass +systemctl start hyperglass +``` + +<Callout type="info"> + If you used a different app directory from the default `/etc/hyperglass`, change the + `EnvironmentFile` value in the `hyperglass.service` file. +</Callout> + +</Steps> diff --git a/docs/pages/installation/reverse-proxy.mdx b/docs/pages/installation/reverse-proxy.mdx new file mode 100644 index 000000000..aa821f617 --- /dev/null +++ b/docs/pages/installation/reverse-proxy.mdx @@ -0,0 +1,44 @@ +--- +title: Reverse Proxy +description: Setting up a reverse proxy for hyperglass +--- + +import { Cards, Callout } from "nextra/components"; + +[Caddy](https://caddyserver.com) is recommended, but any reverse proxy ([NGINX](https://www.nginx.com), [Apache2](https://httpd.apache.org)) will work. + +## Caddy + +<Cards> + <Cards.Card + title="Install Caddy" + target="_blank" + href="https://caddyserver.com/docs/install" + arrow + /> +</Cards> + +```shell copy +cp /opt/hyperglass/.samples/Caddyfile /etc/caddy/Caddyfile +``` + +Change the `lg.example.com` and `person@example.com` values to match your hyperglass FQDN and email address (the email address is for automatic SSL certificate generation via Let's Encrypt). + +<Callout type="info"> + If you prefer to use other Let's Encrypt validation methods or your own SSL certificate, modify + your `/etc/hyperglass/Caddyfile` in accordance with the [Caddy + docs](https://caddyserver.com/docs/caddyfile-tutorial). +</Callout> + +Restart the Caddy service: `systemctl restart caddy{:shell}` + +## NGINX + +```shell copy +cp /opt/hyperglass/.samples/hyperglass.nginx /etc/nginx/sites-available/hyperglass +ln -s /etc/nginx/sites-available/hyperglass /etc/nginx/sites-enabled/hyperglass +``` + +Change the `lg.example.com` value to match your hyperglass FQDN. + +Change the `<path to cert chain>` and `<path to key>` values to match the path to your certificate and private key files. diff --git a/docs/pages/installation/upgrading.mdx b/docs/pages/installation/upgrading.mdx new file mode 100644 index 000000000..f70da5edc --- /dev/null +++ b/docs/pages/installation/upgrading.mdx @@ -0,0 +1,11 @@ +## Using Docker + +```shell copy +cd /opt/hyperglass +docker compose down +docker compose rm -f +git fetch +git checkout v2.0.4 +docker compose build +docker compose up +``` diff --git a/docs/pages/license.mdx b/docs/pages/license.mdx new file mode 100644 index 000000000..7d80c0f46 --- /dev/null +++ b/docs/pages/license.mdx @@ -0,0 +1,20 @@ +# The Clear BSD License + +export const Year = () => new Date().getFullYear(); + +**Copyright © <Year/> Matthew Love** + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +- Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +> NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/pages/platforms.mdx b/docs/pages/platforms.mdx new file mode 100644 index 000000000..365cb171c --- /dev/null +++ b/docs/pages/platforms.mdx @@ -0,0 +1,28 @@ +--- +description: Platforms supported by hyperglass +--- + +import { Callout } from "nextra/components"; +import { PlatformTable } from "~/components/platforms"; + +hyperglass uses [Netmiko](https://github.com/ktbyers/netmiko) to interact with devices via SSH/telnet. [All platforms supported by Netmiko](https://github.com/ktbyers/netmiko/blob/develop/PLATFORMS.md) are supported by hyperglass. + +## Netmiko Platforms + +<Callout type="info"> + Just because Netmiko supports a given platform doesn't mean it will automatically work with + hyperglass. hyperglass has a limited number of built in directives (listed below). Any platforms + other than the ones listed below will require you to [add a custom + directive](configuration/examples/add-your-own-command.mdx) for each command you wish to make + available. +</Callout> + +<br /> + +<PlatformTable /> + +## Other Platforms + +| Platform | Key | Natively Supported | +| :---------------- | :----- | :------------------------------------------------------------------: | +| Any HTTP Endpoint | `http` | [See HTTP Device Docs](configuration/devices.mdx#http-configuration) | diff --git a/docs/pages/plugins.mdx b/docs/pages/plugins.mdx new file mode 100644 index 000000000..adc5087b1 --- /dev/null +++ b/docs/pages/plugins.mdx @@ -0,0 +1,144 @@ +--- +title: Plugins +description: hyperglass Plugins +--- + +import { Cards, Card } from "nextra/components"; + +Starting in version 2.0, hyperglass supports custom plugins. There are two main types of plugins: + +- [**Input Plugins**](#input-plugins): Apply custom validation logic to or transform user input before the query is sent to a device. +- [**Output Plugins**](#output-plugins): Interact with the output from a device before it's displayed to the user. + +Plugins are associated with a directive. + +## Examples + +### Input Plugins + +#### Convert a CIDR Prefix to Network and Mask + +In this example, the following query is sent to hyperglass: + +```json filename="Example Query" +{ + "query_target": "192.0.2.0/24", + "query_location": "your_location", + "query_type": "ip_route_directive" +} +``` + +The below plugin is defined and referenced by a directive: + +```python filename="/path/to/your/transform_plugin.py" +from ipaddress import ip_network +from hyperglass.plugins import InputPlugin + + +class TransformCIDR(InputPlugin): + def transform(self, query): + (target := query.query_target) + target_network = ip_network(target) + if target_network.version == 4: + return f"{target_network.network_address!s} {target_network.netmask!s}" + return target +``` + +```yaml filename="directives.yaml" +ip_route_directive: + name: IP Route + plugins: + - "/path/to/your/transform_plugin.py" + rules: + - condition: "0.0.0.0/0" + action: permit + command: "show ip route {target}" + - condition: "::/0" + action: permit + command: "show ipv6 route {target}" +``` + +When the query is received, the query target is transformed, resulting in this being sent to the device: + +```text +show ip route 192.0.2.0 255.255.255.0 +``` + +instead of: + +```text +show ip route 192.0.2.0/24 +``` + +#### Validate User Input + +In this example, we want to perform some custom validation not available via the directives `condition` API. For instance, say you wanted to ensure a query target isn't contained within a dynamic list of [bogon prefixes from Team Cymru](https://www.team-cymru.com/bogon-networks). + +```python filename="/path/to/your/validation_plugin.py" +from ipaddress import ip_network + +from hyperglass.plugins import InputPlugin +from hyperglass.external import HTTPClient + +class BogonPlugin(InputPlugin): + def validate(self, query): + target = ip_network(query.query_target) + + with HTTPClient(base_url="https://team-cymru.org") as client: + response = client.get("/Services/Bogons/fullbogons-ipv4.txt") + + bogon_strings = [line.strip() for line in response.text.split("\n") if not line.startswith("#")] + bogons = [ip_network(bogon) for bogon in bogon_strings] + + for bogon in bogons: + if target in bogon or target == bogon: + return False # Return false to fail validation. + return True +``` + +This isn't the best real-world example, since the above plugin would be run on every request, likely resulting in slow query responses, but it illustrates the power of plugins. + +### Output Plugins + +#### Redact Sensitive Information + +Say one of your directives might show some sensitive information in the query output. Using an output plugin, we can replace any text matching a pattern (or multiple patterns) with some other text. + +```python +import re +from hyperglass.plugins import OutputPlugin + +SENSITIVE_PATTERN = re.compile("SuperSecretInfo") + +class Redact(OutputPlugin): + def process(self, output, query): + result = [] + for each_output in output: + redacted = SENSITIVE_PATTERN.sub("<REDACTED>", each_output) + result.append(redacted) + return result +``` + +If the query output was: + +```text +Lorem ipsum dolor sit amet, SuperSecretInfo consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Viverra suspendisse potenti nullam ac. At elementum eu facilisis sed odio morbi. SuperSecretInfo iaculis urna id volutpat lacus.Nisl nisi +scelerisque eu ultrices vitae. Accumsan SuperSecretInfo tortor posuere ac ut consequat semper viverra nam libero. Libero id faucibus nisl +tincidunt eget nullam non nisi. Et ligula ullamcorper malesuada SuperSecretInfo proin libero nunc. Et malesuada fames ac turpis egestas sed. +Nulla facilisi cras fermentum odio eu. SuperSecretInfo condimentum mattis pellentesque id nibh tortor id aliquet lectus proin. Nisl rhoncus +mattis rhoncus urna neque. Tortor aliquam nulla facilisi cras SuperSecretInfo fermentum odio eu feugiat. Neque egestas congue quisque egestas +diam in arcu cursus SuperSecretInfo. +``` + +The above plugin would transform the output to: + +```text +Lorem ipsum dolor sit amet, <REDACTED> consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Viverra suspendisse potenti nullam ac. At elementum eu facilisis sed odio morbi. <REDACTED> iaculis urna id volutpat lacus.Nisl nisi +scelerisque eu ultrices vitae. Accumsan <REDACTED> tortor posuere ac ut consequat semper viverra nam libero. Libero id faucibus nisl +tincidunt eget nullam non nisi. Et ligula ullamcorper malesuada <REDACTED> proin libero nunc. Et malesuada fames ac turpis egestas sed. +Nulla facilisi cras fermentum odio eu. <REDACTED> condimentum mattis pellentesque id nibh tortor id aliquet lectus proin. Nisl rhoncus +mattis rhoncus urna neque. Tortor aliquam nulla facilisi cras <REDACTED> fermentum odio eu feugiat. Neque egestas congue quisque egestas +diam in arcu cursus <REDACTED>. +``` diff --git a/docs/platforms.json b/docs/platforms.json new file mode 100644 index 000000000..43164abbc --- /dev/null +++ b/docs/platforms.json @@ -0,0 +1 @@ +[{"name": "Arista EOS", "keys": ["arista_eos"], "native": true}, {"name": "BIRD", "keys": ["bird"], "native": true}, {"name": "Cisco IOS", "keys": ["cisco_ios"], "native": true}, {"name": "Cisco NX-OS", "keys": ["cisco_nxos"], "native": true}, {"name": "Cisco IOS-XR", "keys": ["cisco_xr"], "native": true}, {"name": "FRRouting", "keys": ["frr"], "native": true}, {"name": "Huawei VRP", "keys": ["huawei", "huawei_vrpv8"], "native": true}, {"name": "Juniper Junos", "keys": ["juniper", "juniper_junos"], "native": true}, {"name": "Mikrotik", "keys": ["mikrotik_routeros", "mikrotik_switchos"], "native": true}, {"name": "Nokia SR OS", "keys": ["nokia_sros"], "native": true}, {"name": "OpenBGPD", "keys": ["openbgpd"], "native": true}, {"name": "TNSR", "keys": ["tnsr"], "native": true}, {"name": "VyOS", "keys": ["vyos"], "native": true}, {"name": "", "keys": ["a10"], "native": false}, {"name": "", "keys": ["a10_ssh"], "native": false}, {"name": "", "keys": ["accedian"], "native": false}, {"name": "", "keys": ["accedian_ssh"], "native": false}, {"name": "", "keys": ["adtran_os"], "native": false}, {"name": "", "keys": ["adtran_os_ssh"], "native": false}, {"name": "", "keys": ["adtran_os_telnet"], "native": false}, {"name": "", "keys": ["alcatel_aos"], "native": false}, {"name": "", "keys": ["alcatel_aos_ssh"], "native": false}, {"name": "", "keys": ["alcatel_sros"], "native": false}, {"name": "", "keys": ["alcatel_sros_ssh"], "native": false}, {"name": "", "keys": ["allied_telesis_awplus"], "native": false}, {"name": "", "keys": ["allied_telesis_awplus_ssh"], "native": false}, {"name": "", "keys": ["apresia_aeos"], "native": false}, {"name": "", "keys": ["apresia_aeos_ssh"], "native": false}, {"name": "", "keys": ["apresia_aeos_telnet"], "native": false}, {"name": "", "keys": ["arista_eos_ssh"], "native": false}, {"name": "", "keys": ["arista_eos_telnet"], "native": false}, {"name": "", "keys": ["aruba_os"], "native": false}, {"name": "", "keys": ["aruba_os_ssh"], "native": false}, {"name": "", "keys": ["aruba_osswitch"], "native": false}, {"name": "", "keys": ["aruba_osswitch_ssh"], "native": false}, {"name": "", "keys": ["aruba_procurve"], "native": false}, {"name": "", "keys": ["aruba_procurve_ssh"], "native": false}, {"name": "", "keys": ["aruba_procurve_telnet"], "native": false}, {"name": "", "keys": ["audiocode_66"], "native": false}, {"name": "", "keys": ["audiocode_66_ssh"], "native": false}, {"name": "", "keys": ["audiocode_66_telnet"], "native": false}, {"name": "", "keys": ["audiocode_72"], "native": false}, {"name": "", "keys": ["audiocode_72_ssh"], "native": false}, {"name": "", "keys": ["audiocode_72_telnet"], "native": false}, {"name": "", "keys": ["audiocode_shell"], "native": false}, {"name": "", "keys": ["audiocode_shell_ssh"], "native": false}, {"name": "", "keys": ["audiocode_shell_telnet"], "native": false}, {"name": "", "keys": ["autodetect"], "native": false}, {"name": "", "keys": ["avaya_ers"], "native": false}, {"name": "", "keys": ["avaya_ers_ssh"], "native": false}, {"name": "", "keys": ["avaya_vsp"], "native": false}, {"name": "", "keys": ["avaya_vsp_ssh"], "native": false}, {"name": "", "keys": ["broadcom_icos"], "native": false}, {"name": "", "keys": ["broadcom_icos_ssh"], "native": false}, {"name": "", "keys": ["brocade_fastiron"], "native": false}, {"name": "", "keys": ["brocade_fastiron_ssh"], "native": false}, {"name": "", "keys": ["brocade_fastiron_telnet"], "native": false}, {"name": "", "keys": ["brocade_fos"], "native": false}, {"name": "", "keys": ["brocade_fos_ssh"], "native": false}, {"name": "", "keys": ["brocade_netiron"], "native": false}, {"name": "", "keys": ["brocade_netiron_ssh"], "native": false}, {"name": "", "keys": ["brocade_netiron_telnet"], "native": false}, {"name": "", "keys": ["brocade_nos"], "native": false}, {"name": "", "keys": ["brocade_nos_ssh"], "native": false}, {"name": "", "keys": ["brocade_vdx"], "native": false}, {"name": "", "keys": ["brocade_vdx_ssh"], "native": false}, {"name": "", "keys": ["brocade_vyos"], "native": false}, {"name": "", "keys": ["brocade_vyos_ssh"], "native": false}, {"name": "", "keys": ["calix_b6"], "native": false}, {"name": "", "keys": ["calix_b6_ssh"], "native": false}, {"name": "", "keys": ["calix_b6_telnet"], "native": false}, {"name": "", "keys": ["cdot_cros"], "native": false}, {"name": "", "keys": ["cdot_cros_ssh"], "native": false}, {"name": "", "keys": ["centec_os"], "native": false}, {"name": "", "keys": ["centec_os_ssh"], "native": false}, {"name": "", "keys": ["centec_os_telnet"], "native": false}, {"name": "", "keys": ["checkpoint_gaia"], "native": false}, {"name": "", "keys": ["checkpoint_gaia_ssh"], "native": false}, {"name": "", "keys": ["ciena_saos"], "native": false}, {"name": "", "keys": ["ciena_saos_ssh"], "native": false}, {"name": "", "keys": ["ciena_saos_telnet"], "native": false}, {"name": "", "keys": ["cisco_asa"], "native": false}, {"name": "", "keys": ["cisco_asa_ssh"], "native": false}, {"name": "", "keys": ["cisco_ftd"], "native": false}, {"name": "", "keys": ["cisco_ftd_ssh"], "native": false}, {"name": "", "keys": ["cisco_ios_serial"], "native": false}, {"name": "", "keys": ["cisco_ios_ssh"], "native": false}, {"name": "", "keys": ["cisco_ios_telnet"], "native": false}, {"name": "", "keys": ["cisco_nxos_ssh"], "native": false}, {"name": "", "keys": ["cisco_s300"], "native": false}, {"name": "", "keys": ["cisco_s300_ssh"], "native": false}, {"name": "", "keys": ["cisco_s300_telnet"], "native": false}, {"name": "", "keys": ["cisco_tp"], "native": false}, {"name": "", "keys": ["cisco_tp_ssh"], "native": false}, {"name": "", "keys": ["cisco_viptela"], "native": false}, {"name": "", "keys": ["cisco_viptela_ssh"], "native": false}, {"name": "", "keys": ["cisco_wlc"], "native": false}, {"name": "", "keys": ["cisco_wlc_ssh"], "native": false}, {"name": "", "keys": ["cisco_xe"], "native": false}, {"name": "", "keys": ["cisco_xe_ssh"], "native": false}, {"name": "", "keys": ["cisco_xr_ssh"], "native": false}, {"name": "", "keys": ["cisco_xr_telnet"], "native": false}, {"name": "", "keys": ["cloudgenix_ion"], "native": false}, {"name": "", "keys": ["cloudgenix_ion_ssh"], "native": false}, {"name": "", "keys": ["coriant"], "native": false}, {"name": "", "keys": ["coriant_ssh"], "native": false}, {"name": "", "keys": ["dell_dnos6_telnet"], "native": false}, {"name": "", "keys": ["dell_dnos9"], "native": false}, {"name": "", "keys": ["dell_dnos9_ssh"], "native": false}, {"name": "", "keys": ["dell_force10"], "native": false}, {"name": "", "keys": ["dell_force10_ssh"], "native": false}, {"name": "", "keys": ["dell_isilon"], "native": false}, {"name": "", "keys": ["dell_isilon_ssh"], "native": false}, {"name": "", "keys": ["dell_os10"], "native": false}, {"name": "", "keys": ["dell_os10_ssh"], "native": false}, {"name": "", "keys": ["dell_os6"], "native": false}, {"name": "", "keys": ["dell_os6_ssh"], "native": false}, {"name": "", "keys": ["dell_os9"], "native": false}, {"name": "", "keys": ["dell_os9_ssh"], "native": false}, {"name": "", "keys": ["dell_powerconnect"], "native": false}, {"name": "", "keys": ["dell_powerconnect_ssh"], "native": false}, {"name": "", "keys": ["dell_powerconnect_telnet"], "native": false}, {"name": "", "keys": ["dell_sonic"], "native": false}, {"name": "", "keys": ["dell_sonic_ssh"], "native": false}, {"name": "", "keys": ["dlink_ds"], "native": false}, {"name": "", "keys": ["dlink_ds_ssh"], "native": false}, {"name": "", "keys": ["dlink_ds_telnet"], "native": false}, {"name": "", "keys": ["eltex"], "native": false}, {"name": "", "keys": ["eltex_esr"], "native": false}, {"name": "", "keys": ["eltex_esr_ssh"], "native": false}, {"name": "", "keys": ["eltex_ssh"], "native": false}, {"name": "", "keys": ["endace"], "native": false}, {"name": "", "keys": ["endace_ssh"], "native": false}, {"name": "", "keys": ["enterasys"], "native": false}, {"name": "", "keys": ["enterasys_ssh"], "native": false}, {"name": "", "keys": ["ericsson_ipos"], "native": false}, {"name": "", "keys": ["ericsson_ipos_ssh"], "native": false}, {"name": "", "keys": ["extreme"], "native": false}, {"name": "", "keys": ["extreme_ers"], "native": false}, {"name": "", "keys": ["extreme_ers_ssh"], "native": false}, {"name": "", "keys": ["extreme_exos"], "native": false}, {"name": "", "keys": ["extreme_exos_ssh"], "native": false}, {"name": "", "keys": ["extreme_exos_telnet"], "native": false}, {"name": "", "keys": ["extreme_netiron"], "native": false}, {"name": "", "keys": ["extreme_netiron_ssh"], "native": false}, {"name": "", "keys": ["extreme_netiron_telnet"], "native": false}, {"name": "", "keys": ["extreme_nos"], "native": false}, {"name": "", "keys": ["extreme_nos_ssh"], "native": false}, {"name": "", "keys": ["extreme_slx"], "native": false}, {"name": "", "keys": ["extreme_slx_ssh"], "native": false}, {"name": "", "keys": ["extreme_ssh"], "native": false}, {"name": "", "keys": ["extreme_telnet"], "native": false}, {"name": "", "keys": ["extreme_tierra"], "native": false}, {"name": "", "keys": ["extreme_tierra_ssh"], "native": false}, {"name": "", "keys": ["extreme_vdx"], "native": false}, {"name": "", "keys": ["extreme_vdx_ssh"], "native": false}, {"name": "", "keys": ["extreme_vsp"], "native": false}, {"name": "", "keys": ["extreme_vsp_ssh"], "native": false}, {"name": "", "keys": ["extreme_wing"], "native": false}, {"name": "", "keys": ["extreme_wing_ssh"], "native": false}, {"name": "", "keys": ["f5_linux"], "native": false}, {"name": "", "keys": ["f5_linux_ssh"], "native": false}, {"name": "", "keys": ["f5_ltm"], "native": false}, {"name": "", "keys": ["f5_ltm_ssh"], "native": false}, {"name": "", "keys": ["f5_tmsh"], "native": false}, {"name": "", "keys": ["f5_tmsh_ssh"], "native": false}, {"name": "", "keys": ["flexvnf"], "native": false}, {"name": "", "keys": ["flexvnf_ssh"], "native": false}, {"name": "", "keys": ["fortinet"], "native": false}, {"name": "", "keys": ["fortinet_ssh"], "native": false}, {"name": "", "keys": ["generic"], "native": false}, {"name": "", "keys": ["generic_ssh"], "native": false}, {"name": "", "keys": ["generic_telnet"], "native": false}, {"name": "", "keys": ["generic_termserver"], "native": false}, {"name": "", "keys": ["generic_termserver_ssh"], "native": false}, {"name": "", "keys": ["generic_termserver_telnet"], "native": false}, {"name": "", "keys": ["hp_comware"], "native": false}, {"name": "", "keys": ["hp_comware_ssh"], "native": false}, {"name": "", "keys": ["hp_comware_telnet"], "native": false}, {"name": "", "keys": ["hp_procurve"], "native": false}, {"name": "", "keys": ["hp_procurve_ssh"], "native": false}, {"name": "", "keys": ["hp_procurve_telnet"], "native": false}, {"name": "", "keys": ["huawei_olt"], "native": false}, {"name": "", "keys": ["huawei_olt_ssh"], "native": false}, {"name": "", "keys": ["huawei_olt_telnet"], "native": false}, {"name": "", "keys": ["huawei_smartax"], "native": false}, {"name": "", "keys": ["huawei_smartax_ssh"], "native": false}, {"name": "", "keys": ["huawei_ssh"], "native": false}, {"name": "", "keys": ["huawei_telnet"], "native": false}, {"name": "", "keys": ["huawei_vrpv8_ssh"], "native": false}, {"name": "", "keys": ["ipinfusion_ocnos"], "native": false}, {"name": "", "keys": ["ipinfusion_ocnos_ssh"], "native": false}, {"name": "", "keys": ["ipinfusion_ocnos_telnet"], "native": false}, {"name": "", "keys": ["juniper_junos_ssh"], "native": false}, {"name": "", "keys": ["juniper_junos_telnet"], "native": false}, {"name": "", "keys": ["juniper_screenos"], "native": false}, {"name": "", "keys": ["juniper_screenos_ssh"], "native": false}, {"name": "", "keys": ["juniper_ssh"], "native": false}, {"name": "", "keys": ["keymile"], "native": false}, {"name": "", "keys": ["keymile_nos"], "native": false}, {"name": "", "keys": ["keymile_nos_ssh"], "native": false}, {"name": "", "keys": ["keymile_ssh"], "native": false}, {"name": "", "keys": ["linux"], "native": false}, {"name": "", "keys": ["linux_ssh"], "native": false}, {"name": "", "keys": ["mellanox"], "native": false}, {"name": "", "keys": ["mellanox_mlnxos"], "native": false}, {"name": "", "keys": ["mellanox_mlnxos_ssh"], "native": false}, {"name": "", "keys": ["mellanox_ssh"], "native": false}, {"name": "", "keys": ["mikrotik_routeros_ssh"], "native": false}, {"name": "", "keys": ["mikrotik_switchos_ssh"], "native": false}, {"name": "", "keys": ["mrv_lx"], "native": false}, {"name": "", "keys": ["mrv_lx_ssh"], "native": false}, {"name": "", "keys": ["mrv_optiswitch"], "native": false}, {"name": "", "keys": ["mrv_optiswitch_ssh"], "native": false}, {"name": "", "keys": ["netapp_cdot"], "native": false}, {"name": "", "keys": ["netapp_cdot_ssh"], "native": false}, {"name": "", "keys": ["netgear_prosafe"], "native": false}, {"name": "", "keys": ["netgear_prosafe_ssh"], "native": false}, {"name": "", "keys": ["netscaler"], "native": false}, {"name": "", "keys": ["netscaler_ssh"], "native": false}, {"name": "", "keys": ["nokia_srl"], "native": false}, {"name": "", "keys": ["nokia_srl_ssh"], "native": false}, {"name": "", "keys": ["nokia_sros_ssh"], "native": false}, {"name": "", "keys": ["nokia_sros_telnet"], "native": false}, {"name": "", "keys": ["oneaccess_oneos"], "native": false}, {"name": "", "keys": ["oneaccess_oneos_ssh"], "native": false}, {"name": "", "keys": ["oneaccess_oneos_telnet"], "native": false}, {"name": "", "keys": ["ovs_linux"], "native": false}, {"name": "", "keys": ["ovs_linux_ssh"], "native": false}, {"name": "", "keys": ["paloalto_panos"], "native": false}, {"name": "", "keys": ["paloalto_panos_ssh"], "native": false}, {"name": "", "keys": ["paloalto_panos_telnet"], "native": false}, {"name": "", "keys": ["pluribus"], "native": false}, {"name": "", "keys": ["pluribus_ssh"], "native": false}, {"name": "", "keys": ["quanta_mesh"], "native": false}, {"name": "", "keys": ["quanta_mesh_ssh"], "native": false}, {"name": "", "keys": ["rad_etx"], "native": false}, {"name": "", "keys": ["rad_etx_ssh"], "native": false}, {"name": "", "keys": ["rad_etx_telnet"], "native": false}, {"name": "", "keys": ["raisecom_roap"], "native": false}, {"name": "", "keys": ["raisecom_roap_ssh"], "native": false}, {"name": "", "keys": ["raisecom_telnet"], "native": false}, {"name": "", "keys": ["ruckus_fastiron"], "native": false}, {"name": "", "keys": ["ruckus_fastiron_ssh"], "native": false}, {"name": "", "keys": ["ruckus_fastiron_telnet"], "native": false}, {"name": "", "keys": ["ruijie_os"], "native": false}, {"name": "", "keys": ["ruijie_os_ssh"], "native": false}, {"name": "", "keys": ["ruijie_os_telnet"], "native": false}, {"name": "", "keys": ["sixwind_os"], "native": false}, {"name": "", "keys": ["sixwind_os_ssh"], "native": false}, {"name": "", "keys": ["sophos_sfos"], "native": false}, {"name": "", "keys": ["sophos_sfos_ssh"], "native": false}, {"name": "", "keys": ["supermicro_smis"], "native": false}, {"name": "", "keys": ["supermicro_smis_ssh"], "native": false}, {"name": "", "keys": ["supermicro_smis_telnet"], "native": false}, {"name": "", "keys": ["terminal_server"], "native": false}, {"name": "", "keys": ["tplink_jetstream"], "native": false}, {"name": "", "keys": ["tplink_jetstream_ssh"], "native": false}, {"name": "", "keys": ["tplink_jetstream_telnet"], "native": false}, {"name": "", "keys": ["ubiquiti_edge"], "native": false}, {"name": "", "keys": ["ubiquiti_edge_ssh"], "native": false}, {"name": "", "keys": ["ubiquiti_edgerouter"], "native": false}, {"name": "", "keys": ["ubiquiti_edgerouter_ssh"], "native": false}, {"name": "", "keys": ["ubiquiti_edgeswitch"], "native": false}, {"name": "", "keys": ["ubiquiti_edgeswitch_ssh"], "native": false}, {"name": "", "keys": ["ubiquiti_unifiswitch"], "native": false}, {"name": "", "keys": ["ubiquiti_unifiswitch_ssh"], "native": false}, {"name": "", "keys": ["vyatta_vyos"], "native": false}, {"name": "", "keys": ["vyatta_vyos_ssh"], "native": false}, {"name": "", "keys": ["vyos_ssh"], "native": false}, {"name": "", "keys": ["watchguard_fireware"], "native": false}, {"name": "", "keys": ["watchguard_fireware_ssh"], "native": false}, {"name": "", "keys": ["yamaha"], "native": false}, {"name": "", "keys": ["yamaha_ssh"], "native": false}, {"name": "", "keys": ["yamaha_telnet"], "native": false}, {"name": "", "keys": ["zte_zxros"], "native": false}, {"name": "", "keys": ["zte_zxros_ssh"], "native": false}, {"name": "", "keys": ["zte_zxros_telnet"], "native": false}, {"name": "", "keys": ["zyxel_os"], "native": false}, {"name": "", "keys": ["zyxel_os_ssh"], "native": false}] \ No newline at end of file diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 000000000..9585e0df9 --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,3554 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + next: + specifier: ^14.1.1 + version: 14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + nextra: + specifier: 3.0.0-alpha.24 + version: 3.0.0-alpha.24(@types/react@18.0.26)(next@14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.3.3) + nextra-theme-docs: + specifier: 3.0.0-alpha.24 + version: 3.0.0-alpha.24(next@14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nextra@3.0.0-alpha.24(@types/react@18.0.26)(next@14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.3.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@types/node': + specifier: ^20.11.24 + version: 20.11.24 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + +packages: + + '@braintree/sanitize-url@6.0.4': + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + + '@headlessui/react@1.7.18': + resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + + '@mdx-js/mdx@3.0.1': + resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==} + + '@mdx-js/react@3.0.1': + resolution: {integrity: sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@napi-rs/simple-git-android-arm-eabi@0.1.16': + resolution: {integrity: sha512-dbrCL0Pl5KZG7x7tXdtVsA5CO6At5ohDX3myf5xIYn9kN4jDFxsocl8bNt6Vb/hZQoJd8fI+k5VlJt+rFhbdVw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/simple-git-android-arm64@0.1.16': + resolution: {integrity: sha512-xYz+TW5J09iK8SuTAKK2D5MMIsBUXVSs8nYp7HcMi8q6FCRO7yJj96YfP9PvKsc/k64hOyqGmL5DhCzY9Cu1FQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/simple-git-darwin-arm64@0.1.16': + resolution: {integrity: sha512-XfgsYqxhUE022MJobeiX563TJqyQyX4FmYCnqrtJwAfivESVeAJiH6bQIum8dDEYMHXCsG7nL8Ok0Dp8k2m42g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/simple-git-darwin-x64@0.1.16': + resolution: {integrity: sha512-tkEVBhD6vgRCbeWsaAQqM3bTfpIVGeitamPPRVSbsq8qgzJ5Dx6ZedH27R7KSsA/uao7mZ3dsrNLXbu1Wy5MzA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/simple-git-linux-arm-gnueabihf@0.1.16': + resolution: {integrity: sha512-R6VAyNnp/yRaT7DV1Ao3r67SqTWDa+fNq2LrNy0Z8gXk2wB9ZKlrxFtLPE1WSpWknWtyRDLpRlsorh7Evk7+7w==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/simple-git-linux-arm64-gnu@0.1.16': + resolution: {integrity: sha512-LAGI0opFKw/HBMCV2qIBK3uWSEW9h4xd2ireZKLJy8DBPymX6NrWIamuxYNyCuACnFdPRxR4LaRFy4J5ZwuMdw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/simple-git-linux-arm64-musl@0.1.16': + resolution: {integrity: sha512-I57Ph0F0Yn2KW93ep+V1EzKhACqX0x49vvSiapqIsdDA2PifdEWLc1LJarBolmK7NKoPqKmf6lAKKO9lhiZzkg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/simple-git-linux-x64-gnu@0.1.16': + resolution: {integrity: sha512-AZYYFY2V7hlcQASPEOWyOa3e1skzTct9QPzz0LiDM3f/hCFY/wBaU2M6NC5iG3d2Kr38heuyFS/+JqxLm5WaKA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/simple-git-linux-x64-musl@0.1.16': + resolution: {integrity: sha512-9TyMcYSBJwjT8jwjY9m24BZbu7ozyWTjsmYBYNtK3B0Um1Ov6jthSNneLVvouQ6x+k3Ow+00TiFh6bvmT00r8g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/simple-git-win32-arm64-msvc@0.1.16': + resolution: {integrity: sha512-uslJ1WuAHCYJWui6xjsyT47SjX6KOHDtClmNO8hqKz1pmDSNY7AjyUY8HxvD1lK9bDnWwc4JYhikS9cxCqHybw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/simple-git-win32-x64-msvc@0.1.16': + resolution: {integrity: sha512-SoEaVeCZCDF1MP+M9bMSXsZWgEjk4On9GWADO5JOulvzR1bKjk0s9PMHwe/YztR9F0sJzrCxwtvBZowhSJsQPg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/simple-git@0.1.16': + resolution: {integrity: sha512-C5wRPw9waqL2jk3jEDeJv+f7ScuO3N0a39HVdyFLkwKxHH4Sya4ZbzZsu2JLi6eEqe7RuHipHL6mC7B2OfYZZw==} + engines: {node: '>= 10'} + + '@next/env@14.1.1': + resolution: {integrity: sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA==} + + '@next/swc-darwin-arm64@14.1.1': + resolution: {integrity: sha512-yDjSFKQKTIjyT7cFv+DqQfW5jsD+tVxXTckSe1KIouKk75t1qZmj/mV3wzdmFb0XHVGtyRjDMulfVG8uCKemOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@14.1.1': + resolution: {integrity: sha512-KCQmBL0CmFmN8D64FHIZVD9I4ugQsDBBEJKiblXGgwn7wBCSe8N4Dx47sdzl4JAg39IkSN5NNrr8AniXLMb3aw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@14.1.1': + resolution: {integrity: sha512-YDQfbWyW0JMKhJf/T4eyFr4b3tceTorQ5w2n7I0mNVTFOvu6CGEzfwT3RSAQGTi/FFMTFcuspPec/7dFHuP7Eg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@14.1.1': + resolution: {integrity: sha512-fiuN/OG6sNGRN/bRFxRvV5LyzLB8gaL8cbDH5o3mEiVwfcMzyE5T//ilMmaTrnA8HLMS6hoz4cHOu6Qcp9vxgQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@14.1.1': + resolution: {integrity: sha512-rv6AAdEXoezjbdfp3ouMuVqeLjE1Bin0AuE6qxE6V9g3Giz5/R3xpocHoAi7CufRR+lnkuUjRBn05SYJ83oKNQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@14.1.1': + resolution: {integrity: sha512-YAZLGsaNeChSrpz/G7MxO3TIBLaMN8QWMr3X8bt6rCvKovwU7GqQlDu99WdvF33kI8ZahvcdbFsy4jAFzFX7og==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@14.1.1': + resolution: {integrity: sha512-1L4mUYPBMvVDMZg1inUYyPvFSduot0g73hgfD9CODgbr4xiTYe0VOMTZzaRqYJYBA9mana0x4eaAaypmWo1r5A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.1.1': + resolution: {integrity: sha512-jvIE9tsuj9vpbbXlR5YxrghRfMuG0Qm/nZ/1KDHc+y6FpnZ/apsgh+G6t15vefU0zp3WSpTMIdXRUsNl/7RSuw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@14.1.1': + resolution: {integrity: sha512-S6K6EHDU5+1KrBDLko7/c1MNy/Ya73pIAmvKeFwsF4RmBFJSO7/7YeD4FnZ4iBdzE69PpQ4sOMU9ORKeNuxe8A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@shikijs/core@1.6.1': + resolution: {integrity: sha512-CqYyepN4SnBopaoXYwng4NO8riB5ask/LTCkhOFq+GNGtr2X+aKeD767eYdqYukeixEUvv4bXdyTYVaogj7KBw==} + + '@shikijs/twoslash@1.6.1': + resolution: {integrity: sha512-VluGZXQ97sDFyxneOzsPkEHK06A6C1SRDh+kSM9AZAkzHorZaGxF4awgA3rh2K0oZnR94NZzfhq8GtERm38EEQ==} + + '@swc/helpers@0.5.2': + resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} + + '@tanstack/react-virtual@3.1.3': + resolution: {integrity: sha512-YCzcbF/Ws/uZ0q3Z6fagH+JVhx4JLvbSflgldMgLsuvB8aXjZLLb3HvrEVxY480F9wFlBiXlvQxOyXb5ENPrNA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@tanstack/virtual-core@3.1.3': + resolution: {integrity: sha512-Y5B4EYyv1j9V8LzeAoOVeTg0LI7Fo5InYKgAjkY1Pu9GjtUwX/EKxNcU7ng3sKr99WEf+bPTcktAeybyMOYo+g==} + + '@theguild/remark-mermaid@0.0.5': + resolution: {integrity: sha512-e+ZIyJkEv9jabI4m7q29wZtZv+2iwPGsXJ2d46Zi7e+QcFudiyuqhLhHG/3gX3ZEB+hxTch+fpItyMS8jwbIcw==} + peerDependencies: + react: ^18.2.0 + + '@theguild/remark-npm2yarn@0.3.0': + resolution: {integrity: sha512-Fofw+9airYgjBd9G6PiHHCrptjyUybQ50JH9/5o9LCH54kggJ7stpCofzHjICB8L7VQbQ1Gwu23P/3CMVY1R4Q==} + + '@types/acorn@4.0.6': + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + + '@types/d3-scale-chromatic@3.0.3': + resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} + + '@types/d3-scale@4.0.8': + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + + '@types/d3-time@3.0.3': + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + + '@types/debug@4.1.7': + resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} + + '@types/estree-jsx@1.0.0': + resolution: {integrity: sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==} + + '@types/estree@1.0.0': + resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + + '@types/mdast@3.0.10': + resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} + + '@types/mdast@4.0.3': + resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} + + '@types/mdx@2.0.3': + resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==} + + '@types/ms@0.7.31': + resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} + + '@types/nlcst@1.0.4': + resolution: {integrity: sha512-ABoYdNQ/kBSsLvZAekMhIPMQ3YUZvavStpKYs7BjLLuKVmIMA0LUgZ7b54zzuWJRbHF80v1cNf4r90Vd6eMQDg==} + + '@types/node@20.11.24': + resolution: {integrity: sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==} + + '@types/prop-types@15.7.5': + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + + '@types/react@18.0.26': + resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} + + '@types/scheduler@0.16.2': + resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + + '@types/unist@2.0.6': + resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} + + '@types/unist@3.0.2': + resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + + '@typescript/vfs@1.5.0': + resolution: {integrity: sha512-AJS307bPgbsZZ9ggCT3wwpg3VbTKMFNHfaY/uF0ahSkYYrPF2dSSKDNIDIQAHm9qJqbLvCsSJH7yN4Vs/CsMMg==} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.8.1: + resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + + arg@1.0.0: + resolution: {integrity: sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astring@1.8.3: + resolution: {integrity: sha512-sRpyiNrx2dEYIMmUXprS8nlpRg2Drs8m9ElX9vVEXaCB4XEAJhKfs7IcX0IwShjuOAjLR6wzIrgoptz1n19i1A==} + hasBin: true + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + better-react-mathjax@2.0.3: + resolution: {integrity: sha512-wfifT8GFOKb1TWm2+E50I6DJpLZ5kLbch283Lu043EJtwSv0XvZDjr4YfR4d2MjAhqP6SH4VjjrKgbX8R00oCQ==} + peerDependencies: + react: '>=16.8' + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + caniuse-lite@1.0.30001591: + resolution: {integrity: sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@2.3.0: + resolution: {integrity: sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==} + engines: {node: '>=4'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clipboardy@1.2.2: + resolution: {integrity: sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==} + engines: {node: '>=4'} + + clsx@2.1.0: + resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} + engines: {node: '>=6'} + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + commander@9.2.0: + resolution: {integrity: sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==} + engines: {node: ^12.20.0 || >=14} + + compute-scroll-into-view@3.1.0: + resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + + csstype@3.1.1: + resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.28.1: + resolution: {integrity: sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.0: + resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.0.0: + resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.8.5: + resolution: {integrity: sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.10: + resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==} + + dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + + dompurify@3.0.9: + resolution: {integrity: sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==} + + elkjs@0.9.2: + resolution: {integrity: sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@2.0.1: + resolution: {integrity: sha512-rxZj1GkQhY4x1j/CSnybK9cGuMFQYFPLq0iNyopqf14aOVLFtMv7Esika+ObJWPWiOHuMOAHz3YkWoLYYRnzWQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-value-to-estree@1.3.0: + resolution: {integrity: sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==} + engines: {node: '>=12.0.0'} + + estree-util-value-to-estree@3.1.1: + resolution: {integrity: sha512-5mvUrF2suuv5f5cGDnDphIy4/gW86z82kl5qG6mM9z04SEQI4FB5Apmaw/TGEf3l55nLtMs5s51dmhUzvAHQCA==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@3.0.1: + resolution: {integrity: sha512-woY0RUD87WzMBUiZLx8NsYr23N5BKsOMZHhu2hoNRVh6NXGfoiT1KOL8G3UHlJAnEDGmfa5ubNA/AacfG+Kb0g==} + + execa@0.8.0: + resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==} + engines: {node: '>=4'} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + flexsearch@0.7.43: + resolution: {integrity: sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==} + + focus-visible@5.2.0: + resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + get-stream@3.0.0: + resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} + engines: {node: '>=4'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + has-flag@2.0.0: + resolution: {integrity: sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==} + engines: {node: '>=0.10.0'} + + hast-util-from-dom@5.0.0: + resolution: {integrity: sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.1: + resolution: {integrity: sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==} + + hast-util-from-parse5@8.0.1: + resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.0.2: + resolution: {integrity: sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==} + + hast-util-to-estree@3.1.0: + resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} + + hast-util-to-jsx-runtime@2.3.0: + resolution: {integrity: sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-string@3.0.0: + resolution: {integrity: sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==} + + hast-util-to-text@4.0.0: + resolution: {integrity: sha512-EWiE1FSArNBPUo1cKWtzqgnuRQwEeQbQtnFJRYV1hb1BWDgrAlBU0ExptvZMM/KSA82cDpm2sFGf3Dmc5Mza3w==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@8.0.0: + resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} + + heap@0.2.7: + resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + inline-style-parser@0.2.3: + resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + intersection-observer@0.12.2: + resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-reference@3.0.0: + resolution: {integrity: sha512-Eo1W3wUoHWoCoVM4GVl/a+K0IgiqE5aIo4kJABFyMum1ZORlPkC+UC357sSQUL5w5QCE5kCC9upl75b7+7CY/Q==} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + katex@0.16.9: + resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@3.0.3: + resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} + + mathjax-full@3.2.2: + resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + + mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + + mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + + mdast-util-from-markdown@2.0.1: + resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-gfm-autolink-literal@2.0.0: + resolution: {integrity: sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==} + + mdast-util-gfm-footnote@2.0.0: + resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.0.0: + resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + + mdast-util-mdx-expression@2.0.0: + resolution: {integrity: sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==} + + mdast-util-mdx-jsx@3.1.2: + resolution: {integrity: sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.1.0: + resolution: {integrity: sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==} + + mdast-util-to-markdown@2.1.0: + resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==} + + mdast-util-to-string@3.1.0: + resolution: {integrity: sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mermaid@10.8.0: + resolution: {integrity: sha512-9CzfSreRjdDJxX796+jW4zjEq0DVw5xVF0nWsqff8OTbrt+ml0TZ5PyYUjjUZJa2NYxYJZZXewEquxGiM8qZEA==} + + mhchemparser@4.2.1: + resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + + micromark-core-commonmark@1.0.6: + resolution: {integrity: sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==} + + micromark-core-commonmark@2.0.1: + resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==} + + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + + micromark-extension-gfm-autolink-literal@2.0.0: + resolution: {integrity: sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==} + + micromark-extension-gfm-footnote@2.0.0: + resolution: {integrity: sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==} + + micromark-extension-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==} + + micromark-extension-gfm-table@2.0.0: + resolution: {integrity: sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.0.1: + resolution: {integrity: sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-math@3.0.0: + resolution: {integrity: sha512-iJ2Q28vBoEovLN5o3GO12CpqorQRYDPT+p4zW50tGwTfJB+iv/VnB6Ini+gqa24K97DwptMBBIvVX6Bjk49oyQ==} + + micromark-extension-mdx-expression@3.0.0: + resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==} + + micromark-extension-mdx-jsx@3.0.0: + resolution: {integrity: sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@1.0.0: + resolution: {integrity: sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==} + + micromark-factory-destination@2.0.0: + resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} + + micromark-factory-label@1.0.2: + resolution: {integrity: sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==} + + micromark-factory-label@2.0.0: + resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} + + micromark-factory-mdx-expression@2.0.1: + resolution: {integrity: sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg==} + + micromark-factory-space@1.0.0: + resolution: {integrity: sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==} + + micromark-factory-space@2.0.0: + resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} + + micromark-factory-title@1.0.2: + resolution: {integrity: sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==} + + micromark-factory-title@2.0.0: + resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} + + micromark-factory-whitespace@1.0.0: + resolution: {integrity: sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==} + + micromark-factory-whitespace@2.0.0: + resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} + + micromark-util-character@1.1.0: + resolution: {integrity: sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==} + + micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + + micromark-util-chunked@1.0.0: + resolution: {integrity: sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==} + + micromark-util-chunked@2.0.0: + resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} + + micromark-util-classify-character@1.0.0: + resolution: {integrity: sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==} + + micromark-util-classify-character@2.0.0: + resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} + + micromark-util-combine-extensions@1.0.0: + resolution: {integrity: sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==} + + micromark-util-combine-extensions@2.0.0: + resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} + + micromark-util-decode-numeric-character-reference@1.0.0: + resolution: {integrity: sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==} + + micromark-util-decode-numeric-character-reference@2.0.1: + resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} + + micromark-util-decode-string@1.0.2: + resolution: {integrity: sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==} + + micromark-util-decode-string@2.0.0: + resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} + + micromark-util-encode@1.0.1: + resolution: {integrity: sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==} + + micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + micromark-util-events-to-acorn@2.0.2: + resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==} + + micromark-util-html-tag-name@1.1.0: + resolution: {integrity: sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==} + + micromark-util-html-tag-name@2.0.0: + resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} + + micromark-util-normalize-identifier@1.0.0: + resolution: {integrity: sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==} + + micromark-util-normalize-identifier@2.0.0: + resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} + + micromark-util-resolve-all@1.0.0: + resolution: {integrity: sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==} + + micromark-util-resolve-all@2.0.0: + resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} + + micromark-util-sanitize-uri@1.1.0: + resolution: {integrity: sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==} + + micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + + micromark-util-subtokenize@1.0.2: + resolution: {integrity: sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==} + + micromark-util-subtokenize@2.0.1: + resolution: {integrity: sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==} + + micromark-util-symbol@1.0.1: + resolution: {integrity: sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==} + + micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + micromark-util-types@1.0.2: + resolution: {integrity: sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==} + + micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + micromark@3.1.0: + resolution: {integrity: sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==} + + micromark@4.0.0: + resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} + + mj-context-menu@0.6.1: + resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next-themes@0.3.0: + resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} + peerDependencies: + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + + next@14.1.1: + resolution: {integrity: sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + + nextra-theme-docs@3.0.0-alpha.24: + resolution: {integrity: sha512-AUuv3o1xYzGGvUP/lxxf7i++Zyycy4bH22l6T8uDVez2n0j/RDbzqmSIdL4Jh4POUd0fyxEG+BUomKXKyj3L1g==} + peerDependencies: + next: '>=13' + nextra: 3.0.0-alpha.24 + react: '>=16.13.1' + react-dom: '>=16.13.1' + + nextra@3.0.0-alpha.24: + resolution: {integrity: sha512-KScl/DQG68JBGy6gfOFNdVcrlc24YvcegGnYwUXvpCoHx2YcBUSp6FE7LeaX/C/mI4PPIMmTJuEWIjGH9DXeCQ==} + engines: {node: '>=18'} + peerDependencies: + next: '>=13' + react: '>=16.13.1' + react-dom: '>=16.13.1' + + nlcst-to-string@3.1.1: + resolution: {integrity: sha512-63mVyqaqt0cmn2VcI2aH6kxe1rLAmSROqHMA0i4qqg1tidkfExgpb0FGMikMCn86mw5dFtBtEANfmSSK7TjNHw==} + + non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-to-yarn@2.2.1: + resolution: {integrity: sha512-O/j/ROyX0KGLG7O6Ieut/seQ0oiTpHF2tXAcFbpdTLQFiaNtkyTXXocM1fwpaa60dg1qpWj0nHlbNhx6qwuENQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + parse-entities@4.0.0: + resolution: {integrity: sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ==} + + parse-latin@5.0.1: + resolution: {integrity: sha512-b/K8ExXaWC9t34kKeDV8kGXBkXZ1HCSAZRYE7HR14eA1GlXX5L8iWhs8USJNhQU9q5ci413jCKF0gOyovvyRBg==} + + parse-numeric-range@1.3.0: + resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + + parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + periscopic@3.0.4: + resolution: {integrity: sha512-SFx68DxCv0Iyo6APZuw/AKewkkThGwssmU0QWtTlvov3VAtPX+QJ4CadwSaz8nrT5jPIuxdvJWB4PnD2KNDxQg==} + + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + property-information@6.2.0: + resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==} + + pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + + react-dom@18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + + react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + + reading-time@1.5.0: + resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} + + rehype-katex@7.0.0: + resolution: {integrity: sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q==} + + rehype-parse@9.0.0: + resolution: {integrity: sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==} + + rehype-pretty-code@0.13.0: + resolution: {integrity: sha512-+22dz1StXlF7dlMyOySNaVxgcGhMI4BCxq0JxJJPWYGiKsI6cu5jyuIKGHXHvH18D8sv1rdKtvsY9UEfN3++SQ==} + engines: {node: '>=18'} + peerDependencies: + shiki: ^1.0.0 + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + + remark-gfm@4.0.0: + resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} + + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + + remark-mdx@3.0.1: + resolution: {integrity: sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-reading-time@2.0.1: + resolution: {integrity: sha512-fy4BKy9SRhtYbEHvp6AItbRTnrhiDGbqLQTSYVbQPGuRCncU1ubSsh9p/W5QZSxtYcUXv8KGL0xBgPLyNJA1xw==} + + remark-rehype@11.1.0: + resolution: {integrity: sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==} + + remark-smartypants@2.1.0: + resolution: {integrity: sha512-qoF6Vz3BjU2tP6OfZqHOvCU0ACmu/6jhGaINSQRI9mM7wCxNQTKB3JUAN4SVoN2ybElEDTxBIABRep7e569iJw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + retext-latin@3.1.0: + resolution: {integrity: sha512-5MrD1tuebzO8ppsja5eEu+ZbBeUNCjoEarn70tkXOS7Bdsdf6tNahsv2bY0Z8VooFF6cw7/6S+d3yI/TMlMVVQ==} + + retext-smartypants@5.2.0: + resolution: {integrity: sha512-Do8oM+SsjrbzT2UNIKgheP0hgUQTDDQYyZaIY3kfq0pdFzoPk+ZClYJ+OERNXveog4xf1pZL4PfRxNoVL7a/jw==} + + retext-stringify@3.1.0: + resolution: {integrity: sha512-767TLOaoXFXyOnjx/EggXlb37ZD2u4P1n0GJqVdpipqACsQP+20W+BNpMYrlJkq7hxffnFk+jc6mAK9qrbuB8w==} + + retext@8.1.0: + resolution: {integrity: sha512-N9/Kq7YTn6ZpzfiGW45WfEGJqFf1IM1q8OsRa1CGzIebCJBNCANDRmOrholiDRGKo/We7ofKR4SEvcGAWEMD3Q==} + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + + scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shiki@1.6.1: + resolution: {integrity: sha512-1Pu/A1rtsG6HZvQm4W0NExQ45e02og+rPog7PDaFDiMumZgOYnZIu4JtGQeAIfMwdbKSjJQoCUr79vDLKUUxWA==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speech-rule-engine@4.0.7: + resolution: {integrity: sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==} + hasBin: true + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + stringify-entities@4.0.3: + resolution: {integrity: sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + style-to-object@0.4.4: + resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} + + style-to-object@1.0.6: + resolution: {integrity: sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + stylis@4.3.1: + resolution: {integrity: sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==} + + supports-color@4.5.0: + resolution: {integrity: sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==} + engines: {node: '>=4'} + + title@3.5.3: + resolution: {integrity: sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==} + hasBin: true + + titleize@1.0.0: + resolution: {integrity: sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw==} + engines: {node: '>=0.10.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.1.0: + resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + tslib@2.4.1: + resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} + + twoslash-protocol@0.2.6: + resolution: {integrity: sha512-8NbJlYeRdBcCTQ7ui7pdRPC1NL16aOnoYNz06oBW+W0qWNuiQXHgE8UeNvbA038aDd6ZPuuD5WedsBIESocB4g==} + + twoslash@0.2.6: + resolution: {integrity: sha512-DcAKIyXMB6xNs+SOw/oF8GvUr/cfJSqznngVXYbAUIVfTW3M8vWSEoCaz/RgSD+M6vwtK8DJ4/FmYBF5MN8BGw==} + peerDependencies: + typescript: '*' + + typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + unherit@3.0.1: + resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==} + + unified@10.1.2: + resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} + + unified@11.0.4: + resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@5.1.1: + resolution: {integrity: sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-modify-children@3.1.1: + resolution: {integrity: sha512-yXi4Lm+TG5VG+qvokP6tpnk+r1EPwyYL04JWDxLvgvPV40jANh7nm3udk65OOWquvbMDe+PL9+LmkxDpTv/7BA==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-remove@4.0.0: + resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==} + + unist-util-stringify-position@3.0.2: + resolution: {integrity: sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@2.0.2: + resolution: {integrity: sha512-+LWpMFqyUwLGpsQxpumsQ9o9DG2VGLFrpz+rpVXYIEdPy57GSy5HioC0g3bg/8WP9oCLlapQtklOzQ8uLS496Q==} + + unist-util-visit-parents@4.1.1: + resolution: {integrity: sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==} + + unist-util-visit-parents@5.1.1: + resolution: {integrity: sha512-gks4baapT/kNRaWxuGkl5BIhoanZo7sC/cUT/JToSRNL1dYoXRFl75d++NkjYk4TAu2uv2Px+l8guMajogeuiw==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@3.1.0: + resolution: {integrity: sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==} + + unist-util-visit@4.1.1: + resolution: {integrity: sha512-n9KN3WV9k4h1DxYR1LoajgN93wpEi/7ZplVe02IoB4gH5ctI1AaF2670BLHQYbwj+pY83gFtyeySFiyMHJklrg==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + + vfile-location@5.0.2: + resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} + + vfile-message@3.1.3: + resolution: {integrity: sha512-0yaU+rj2gKAyEk12ffdSbBfjnnj+b1zqTBv3OQCTn8yEB02bsPizwdBPrLJjHnK+cU9EMMcUnNv938XcZIkmdA==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@5.3.6: + resolution: {integrity: sha512-ADBsmerdGBs2WYckrLBEmuETSPyTD4TuLxTrw0DvjirxW1ra4ZwkbzG8ndsv3Q57smvHxo677MHaQrY9yxH8cA==} + + vfile@6.0.1: + resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + web-worker@1.3.0: + resolution: {integrity: sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + wicked-good-xpath@1.3.0: + resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} + + xmldom-sre@0.1.31: + resolution: {integrity: sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==} + engines: {node: '>=0.1'} + + yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + + yaml@2.4.2: + resolution: {integrity: sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==} + engines: {node: '>= 14'} + hasBin: true + + yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + + zod-validation-error@1.5.0: + resolution: {integrity: sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw==} + engines: {node: '>=16.0.0'} + peerDependencies: + zod: ^3.18.0 + + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@braintree/sanitize-url@6.0.4': {} + + '@headlessui/react@1.7.18(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@tanstack/react-virtual': 3.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + client-only: 0.0.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@mdx-js/mdx@3.0.1': + dependencies: + '@types/estree': 1.0.0 + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.3 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-build-jsx: 3.0.1 + estree-util-is-identifier-name: 3.0.0 + estree-util-to-js: 2.0.0 + estree-walker: 3.0.1 + hast-util-to-estree: 3.1.0 + hast-util-to-jsx-runtime: 2.3.0 + markdown-extensions: 2.0.0 + periscopic: 3.0.4 + remark-mdx: 3.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.0 + source-map: 0.7.4 + unified: 11.0.4 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + transitivePeerDependencies: + - supports-color + + '@mdx-js/react@3.0.1(@types/react@18.0.26)(react@18.2.0)': + dependencies: + '@types/mdx': 2.0.3 + '@types/react': 18.0.26 + react: 18.2.0 + + '@napi-rs/simple-git-android-arm-eabi@0.1.16': + optional: true + + '@napi-rs/simple-git-android-arm64@0.1.16': + optional: true + + '@napi-rs/simple-git-darwin-arm64@0.1.16': + optional: true + + '@napi-rs/simple-git-darwin-x64@0.1.16': + optional: true + + '@napi-rs/simple-git-linux-arm-gnueabihf@0.1.16': + optional: true + + '@napi-rs/simple-git-linux-arm64-gnu@0.1.16': + optional: true + + '@napi-rs/simple-git-linux-arm64-musl@0.1.16': + optional: true + + '@napi-rs/simple-git-linux-x64-gnu@0.1.16': + optional: true + + '@napi-rs/simple-git-linux-x64-musl@0.1.16': + optional: true + + '@napi-rs/simple-git-win32-arm64-msvc@0.1.16': + optional: true + + '@napi-rs/simple-git-win32-x64-msvc@0.1.16': + optional: true + + '@napi-rs/simple-git@0.1.16': + optionalDependencies: + '@napi-rs/simple-git-android-arm-eabi': 0.1.16 + '@napi-rs/simple-git-android-arm64': 0.1.16 + '@napi-rs/simple-git-darwin-arm64': 0.1.16 + '@napi-rs/simple-git-darwin-x64': 0.1.16 + '@napi-rs/simple-git-linux-arm-gnueabihf': 0.1.16 + '@napi-rs/simple-git-linux-arm64-gnu': 0.1.16 + '@napi-rs/simple-git-linux-arm64-musl': 0.1.16 + '@napi-rs/simple-git-linux-x64-gnu': 0.1.16 + '@napi-rs/simple-git-linux-x64-musl': 0.1.16 + '@napi-rs/simple-git-win32-arm64-msvc': 0.1.16 + '@napi-rs/simple-git-win32-x64-msvc': 0.1.16 + + '@next/env@14.1.1': {} + + '@next/swc-darwin-arm64@14.1.1': + optional: true + + '@next/swc-darwin-x64@14.1.1': + optional: true + + '@next/swc-linux-arm64-gnu@14.1.1': + optional: true + + '@next/swc-linux-arm64-musl@14.1.1': + optional: true + + '@next/swc-linux-x64-gnu@14.1.1': + optional: true + + '@next/swc-linux-x64-musl@14.1.1': + optional: true + + '@next/swc-win32-arm64-msvc@14.1.1': + optional: true + + '@next/swc-win32-ia32-msvc@14.1.1': + optional: true + + '@next/swc-win32-x64-msvc@14.1.1': + optional: true + + '@popperjs/core@2.11.8': {} + + '@shikijs/core@1.6.1': {} + + '@shikijs/twoslash@1.6.1(typescript@5.3.3)': + dependencies: + '@shikijs/core': 1.6.1 + twoslash: 0.2.6(typescript@5.3.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@swc/helpers@0.5.2': + dependencies: + tslib: 2.4.1 + + '@tanstack/react-virtual@3.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@tanstack/virtual-core': 3.1.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@tanstack/virtual-core@3.1.3': {} + + '@theguild/remark-mermaid@0.0.5(react@18.2.0)': + dependencies: + mermaid: 10.8.0 + react: 18.2.0 + unist-util-visit: 5.0.0 + transitivePeerDependencies: + - supports-color + + '@theguild/remark-npm2yarn@0.3.0': + dependencies: + npm-to-yarn: 2.2.1 + unist-util-visit: 5.0.0 + + '@types/acorn@4.0.6': + dependencies: + '@types/estree': 1.0.0 + + '@types/d3-scale-chromatic@3.0.3': {} + + '@types/d3-scale@4.0.8': + dependencies: + '@types/d3-time': 3.0.3 + + '@types/d3-time@3.0.3': {} + + '@types/debug@4.1.7': + dependencies: + '@types/ms': 0.7.31 + + '@types/estree-jsx@1.0.0': + dependencies: + '@types/estree': 1.0.0 + + '@types/estree@1.0.0': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.2 + + '@types/katex@0.16.7': {} + + '@types/mdast@3.0.10': + dependencies: + '@types/unist': 3.0.2 + + '@types/mdast@4.0.3': + dependencies: + '@types/unist': 3.0.2 + + '@types/mdx@2.0.3': {} + + '@types/ms@0.7.31': {} + + '@types/nlcst@1.0.4': + dependencies: + '@types/unist': 2.0.6 + + '@types/node@20.11.24': + dependencies: + undici-types: 5.26.5 + + '@types/prop-types@15.7.5': {} + + '@types/react@18.0.26': + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.2 + csstype: 3.1.1 + + '@types/scheduler@0.16.2': {} + + '@types/unist@2.0.6': {} + + '@types/unist@3.0.2': {} + + '@typescript/vfs@1.5.0': + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@ungap/structured-clone@1.2.0': {} + + acorn-jsx@5.3.2(acorn@8.8.1): + dependencies: + acorn: 8.8.1 + + acorn@8.8.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + arch@2.2.0: {} + + arg@1.0.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + array-iterate@2.0.1: {} + + astring@1.8.3: {} + + bail@2.0.2: {} + + better-react-mathjax@2.0.3(react@18.2.0): + dependencies: + mathjax-full: 3.2.2 + react: 18.2.0 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + caniuse-lite@1.0.30001591: {} + + ccount@2.0.1: {} + + chalk@2.3.0: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 4.5.0 + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + client-only@0.0.1: {} + + clipboardy@1.2.2: + dependencies: + arch: 2.2.0 + execa: 0.8.0 + + clsx@2.1.0: {} + + collapse-white-space@2.1.0: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-name@1.1.3: {} + + comma-separated-tokens@2.0.3: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + commander@9.2.0: {} + + compute-scroll-into-view@3.1.0: {} + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cross-spawn@5.1.0: + dependencies: + lru-cache: 4.1.5 + shebang-command: 1.2.0 + which: 1.3.1 + + csstype@3.1.1: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.28.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.28.1 + + cytoscape@3.28.1: + dependencies: + heap: 0.2.7 + lodash: 4.17.21 + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.0.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.8.5: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.0 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.0.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.10: + dependencies: + d3: 7.8.5 + lodash-es: 4.17.21 + + dayjs@1.11.10: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@5.1.0: {} + + dompurify@3.0.9: {} + + elkjs@0.9.2: {} + + entities@4.5.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@5.0.0: {} + + esm@3.2.25: {} + + esprima@4.0.1: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.0 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.1 + + estree-util-is-identifier-name@2.0.1: {} + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.0 + astring: 1.8.3 + source-map: 0.7.4 + + estree-util-value-to-estree@1.3.0: + dependencies: + is-plain-obj: 3.0.0 + + estree-util-value-to-estree@3.1.1: + dependencies: + '@types/estree': 1.0.0 + is-plain-obj: 4.1.0 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.0 + '@types/unist': 3.0.2 + + estree-walker@3.0.1: {} + + execa@0.8.0: + dependencies: + cross-spawn: 5.1.0 + get-stream: 3.0.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + fault@2.0.1: + dependencies: + format: 0.2.2 + + flexsearch@0.7.43: {} + + focus-visible@5.2.0: {} + + format@0.2.2: {} + + get-stream@3.0.0: {} + + github-slugger@2.0.0: {} + + graceful-fs@4.2.11: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + has-flag@2.0.0: {} + + hast-util-from-dom@5.0.0: + dependencies: + '@types/hast': 3.0.4 + hastscript: 8.0.0 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.0 + hast-util-from-html: 2.0.1 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.1: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.1 + parse5: 7.1.2 + vfile: 6.0.1 + vfile-message: 4.0.2 + + hast-util-from-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + devlop: 1.1.0 + hastscript: 8.0.0 + property-information: 6.2.0 + vfile: 6.0.1 + vfile-location: 5.0.2 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + '@ungap/structured-clone': 1.2.0 + hast-util-from-parse5: 8.0.1 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.1.0 + parse5: 7.1.2 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.0: + dependencies: + '@types/estree': 1.0.0 + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.0 + mdast-util-mdx-jsx: 3.1.2 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.2.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.4 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-jsx-runtime@2.3.0: + dependencies: + '@types/estree': 1.0.0 + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.0 + mdast-util-mdx-jsx: 3.1.2 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.2.0 + space-separated-tokens: 2.0.2 + style-to-object: 1.0.6 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.2.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-string@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-to-text@4.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 6.2.0 + space-separated-tokens: 2.0.2 + + heap@0.2.7: {} + + html-void-elements@3.0.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + inline-style-parser@0.1.1: {} + + inline-style-parser@0.2.3: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + intersection-observer@0.12.2: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-buffer@2.0.5: {} + + is-decimal@2.0.1: {} + + is-extendable@0.1.1: {} + + is-hexadecimal@2.0.1: {} + + is-plain-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-reference@3.0.0: + dependencies: + '@types/estree': 1.0.0 + + is-stream@1.1.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + katex@0.16.9: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + kind-of@6.0.3: {} + + kleur@4.1.5: {} + + layout-base@1.0.2: {} + + lodash-es@4.17.21: {} + + lodash@4.17.21: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@4.1.5: + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + + markdown-extensions@2.0.0: {} + + markdown-table@3.0.3: {} + + mathjax-full@3.2.2: + dependencies: + esm: 3.2.25 + mhchemparser: 4.2.1 + mj-context-menu: 0.6.1 + speech-rule-engine: 4.0.7 + + mdast-util-find-and-replace@3.0.1: + dependencies: + '@types/mdast': 4.0.3 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@1.3.1: + dependencies: + '@types/mdast': 3.0.10 + '@types/unist': 2.0.6 + decode-named-character-reference: 1.0.2 + mdast-util-to-string: 3.1.0 + micromark: 3.1.0 + micromark-util-decode-numeric-character-reference: 1.0.0 + micromark-util-decode-string: 1.0.2 + micromark-util-normalize-identifier: 1.0.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + unist-util-stringify-position: 3.0.2 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + mdast-util-from-markdown@2.0.1: + dependencies: + '@types/mdast': 4.0.3 + '@types/unist': 3.0.2 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.3 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.0: + dependencies: + '@types/mdast': 4.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.1.0 + + mdast-util-gfm-footnote@2.0.0: + dependencies: + '@types/mdast': 4.0.3 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.3 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.3 + devlop: 1.1.0 + markdown-table: 3.0.3 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.3 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.1 + mdast-util-gfm-autolink-literal: 2.0.0 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.3 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.3 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.1.2: + dependencies: + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.3 + '@types/unist': 3.0.2 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + parse-entities: 4.0.0 + stringify-entities: 4.0.3 + unist-util-remove-position: 5.0.0 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.1 + mdast-util-mdx-expression: 2.0.0 + mdast-util-mdx-jsx: 3.1.2 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.3 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.3 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.3 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + + mdast-util-to-markdown@2.1.0: + dependencies: + '@types/mdast': 4.0.3 + '@types/unist': 3.0.2 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-decode-string: 2.0.0 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@3.1.0: {} + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.3 + + mermaid@10.8.0: + dependencies: + '@braintree/sanitize-url': 6.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-scale-chromatic': 3.0.3 + cytoscape: 3.28.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.28.1) + d3: 7.8.5 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.10 + dayjs: 1.11.10 + dompurify: 3.0.9 + elkjs: 0.9.2 + khroma: 2.1.0 + lodash-es: 4.17.21 + mdast-util-from-markdown: 1.3.1 + non-layered-tidy-tree-layout: 2.0.2 + stylis: 4.3.1 + ts-dedent: 2.2.0 + uuid: 9.0.1 + web-worker: 1.3.0 + transitivePeerDependencies: + - supports-color + + mhchemparser@4.2.1: {} + + micromark-core-commonmark@1.0.6: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-factory-destination: 1.0.0 + micromark-factory-label: 1.0.2 + micromark-factory-space: 1.0.0 + micromark-factory-title: 1.0.2 + micromark-factory-whitespace: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-chunked: 1.0.0 + micromark-util-classify-character: 1.0.0 + micromark-util-html-tag-name: 1.1.0 + micromark-util-normalize-identifier: 1.0.0 + micromark-util-resolve-all: 1.0.0 + micromark-util-subtokenize: 1.0.2 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + micromark-core-commonmark@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.0 + micromark-factory-label: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-factory-title: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-html-tag-name: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-autolink-literal@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-footnote@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-strikethrough@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-table@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-extension-gfm-task-list-item@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.0.0 + micromark-extension-gfm-footnote: 2.0.0 + micromark-extension-gfm-strikethrough: 2.0.0 + micromark-extension-gfm-table: 2.0.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.0.1 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-math@3.0.0: + dependencies: + '@types/katex': 0.16.7 + devlop: 1.1.0 + katex: 0.16.9 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-mdx-expression@3.0.0: + dependencies: + '@types/estree': 1.0.0 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-mdx-jsx@3.0.0: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + vfile-message: 4.0.2 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.8.1 + acorn-jsx: 5.3.2(acorn@8.8.1) + micromark-extension-mdx-expression: 3.0.0 + micromark-extension-mdx-jsx: 3.0.0 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-destination@1.0.0: + dependencies: + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + + micromark-factory-destination@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-label@1.0.2: + dependencies: + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + micromark-factory-label@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-mdx-expression@2.0.1: + dependencies: + '@types/estree': 1.0.0 + devlop: 1.1.0 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-factory-space@1.0.0: + dependencies: + micromark-util-character: 1.1.0 + micromark-util-types: 1.0.2 + + micromark-factory-space@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-types: 2.0.0 + + micromark-factory-title@1.0.2: + dependencies: + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + micromark-factory-title@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-whitespace@1.0.0: + dependencies: + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + + micromark-factory-whitespace@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-character@1.1.0: + dependencies: + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + + micromark-util-character@2.1.0: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-chunked@1.0.0: + dependencies: + micromark-util-symbol: 1.0.1 + + micromark-util-chunked@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-classify-character@1.0.0: + dependencies: + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + + micromark-util-classify-character@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-combine-extensions@1.0.0: + dependencies: + micromark-util-chunked: 1.0.0 + micromark-util-types: 1.0.2 + + micromark-util-combine-extensions@2.0.0: + dependencies: + micromark-util-chunked: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-decode-numeric-character-reference@1.0.0: + dependencies: + micromark-util-symbol: 1.0.1 + + micromark-util-decode-numeric-character-reference@2.0.1: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-decode-string@1.0.2: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.0.0 + micromark-util-symbol: 1.0.1 + + micromark-util-decode-string@2.0.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-symbol: 2.0.0 + + micromark-util-encode@1.0.1: {} + + micromark-util-encode@2.0.0: {} + + micromark-util-events-to-acorn@2.0.2: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.0 + '@types/unist': 3.0.2 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + vfile-message: 4.0.2 + + micromark-util-html-tag-name@1.1.0: {} + + micromark-util-html-tag-name@2.0.0: {} + + micromark-util-normalize-identifier@1.0.0: + dependencies: + micromark-util-symbol: 1.0.1 + + micromark-util-normalize-identifier@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-resolve-all@1.0.0: + dependencies: + micromark-util-types: 1.0.2 + + micromark-util-resolve-all@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-util-sanitize-uri@1.1.0: + dependencies: + micromark-util-character: 1.1.0 + micromark-util-encode: 1.0.1 + micromark-util-symbol: 1.0.1 + + micromark-util-sanitize-uri@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + micromark-util-subtokenize@1.0.2: + dependencies: + micromark-util-chunked: 1.0.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + micromark-util-subtokenize@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-symbol@1.0.1: {} + + micromark-util-symbol@2.0.0: {} + + micromark-util-types@1.0.2: {} + + micromark-util-types@2.0.0: {} + + micromark@3.1.0: + dependencies: + '@types/debug': 4.1.7 + debug: 4.3.4 + decode-named-character-reference: 1.0.2 + micromark-core-commonmark: 1.0.6 + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-chunked: 1.0.0 + micromark-util-combine-extensions: 1.0.0 + micromark-util-decode-numeric-character-reference: 1.0.0 + micromark-util-encode: 1.0.1 + micromark-util-normalize-identifier: 1.0.0 + micromark-util-resolve-all: 1.0.0 + micromark-util-sanitize-uri: 1.1.0 + micromark-util-subtokenize: 1.0.2 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + micromark@4.0.0: + dependencies: + '@types/debug': 4.1.7 + debug: 4.3.4 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + transitivePeerDependencies: + - supports-color + + mj-context-menu@0.6.1: {} + + mri@1.2.0: {} + + ms@2.1.2: {} + + nanoid@3.3.7: {} + + next-themes@0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + next@14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@next/env': 14.1.1 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001591 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) + optionalDependencies: + '@next/swc-darwin-arm64': 14.1.1 + '@next/swc-darwin-x64': 14.1.1 + '@next/swc-linux-arm64-gnu': 14.1.1 + '@next/swc-linux-arm64-musl': 14.1.1 + '@next/swc-linux-x64-gnu': 14.1.1 + '@next/swc-linux-x64-musl': 14.1.1 + '@next/swc-win32-arm64-msvc': 14.1.1 + '@next/swc-win32-ia32-msvc': 14.1.1 + '@next/swc-win32-x64-msvc': 14.1.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + nextra-theme-docs@3.0.0-alpha.24(next@14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nextra@3.0.0-alpha.24(@types/react@18.0.26)(next@14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.3.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@headlessui/react': 1.7.18(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@popperjs/core': 2.11.8 + clsx: 2.1.0 + escape-string-regexp: 5.0.0 + flexsearch: 0.7.43 + focus-visible: 5.2.0 + intersection-observer: 0.12.2 + next: 14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next-themes: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + nextra: 3.0.0-alpha.24(@types/react@18.0.26)(next@14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.3.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + scroll-into-view-if-needed: 3.1.0 + zod: 3.22.4 + + nextra@3.0.0-alpha.24(@types/react@18.0.26)(next@14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.3.3): + dependencies: + '@headlessui/react': 1.7.18(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mdx-js/mdx': 3.0.1 + '@mdx-js/react': 3.0.1(@types/react@18.0.26)(react@18.2.0) + '@napi-rs/simple-git': 0.1.16 + '@shikijs/twoslash': 1.6.1(typescript@5.3.3) + '@theguild/remark-mermaid': 0.0.5(react@18.2.0) + '@theguild/remark-npm2yarn': 0.3.0 + better-react-mathjax: 2.0.3(react@18.2.0) + clsx: 2.1.0 + estree-util-to-js: 2.0.0 + estree-util-value-to-estree: 3.1.1 + github-slugger: 2.0.0 + graceful-fs: 4.2.11 + gray-matter: 4.0.3 + hast-util-to-estree: 3.1.0 + katex: 0.16.9 + next: 14.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + p-limit: 4.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + rehype-katex: 7.0.0 + rehype-pretty-code: 0.13.0(shiki@1.6.1) + rehype-raw: 7.0.0 + remark-frontmatter: 5.0.0 + remark-gfm: 4.0.0 + remark-math: 6.0.0 + remark-reading-time: 2.0.1 + remark-smartypants: 2.1.0 + shiki: 1.6.1 + slash: 5.1.0 + title: 3.5.3 + unist-util-remove: 4.0.0 + unist-util-visit: 5.0.0 + yaml: 2.4.2 + zod: 3.22.4 + zod-validation-error: 1.5.0(zod@3.22.4) + transitivePeerDependencies: + - '@types/react' + - supports-color + - typescript + + nlcst-to-string@3.1.1: + dependencies: + '@types/nlcst': 1.0.4 + + non-layered-tidy-tree-layout@2.0.2: {} + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + npm-to-yarn@2.2.1: {} + + p-finally@1.0.0: {} + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.0.0 + + parse-entities@4.0.0: + dependencies: + '@types/unist': 2.0.6 + character-entities: 2.0.2 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-latin@5.0.1: + dependencies: + nlcst-to-string: 3.1.1 + unist-util-modify-children: 3.1.1 + unist-util-visit-children: 2.0.2 + + parse-numeric-range@1.3.0: {} + + parse5@7.1.2: + dependencies: + entities: 4.5.0 + + path-key@2.0.1: {} + + periscopic@3.0.4: + dependencies: + estree-walker: 3.0.1 + is-reference: 3.0.0 + + picocolors@1.0.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + + property-information@6.2.0: {} + + pseudomap@1.0.2: {} + + react-dom@18.2.0(react@18.2.0): + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + + react@18.2.0: + dependencies: + loose-envify: 1.4.0 + + reading-time@1.5.0: {} + + rehype-katex@7.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.7 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.0 + katex: 0.16.9 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.1 + + rehype-parse@9.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.1 + unified: 11.0.4 + + rehype-pretty-code@0.13.0(shiki@1.6.1): + dependencies: + '@types/hast': 3.0.4 + hast-util-to-string: 3.0.0 + parse-numeric-range: 1.3.0 + rehype-parse: 9.0.0 + shiki: 1.6.1 + unified: 11.0.4 + unist-util-visit: 5.0.0 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.0.2 + vfile: 6.0.1 + + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.3 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.0: + dependencies: + '@types/mdast': 4.0.3 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.3 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.0.0 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.0.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.3 + mdast-util-from-markdown: 2.0.1 + micromark-util-types: 2.0.0 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + + remark-reading-time@2.0.1: + dependencies: + estree-util-is-identifier-name: 2.0.1 + estree-util-value-to-estree: 1.3.0 + reading-time: 1.5.0 + unist-util-visit: 3.1.0 + + remark-rehype@11.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.3 + mdast-util-to-hast: 13.1.0 + unified: 11.0.4 + vfile: 6.0.1 + + remark-smartypants@2.1.0: + dependencies: + retext: 8.1.0 + retext-smartypants: 5.2.0 + unist-util-visit: 5.0.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.3 + mdast-util-to-markdown: 2.1.0 + unified: 11.0.4 + + retext-latin@3.1.0: + dependencies: + '@types/nlcst': 1.0.4 + parse-latin: 5.0.1 + unherit: 3.0.1 + unified: 10.1.2 + + retext-smartypants@5.2.0: + dependencies: + '@types/nlcst': 1.0.4 + nlcst-to-string: 3.1.1 + unified: 10.1.2 + unist-util-visit: 4.1.1 + + retext-stringify@3.1.0: + dependencies: + '@types/nlcst': 1.0.4 + nlcst-to-string: 3.1.1 + unified: 10.1.2 + + retext@8.1.0: + dependencies: + '@types/nlcst': 1.0.4 + retext-latin: 3.1.0 + retext-stringify: 3.1.0 + unified: 10.1.2 + + robust-predicates@3.0.2: {} + + rw@1.3.3: {} + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safer-buffer@2.1.2: {} + + scheduler@0.23.0: + dependencies: + loose-envify: 1.4.0 + + scroll-into-view-if-needed@3.1.0: + dependencies: + compute-scroll-into-view: 3.1.0 + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-regex@1.0.0: {} + + shiki@1.6.1: + dependencies: + '@shikijs/core': 1.6.1 + + signal-exit@3.0.7: {} + + slash@5.1.0: {} + + source-map-js@1.0.2: {} + + source-map@0.7.4: {} + + space-separated-tokens@2.0.2: {} + + speech-rule-engine@4.0.7: + dependencies: + commander: 9.2.0 + wicked-good-xpath: 1.3.0 + xmldom-sre: 0.1.31 + + sprintf-js@1.0.3: {} + + streamsearch@1.1.0: {} + + stringify-entities@4.0.3: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-bom-string@1.0.0: {} + + strip-eof@1.0.0: {} + + style-to-object@0.4.4: + dependencies: + inline-style-parser: 0.1.1 + + style-to-object@1.0.6: + dependencies: + inline-style-parser: 0.2.3 + + styled-jsx@5.1.1(react@18.2.0): + dependencies: + client-only: 0.0.1 + react: 18.2.0 + + stylis@4.3.1: {} + + supports-color@4.5.0: + dependencies: + has-flag: 2.0.0 + + title@3.5.3: + dependencies: + arg: 1.0.0 + chalk: 2.3.0 + clipboardy: 1.2.2 + titleize: 1.0.0 + + titleize@1.0.0: {} + + trim-lines@3.0.1: {} + + trough@2.1.0: {} + + ts-dedent@2.2.0: {} + + tslib@2.4.1: {} + + twoslash-protocol@0.2.6: {} + + twoslash@0.2.6(typescript@5.3.3): + dependencies: + '@typescript/vfs': 1.5.0 + twoslash-protocol: 0.2.6 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + + typescript@5.3.3: {} + + undici-types@5.26.5: {} + + unherit@3.0.1: {} + + unified@10.1.2: + dependencies: + '@types/unist': 2.0.6 + bail: 2.0.2 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 4.1.0 + trough: 2.1.0 + vfile: 5.3.6 + + unified@11.0.4: + dependencies: + '@types/unist': 3.0.2 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.1.0 + vfile: 6.0.1 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + + unist-util-is@5.1.1: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.2 + + unist-util-modify-children@3.1.1: + dependencies: + '@types/unist': 2.0.6 + array-iterate: 2.0.1 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.2 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.2 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.2 + unist-util-visit: 5.0.0 + + unist-util-remove@4.0.0: + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + unist-util-stringify-position@3.0.2: + dependencies: + '@types/unist': 2.0.6 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.2 + + unist-util-visit-children@2.0.2: + dependencies: + '@types/unist': 2.0.6 + + unist-util-visit-parents@4.1.1: + dependencies: + '@types/unist': 2.0.6 + unist-util-is: 5.1.1 + + unist-util-visit-parents@5.1.1: + dependencies: + '@types/unist': 2.0.6 + unist-util-is: 5.1.1 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + + unist-util-visit@3.1.0: + dependencies: + '@types/unist': 2.0.6 + unist-util-is: 5.1.1 + unist-util-visit-parents: 4.1.1 + + unist-util-visit@4.1.1: + dependencies: + '@types/unist': 2.0.6 + unist-util-is: 5.1.1 + unist-util-visit-parents: 5.1.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + uuid@9.0.1: {} + + uvu@0.5.6: + dependencies: + dequal: 2.0.3 + diff: 5.1.0 + kleur: 4.1.5 + sade: 1.8.1 + + vfile-location@5.0.2: + dependencies: + '@types/unist': 3.0.2 + vfile: 6.0.1 + + vfile-message@3.1.3: + dependencies: + '@types/unist': 2.0.6 + unist-util-stringify-position: 3.0.2 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + + vfile@5.3.6: + dependencies: + '@types/unist': 2.0.6 + is-buffer: 2.0.5 + unist-util-stringify-position: 3.0.2 + vfile-message: 3.1.3 + + vfile@6.0.1: + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + + web-namespaces@2.0.1: {} + + web-worker@1.3.0: {} + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + wicked-good-xpath@1.3.0: {} + + xmldom-sre@0.1.31: {} + + yallist@2.1.2: {} + + yaml@2.4.2: {} + + yocto-queue@1.0.0: {} + + zod-validation-error@1.5.0(zod@3.22.4): + dependencies: + zod: 3.22.4 + + zod@3.22.4: {} + + zwitch@2.0.4: {} diff --git a/docs/static/hyperglass-dark.svg b/docs/public/hyperglass-dark.svg similarity index 100% rename from docs/static/hyperglass-dark.svg rename to docs/public/hyperglass-dark.svg diff --git a/docs/static/hyperglass-icon-dark.svg b/docs/public/hyperglass-icon-dark.svg similarity index 100% rename from docs/static/hyperglass-icon-dark.svg rename to docs/public/hyperglass-icon-dark.svg diff --git a/docs/static/hyperglass-icon-light.svg b/docs/public/hyperglass-icon-light.svg similarity index 100% rename from docs/static/hyperglass-icon-light.svg rename to docs/public/hyperglass-icon-light.svg diff --git a/docs/static/hyperglass-light.svg b/docs/public/hyperglass-light.svg similarity index 100% rename from docs/static/hyperglass-light.svg rename to docs/public/hyperglass-light.svg diff --git a/docs/public/hyperglass.svg b/docs/public/hyperglass.svg new file mode 100644 index 000000000..bcfd80da0 --- /dev/null +++ b/docs/public/hyperglass.svg @@ -0,0 +1,21 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2759.9 544.87"> + <g fill="currentColor"> + <g> + <path d="M931.39,683.15q20.28,21.23,20.28,62.74l0,134.13c0,3.35-.85,5.94-2.51,7.76a10.52,10.52,0,0,1-14.15,0c-1.67-1.82-2.51-4.4-2.5-7.75l0-131.86q0-35.58-14.12-51.79t-45.62-16.21q-35.13,0-56.35,21.65t-21.24,57.71l0,120.44q0,5-2.51,7.76a9.12,9.12,0,0,1-7.07,2.74,9,9,0,0,1-6.84-3A10.66,10.66,0,0,1,776,880l.12-305.23q0-5,2.51-7.76a10.52,10.52,0,0,1,14.15,0c1.66,1.83,2.5,4.41,2.5,7.76l0,131.39a71.74,71.74,0,0,1,30.13-32.83q20.07-11.4,46.08-11.39Q911.09,661.93,931.39,683.15Z" transform="translate(-102 -476.45)" /> + <path d="M1201.26,663a9.19,9.19,0,0,1,6.39,2.51,8.56,8.56,0,0,1,2.73,6.62,14.35,14.35,0,0,1-.91,4.1l-131,290.12q-2.75,5.48-8.22,5.47a8.83,8.83,0,0,1-6.16-2.28,7.66,7.66,0,0,1-2.5-5.93l.91-3.65,32.88-72.08L999.6,676.1a14.33,14.33,0,0,1-.91-4.11,8.65,8.65,0,0,1,3.19-6.84,10.93,10.93,0,0,1,7.3-2.73q5,0,7.76,5.48l88,198.49.91-2.28,86.76-194.78C1194.42,665.08,1197.31,662.94,1201.26,663Z" transform="translate(-102 -476.45)" /> + <path d="M1406.57,676.48q22.34,14.38,34.66,40.39t12.29,60.69q0,34.22-12.34,60t-34.69,39.91q-22.37,14.13-51.56,14.12-27.84,0-47.67-12.33t-29.88-35.6l0,118.62c0,3.35-.84,5.93-2.51,7.76a9.15,9.15,0,0,1-7.07,2.73q-9.59,0-9.58-10.5l.11-289.25a10.34,10.34,0,0,1,2.51-7.3,10.49,10.49,0,0,1,14.14,0,10.42,10.42,0,0,1,2.51,7.3v36q10-22.81,29.89-34.9T1355,662.09Q1384.21,662.11,1406.57,676.48ZM1412,848q20.54-24.85,20.55-70.48T1412,706.37q-20.52-25.56-57-25.57-37.41,0-57.5,24.84t-20.1,70.94q0,46.09,20,71.18t57,25.12Q1391.44,872.9,1412,848Z" transform="translate(-102 -476.45)" /> + <path d="M1688.91,847.68a8.28,8.28,0,0,1,2.28,5.71,14.17,14.17,0,0,1-3.43,8.89q-3.42,4.35-12.09,9.81-32.85,19.6-68,19.59-49.73,0-78.23-30.6t-28.49-84q0-34.22,12.8-60.22t36.06-40.36q23.28-14.37,53.39-14.35,42,0,66.14,27.4t24.15,74.83q0,8.21-3.19,11.86t-11.41,3.65l-157-.06q.43,44.71,22.56,68.9t62.27,24.2q21.9,0,36.27-5.91a146.07,146.07,0,0,0,28.07-15.51q9.57-6.37,12.32-6.38A7.16,7.16,0,0,1,1688.91,847.68Zm-141.61-146q-21.22,21.68-24.89,60.44l152.85.06q-.91-39.69-19.6-60.91T1603.2,680Q1568.52,680,1547.3,701.63Z" transform="translate(-102 -476.45)" /> + <path d="M1855.05,663.19c3.65,0,6.16.69,7.52,2.06s2.06,3.42,2.06,6.16q0,7.77-9.59,9.12l-13.69,1.37q-33.3,3.18-50.19,24.61a77.43,77.43,0,0,0-16.9,49.27l0,124.56q0,5-2.51,7.75a10.5,10.5,0,0,1-14.15,0c-1.67-1.83-2.5-4.41-2.5-7.76l.08-206.68q0-10.49,9.58-10.49a9.25,9.25,0,0,1,6.84,2.74c1.83,1.83,2.74,4.41,2.74,7.76v32.85q19.62-39.22,71.19-42.86l6.38-.46Z" transform="translate(-102 -476.45)" /> + <path d="M2078.6,666a9.84,9.84,0,0,1,2.74,7.3l-.08,204.85q0,47.45-22.38,71.62T1992.26,974q-24.65,0-43.34-4.58a127.8,127.8,0,0,1-36-15.07q-8.67-5.47-12.32-9.81a13.79,13.79,0,0,1-3.64-8.9,7.82,7.82,0,0,1,8.21-8.21q2.28,0,11.86,5.48a204.67,204.67,0,0,0,32.85,16q16.86,6.39,43.34,6.4,33.75,0,51.56-18.91t17.81-55.43l0-47.9q-9.59,23.71-29.9,36.26t-49.05,12.53q-29.2,0-51.78-13.71t-35.11-38.8q-12.54-25.08-12.53-57.49t12.57-57.25q12.56-24.85,35.15-38.54t51.79-13.67q28.27,0,48.35,12.34t30.1,35.14l0-36q0-10.5,9.58-10.49A9.25,9.25,0,0,1,2078.6,666Zm-37.25,172.67q20.78-24.39,20.79-66.83t-20.73-66.62q-20.76-24.18-57.71-24.2-36.51,0-57.72,24.16t-21.25,66.6q0,42.88,21.19,67.08t57.71,24.2Q2020.59,863.1,2041.35,838.69Z" transform="translate(-102 -476.45)" /> + <path d="M2161.32,888.24c-1.67-1.83-2.51-4.41-2.5-7.76l.11-305.22q0-5,2.51-7.76a9.09,9.09,0,0,1,7.08-2.73,9,9,0,0,1,6.84,3,10.73,10.73,0,0,1,2.73,7.53L2178,880.49a10.75,10.75,0,0,1-2.74,7.53,9,9,0,0,1-6.85,3A9.09,9.09,0,0,1,2161.32,888.24Z" transform="translate(-102 -476.45)" /> + <path d="M2416.64,744.16l0,136q0,5-2.51,7.76a9.12,9.12,0,0,1-7.08,2.73,9.45,9.45,0,0,1-7.29-3,10.66,10.66,0,0,1-2.74-7.53V848.18q-10,21-29,32.38T2324.88,892a84.41,84.41,0,0,1-38.32-8.68,71.93,71.93,0,0,1-27.6-23.51,59.47,59.47,0,0,1-10.71-33.08q-.43-25.08,12.34-38.78t42.89-19.6q30.12-5.91,84.41-5.9H2397V743.24q0-32.38-13-47.22t-40.83-14.85q-21.9,0-39,5.69t-27.15,13q-1.83,1.38-7.53,5.25c-3.81,2.59-6.93,3.87-9.36,3.87a7.82,7.82,0,0,1-5.7-2.51,8.37,8.37,0,0,1-2.51-6.16q0-8.65,16-18.24a134.23,134.23,0,0,1,34.91-14.59,146.13,146.13,0,0,1,37.64-5Q2416.67,662.5,2416.64,744.16ZM2361.85,863a69.15,69.15,0,0,0,25.78-27.13q9.36-17.34,9.37-39.24V780.2h-8.67q-47.91,0-73,3.85t-35.14,13.21q-10,9.36-10,27.6,0,21,15.49,34.46t39.24,13.47Q2345.41,872.79,2361.85,863Z" transform="translate(-102 -476.45)" /> + <path d="M2526.54,886.78a122.09,122.09,0,0,1-31.93-14.38q-9.14-5.92-12.32-9.82a13.55,13.55,0,0,1-3.19-8.89,8.24,8.24,0,0,1,2.29-5.71,7.17,7.17,0,0,1,5.47-2.5q2.28,0,6.16,2.51c2.58,1.67,4.33,2.82,5.25,3.42a142.87,142.87,0,0,0,27.59,15.75q14.37,6.17,36.27,6.18,29.65,0,45.63-10.94t16-31q0-11.85-5.7-19.62t-18.7-13.46q-13-5.7-37.17-11.19Q2522,778,2504.69,763.82t-17.33-37.42q0-28.29,22.15-46.08t57.26-17.77a114.74,114.74,0,0,1,32.4,4.58,95.21,95.21,0,0,1,27.37,12.78q8.65,6.4,12.54,11.19a15.49,15.49,0,0,1,3.87,9.81,8.24,8.24,0,0,1-2.28,5.7,7.18,7.18,0,0,1-5.48,2.51c-2.44,0-6.38-2.29-11.86-6.85a127.56,127.56,0,0,0-24.86-15.07q-13-5.93-32.62-5.94-25.54,0-41.29,12.07T2508.81,725q0,11.87,5,19.39t17.1,13q12.09,5.47,34.45,10.5,31.47,7.78,48.35,15.76t24,19.18q7.07,11.18,7.06,28.51,0,27.38-22.6,44t-60,16.63A125.17,125.17,0,0,1,2526.54,886.78Z" transform="translate(-102 -476.45)" /> + <path d="M2743.71,886.86a121.76,121.76,0,0,1-31.93-14.38q-9.14-5.93-12.32-9.81a13.6,13.6,0,0,1-3.19-8.9,8.25,8.25,0,0,1,2.29-5.7,7.14,7.14,0,0,1,5.47-2.51q2.28,0,6.16,2.51l5.25,3.42a143.22,143.22,0,0,0,27.6,15.76q14.36,6.16,36.26,6.17,29.65,0,45.63-10.93t16-31q0-11.87-5.69-19.62t-18.7-13.47q-13-5.7-37.18-11.19-40.15-9.13-57.48-23.29t-17.32-37.42q0-28.29,22.14-46.07T2784,662.64a114.31,114.31,0,0,1,32.39,4.57A94.83,94.83,0,0,1,2843.71,680q8.65,6.39,12.54,11.18a15.51,15.51,0,0,1,3.87,9.81,8.24,8.24,0,0,1-2.28,5.7,7.12,7.12,0,0,1-5.47,2.51q-3.66,0-11.86-6.85a128.65,128.65,0,0,0-24.86-15.07q-13-5.92-32.62-5.94-25.56,0-41.3,12.08T2726,725.12q0,11.86,5,19.39t17.1,13q12.09,5.47,34.45,10.51,31.47,7.77,48.35,15.76t24,19.17q7.07,11.19,7.06,28.51,0,27.38-22.6,44t-60,16.62A125.19,125.19,0,0,1,2743.71,886.86Z" transform="translate(-102 -476.45)" /> + </g> + <g> + <path d="M629,748.94A254.5,254.5,0,0,1,177.8,910.5a253.51,253.51,0,0,1-57-141.26A254.75,254.75,0,0,1,269.49,517c48.18-22.05,103.68-28.13,155.56-17.55a253.76,253.76,0,0,1,204,249.5c0,11.58,18,11.6,18,0-.15-56.09-17.45-111.67-50.07-157.4A275.77,275.77,0,0,0,468,492.94c-53.4-19.67-113.16-21.73-167.89-6.22A272.65,272.65,0,0,0,141.23,889.87c28.26,46.62,70.66,84.65,120.38,107.11a275.73,275.73,0,0,0,167.55,18.95,272.71,272.71,0,0,0,142.15-78.52c38.14-39.87,64-91.3,72.33-145.91A287.4,287.4,0,0,0,647,748.94C647,737.36,629,737.34,629,748.94Z" transform="translate(-102 -476.45)" /> + <path d="M276.33,926.44a203,203,0,0,0,98.67,26c7.72,0,7.73-12,0-12a190.29,190.29,0,0,1-92.62-24.36c-6.74-3.79-12.79,6.58-6.05,10.36Z" transform="translate(-102 -476.45)" /> + <path d="M233.58,611c-28.56,28.78-48.31,65.84-55.4,105.83a199.79,199.79,0,0,0,55.4,177c5.45,5.49,13.93-3,8.48-8.48C186.68,829.56,171,742.65,205.27,671.45a191.94,191.94,0,0,1,36.79-52c5.45-5.48-3-14-8.48-8.48Z" transform="translate(-102 -476.45)" /> + </g> + </g> +</svg> diff --git a/docs/static/img/android-chrome-144x144.png b/docs/public/img/android-chrome-144x144.png similarity index 100% rename from docs/static/img/android-chrome-144x144.png rename to docs/public/img/android-chrome-144x144.png diff --git a/docs/static/img/android-chrome-192x192.png b/docs/public/img/android-chrome-192x192.png similarity index 100% rename from docs/static/img/android-chrome-192x192.png rename to docs/public/img/android-chrome-192x192.png diff --git a/docs/static/img/android-chrome-256x256.png b/docs/public/img/android-chrome-256x256.png similarity index 100% rename from docs/static/img/android-chrome-256x256.png rename to docs/public/img/android-chrome-256x256.png diff --git a/docs/static/img/android-chrome-36x36.png b/docs/public/img/android-chrome-36x36.png similarity index 100% rename from docs/static/img/android-chrome-36x36.png rename to docs/public/img/android-chrome-36x36.png diff --git a/docs/static/img/android-chrome-384x384.png b/docs/public/img/android-chrome-384x384.png similarity index 100% rename from docs/static/img/android-chrome-384x384.png rename to docs/public/img/android-chrome-384x384.png diff --git a/docs/static/img/android-chrome-48x48.png b/docs/public/img/android-chrome-48x48.png similarity index 100% rename from docs/static/img/android-chrome-48x48.png rename to docs/public/img/android-chrome-48x48.png diff --git a/docs/static/img/android-chrome-512x512.png b/docs/public/img/android-chrome-512x512.png similarity index 100% rename from docs/static/img/android-chrome-512x512.png rename to docs/public/img/android-chrome-512x512.png diff --git a/docs/static/img/android-chrome-72x72.png b/docs/public/img/android-chrome-72x72.png similarity index 100% rename from docs/static/img/android-chrome-72x72.png rename to docs/public/img/android-chrome-72x72.png diff --git a/docs/static/img/android-chrome-96x96.png b/docs/public/img/android-chrome-96x96.png similarity index 100% rename from docs/static/img/android-chrome-96x96.png rename to docs/public/img/android-chrome-96x96.png diff --git a/docs/static/img/apple-touch-icon-1024x1024.png b/docs/public/img/apple-touch-icon-1024x1024.png similarity index 100% rename from docs/static/img/apple-touch-icon-1024x1024.png rename to docs/public/img/apple-touch-icon-1024x1024.png diff --git a/docs/static/img/apple-touch-icon-114x114.png b/docs/public/img/apple-touch-icon-114x114.png similarity index 100% rename from docs/static/img/apple-touch-icon-114x114.png rename to docs/public/img/apple-touch-icon-114x114.png diff --git a/docs/static/img/apple-touch-icon-120x120.png b/docs/public/img/apple-touch-icon-120x120.png similarity index 100% rename from docs/static/img/apple-touch-icon-120x120.png rename to docs/public/img/apple-touch-icon-120x120.png diff --git a/docs/static/img/apple-touch-icon-144x144.png b/docs/public/img/apple-touch-icon-144x144.png similarity index 100% rename from docs/static/img/apple-touch-icon-144x144.png rename to docs/public/img/apple-touch-icon-144x144.png diff --git a/docs/static/img/apple-touch-icon-152x152.png b/docs/public/img/apple-touch-icon-152x152.png similarity index 100% rename from docs/static/img/apple-touch-icon-152x152.png rename to docs/public/img/apple-touch-icon-152x152.png diff --git a/docs/static/img/apple-touch-icon-167x167.png b/docs/public/img/apple-touch-icon-167x167.png similarity index 100% rename from docs/static/img/apple-touch-icon-167x167.png rename to docs/public/img/apple-touch-icon-167x167.png diff --git a/docs/static/img/apple-touch-icon-180x180.png b/docs/public/img/apple-touch-icon-180x180.png similarity index 100% rename from docs/static/img/apple-touch-icon-180x180.png rename to docs/public/img/apple-touch-icon-180x180.png diff --git a/docs/static/img/apple-touch-icon-57x57.png b/docs/public/img/apple-touch-icon-57x57.png similarity index 100% rename from docs/static/img/apple-touch-icon-57x57.png rename to docs/public/img/apple-touch-icon-57x57.png diff --git a/docs/static/img/apple-touch-icon-60x60.png b/docs/public/img/apple-touch-icon-60x60.png similarity index 100% rename from docs/static/img/apple-touch-icon-60x60.png rename to docs/public/img/apple-touch-icon-60x60.png diff --git a/docs/static/img/apple-touch-icon-72x72.png b/docs/public/img/apple-touch-icon-72x72.png similarity index 100% rename from docs/static/img/apple-touch-icon-72x72.png rename to docs/public/img/apple-touch-icon-72x72.png diff --git a/docs/static/img/apple-touch-icon-76x76.png b/docs/public/img/apple-touch-icon-76x76.png similarity index 100% rename from docs/static/img/apple-touch-icon-76x76.png rename to docs/public/img/apple-touch-icon-76x76.png diff --git a/docs/static/img/apple-touch-icon-precomposed.png b/docs/public/img/apple-touch-icon-precomposed.png similarity index 100% rename from docs/static/img/apple-touch-icon-precomposed.png rename to docs/public/img/apple-touch-icon-precomposed.png diff --git a/docs/static/img/apple-touch-icon.png b/docs/public/img/apple-touch-icon.png similarity index 100% rename from docs/static/img/apple-touch-icon.png rename to docs/public/img/apple-touch-icon.png diff --git a/docs/static/img/apple-touch-startup-image-1125x2436.png b/docs/public/img/apple-touch-startup-image-1125x2436.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1125x2436.png rename to docs/public/img/apple-touch-startup-image-1125x2436.png diff --git a/docs/static/img/apple-touch-startup-image-1136x640.png b/docs/public/img/apple-touch-startup-image-1136x640.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1136x640.png rename to docs/public/img/apple-touch-startup-image-1136x640.png diff --git a/docs/static/img/apple-touch-startup-image-1242x2208.png b/docs/public/img/apple-touch-startup-image-1242x2208.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1242x2208.png rename to docs/public/img/apple-touch-startup-image-1242x2208.png diff --git a/docs/static/img/apple-touch-startup-image-1242x2688.png b/docs/public/img/apple-touch-startup-image-1242x2688.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1242x2688.png rename to docs/public/img/apple-touch-startup-image-1242x2688.png diff --git a/docs/static/img/apple-touch-startup-image-1334x750.png b/docs/public/img/apple-touch-startup-image-1334x750.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1334x750.png rename to docs/public/img/apple-touch-startup-image-1334x750.png diff --git a/docs/static/img/apple-touch-startup-image-1536x2048.png b/docs/public/img/apple-touch-startup-image-1536x2048.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1536x2048.png rename to docs/public/img/apple-touch-startup-image-1536x2048.png diff --git a/docs/static/img/apple-touch-startup-image-1620x2160.png b/docs/public/img/apple-touch-startup-image-1620x2160.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1620x2160.png rename to docs/public/img/apple-touch-startup-image-1620x2160.png diff --git a/docs/static/img/apple-touch-startup-image-1668x2224.png b/docs/public/img/apple-touch-startup-image-1668x2224.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1668x2224.png rename to docs/public/img/apple-touch-startup-image-1668x2224.png diff --git a/docs/static/img/apple-touch-startup-image-1668x2388.png b/docs/public/img/apple-touch-startup-image-1668x2388.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1668x2388.png rename to docs/public/img/apple-touch-startup-image-1668x2388.png diff --git a/docs/static/img/apple-touch-startup-image-1792x828.png b/docs/public/img/apple-touch-startup-image-1792x828.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-1792x828.png rename to docs/public/img/apple-touch-startup-image-1792x828.png diff --git a/docs/static/img/apple-touch-startup-image-2048x1536.png b/docs/public/img/apple-touch-startup-image-2048x1536.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-2048x1536.png rename to docs/public/img/apple-touch-startup-image-2048x1536.png diff --git a/docs/static/img/apple-touch-startup-image-2048x2732.png b/docs/public/img/apple-touch-startup-image-2048x2732.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-2048x2732.png rename to docs/public/img/apple-touch-startup-image-2048x2732.png diff --git a/docs/static/img/apple-touch-startup-image-2160x1620.png b/docs/public/img/apple-touch-startup-image-2160x1620.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-2160x1620.png rename to docs/public/img/apple-touch-startup-image-2160x1620.png diff --git a/docs/static/img/apple-touch-startup-image-2208x1242.png b/docs/public/img/apple-touch-startup-image-2208x1242.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-2208x1242.png rename to docs/public/img/apple-touch-startup-image-2208x1242.png diff --git a/docs/static/img/apple-touch-startup-image-2224x1668.png b/docs/public/img/apple-touch-startup-image-2224x1668.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-2224x1668.png rename to docs/public/img/apple-touch-startup-image-2224x1668.png diff --git a/docs/static/img/apple-touch-startup-image-2388x1668.png b/docs/public/img/apple-touch-startup-image-2388x1668.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-2388x1668.png rename to docs/public/img/apple-touch-startup-image-2388x1668.png diff --git a/docs/static/img/apple-touch-startup-image-2436x1125.png b/docs/public/img/apple-touch-startup-image-2436x1125.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-2436x1125.png rename to docs/public/img/apple-touch-startup-image-2436x1125.png diff --git a/docs/static/img/apple-touch-startup-image-2688x1242.png b/docs/public/img/apple-touch-startup-image-2688x1242.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-2688x1242.png rename to docs/public/img/apple-touch-startup-image-2688x1242.png diff --git a/docs/static/img/apple-touch-startup-image-2732x2048.png b/docs/public/img/apple-touch-startup-image-2732x2048.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-2732x2048.png rename to docs/public/img/apple-touch-startup-image-2732x2048.png diff --git a/docs/static/img/apple-touch-startup-image-640x1136.png b/docs/public/img/apple-touch-startup-image-640x1136.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-640x1136.png rename to docs/public/img/apple-touch-startup-image-640x1136.png diff --git a/docs/static/img/apple-touch-startup-image-750x1334.png b/docs/public/img/apple-touch-startup-image-750x1334.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-750x1334.png rename to docs/public/img/apple-touch-startup-image-750x1334.png diff --git a/docs/static/img/apple-touch-startup-image-828x1792.png b/docs/public/img/apple-touch-startup-image-828x1792.png similarity index 100% rename from docs/static/img/apple-touch-startup-image-828x1792.png rename to docs/public/img/apple-touch-startup-image-828x1792.png diff --git a/docs/static/img/browserconfig.xml b/docs/public/img/browserconfig.xml similarity index 100% rename from docs/static/img/browserconfig.xml rename to docs/public/img/browserconfig.xml diff --git a/docs/static/img/coast-228x228.png b/docs/public/img/coast-228x228.png similarity index 100% rename from docs/static/img/coast-228x228.png rename to docs/public/img/coast-228x228.png diff --git a/docs/static/img/favicon-16x16.png b/docs/public/img/favicon-16x16.png similarity index 100% rename from docs/static/img/favicon-16x16.png rename to docs/public/img/favicon-16x16.png diff --git a/docs/static/img/favicon-32x32.png b/docs/public/img/favicon-32x32.png similarity index 100% rename from docs/static/img/favicon-32x32.png rename to docs/public/img/favicon-32x32.png diff --git a/docs/static/img/favicon-48x48.png b/docs/public/img/favicon-48x48.png similarity index 100% rename from docs/static/img/favicon-48x48.png rename to docs/public/img/favicon-48x48.png diff --git a/docs/static/img/favicon.ico b/docs/public/img/favicon.ico similarity index 100% rename from docs/static/img/favicon.ico rename to docs/public/img/favicon.ico diff --git a/docs/static/img/firefox_app_128x128.png b/docs/public/img/firefox_app_128x128.png similarity index 100% rename from docs/static/img/firefox_app_128x128.png rename to docs/public/img/firefox_app_128x128.png diff --git a/docs/static/img/firefox_app_512x512.png b/docs/public/img/firefox_app_512x512.png similarity index 100% rename from docs/static/img/firefox_app_512x512.png rename to docs/public/img/firefox_app_512x512.png diff --git a/docs/static/img/firefox_app_60x60.png b/docs/public/img/firefox_app_60x60.png similarity index 100% rename from docs/static/img/firefox_app_60x60.png rename to docs/public/img/firefox_app_60x60.png diff --git a/docs/static/img/icon.svg b/docs/public/img/icon.svg similarity index 100% rename from docs/static/img/icon.svg rename to docs/public/img/icon.svg diff --git a/docs/static/img/manifest.json b/docs/public/img/manifest.json similarity index 100% rename from docs/static/img/manifest.json rename to docs/public/img/manifest.json diff --git a/docs/static/img/manifest.webapp b/docs/public/img/manifest.webapp similarity index 100% rename from docs/static/img/manifest.webapp rename to docs/public/img/manifest.webapp diff --git a/docs/static/img/mstile-144x144.png b/docs/public/img/mstile-144x144.png similarity index 100% rename from docs/static/img/mstile-144x144.png rename to docs/public/img/mstile-144x144.png diff --git a/docs/static/img/mstile-150x150.png b/docs/public/img/mstile-150x150.png similarity index 100% rename from docs/static/img/mstile-150x150.png rename to docs/public/img/mstile-150x150.png diff --git a/docs/static/img/mstile-310x150.png b/docs/public/img/mstile-310x150.png similarity index 100% rename from docs/static/img/mstile-310x150.png rename to docs/public/img/mstile-310x150.png diff --git a/docs/static/img/mstile-310x310.png b/docs/public/img/mstile-310x310.png similarity index 100% rename from docs/static/img/mstile-310x310.png rename to docs/public/img/mstile-310x310.png diff --git a/docs/static/img/mstile-70x70.png b/docs/public/img/mstile-70x70.png similarity index 100% rename from docs/static/img/mstile-70x70.png rename to docs/public/img/mstile-70x70.png diff --git a/docs/static/img/undraw_docusaurus_mountain.svg b/docs/public/img/undraw_docusaurus_mountain.svg similarity index 100% rename from docs/static/img/undraw_docusaurus_mountain.svg rename to docs/public/img/undraw_docusaurus_mountain.svg diff --git a/docs/static/img/undraw_docusaurus_react.svg b/docs/public/img/undraw_docusaurus_react.svg similarity index 100% rename from docs/static/img/undraw_docusaurus_react.svg rename to docs/public/img/undraw_docusaurus_react.svg diff --git a/docs/static/img/undraw_docusaurus_tree.svg b/docs/public/img/undraw_docusaurus_tree.svg similarity index 100% rename from docs/static/img/undraw_docusaurus_tree.svg rename to docs/public/img/undraw_docusaurus_tree.svg diff --git a/docs/static/img/yandex-browser-50x50.png b/docs/public/img/yandex-browser-50x50.png similarity index 100% rename from docs/static/img/yandex-browser-50x50.png rename to docs/public/img/yandex-browser-50x50.png diff --git a/docs/static/img/yandex-browser-manifest.json b/docs/public/img/yandex-browser-manifest.json similarity index 100% rename from docs/static/img/yandex-browser-manifest.json rename to docs/public/img/yandex-browser-manifest.json diff --git a/docs/static/opengraph.jpg b/docs/public/opengraph.jpg similarity index 100% rename from docs/static/opengraph.jpg rename to docs/public/opengraph.jpg diff --git a/docs/static/robots.txt b/docs/public/robots.txt similarity index 100% rename from docs/static/robots.txt rename to docs/public/robots.txt diff --git a/docs/static/screenshots/screenshot-favicons.jpg b/docs/public/screenshots/screenshot-favicons.jpg similarity index 100% rename from docs/static/screenshots/screenshot-favicons.jpg rename to docs/public/screenshots/screenshot-favicons.jpg diff --git a/docs/static/traceroute_nanog.pdf b/docs/public/traceroute_nanog.pdf similarity index 100% rename from docs/static/traceroute_nanog.pdf rename to docs/public/traceroute_nanog.pdf diff --git a/docs/screenshots/index.mdx b/docs/screenshots/index.mdx deleted file mode 100644 index 74f4cac46..000000000 --- a/docs/screenshots/index.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -id: screenshots -title: Screenshots -sidebar_label: Screenshots ---- diff --git a/docs/sidebars.js b/docs/sidebars.js deleted file mode 100755 index f9ee0e791..000000000 --- a/docs/sidebars.js +++ /dev/null @@ -1,44 +0,0 @@ -module.exports = { - someSidebar: [ - { - type: "category", - label: "Installation", - items: ["introduction", "getting-started", "setup"], - }, - { - type: "category", - label: "Configuration", - items: [ - "parameters", - "adding-devices", - "commands", - "logging", - "messages", - "query-settings", - "response-caching", - "rest-api", - "table-output", - { - type: "category", - label: "Web UI", - items: [ - "ui/configuration", - "ui/logo", - "ui/text", - "ui/theme", - "ui/example", - ], - }, - ], - }, - { - type: "category", - label: "Linux Agent", - items: ["agent/installation", "agent/setup", "agent/parameters"], - }, - { type: "doc", id: "production" }, - { type: "doc", id: "upgrading" }, - { type: "doc", id: "platforms" }, - { type: "doc", id: "license" }, - ], -}; diff --git a/docs/src/components/Color.tsx b/docs/src/components/Color.tsx deleted file mode 100644 index 7c2864e94..000000000 --- a/docs/src/components/Color.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from "react"; -import styles from "./styles.module.css"; - -type ColorProps = { - hex: string; -}; - -const Color = (props: React.PropsWithChildren<ColorProps>): JSX.Element => { - const { hex } = props; - return ( - <div className={styles.colorCode}> - <span style={{ backgroundColor: hex }} className={styles.color} /> - <span className={styles.code}>{hex}</span> - </div> - ); -}; - -export default Color; diff --git a/docs/src/components/Datetime.tsx b/docs/src/components/Datetime.tsx deleted file mode 100644 index 4c811d8ce..000000000 --- a/docs/src/components/Datetime.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from "react"; - -type DatetimeProps = { - year?: boolean; -}; - -const Datetime = ( - props: React.PropsWithChildren<DatetimeProps> -): JSX.Element => { - const { year } = props; - const date = new Date(); - const granularity = year ? date.getFullYear() : date.toString(); - return <span {...props}>{granularity}</span>; -}; - -export default Datetime; diff --git a/docs/src/components/Font.tsx b/docs/src/components/Font.tsx deleted file mode 100644 index e6bb20a56..000000000 --- a/docs/src/components/Font.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react"; -import clsx from "clsx"; -import styles from "./fonts.module.css"; - -type FontProps = { - name: string; -}; - -const Font = (props: React.PropsWithChildren<FontProps>): JSX.Element => { - const { name } = props; - const fontClass = { Nunito: "fontBody", "Fira Code": "fontMono" }; - return ( - <a href={`https://fonts.google.com/specimen/${name.split(" ").join("+")}`}> - <span className={clsx(styles.font, styles[fontClass[name]])}>{name}</span> - </a> - ); -}; - -export default Font; diff --git a/docs/src/components/JSXCode.tsx b/docs/src/components/JSXCode.tsx deleted file mode 100644 index 48de6ed8d..000000000 --- a/docs/src/components/JSXCode.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import * as React from "react"; -import styles from "./styles.module.css"; - -const JSXCode = (props: React.ComponentProps<"span">): JSX.Element => { - return <span className={styles.code} {...props} />; -}; - -export default JSXCode; diff --git a/docs/src/components/MiniNote.tsx b/docs/src/components/MiniNote.tsx deleted file mode 100644 index 4086adce1..000000000 --- a/docs/src/components/MiniNote.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from "react"; - -type MiniNodeProps = { - newLine: boolean; -}; - -const MiniNote = ( - props: React.PropsWithChildren<MiniNodeProps> -): JSX.Element => { - const { newLine, children } = props; - return ( - <> - {newLine && <br />} - <span - style={{ - fontSize: "var(--ifm-font-size-sm)", - color: "var(--ifm-blockquote-color)", - display: "inline-block", - fontStyle: "italic", - }} - > - {children} - </span> - </> - ); -}; - -export default MiniNote; diff --git a/docs/src/components/Native.tsx b/docs/src/components/Native.tsx deleted file mode 100644 index 28e3ad2af..000000000 --- a/docs/src/components/Native.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from "react"; -import styles from "./styles.module.css"; - -const Native = (props: React.ComponentProps<"span">): JSX.Element => { - const { children } = props; - return <span className={styles.Native}>{children}</span>; -}; - -export default Native; diff --git a/docs/src/components/PageLink.tsx b/docs/src/components/PageLink.tsx deleted file mode 100644 index 81f509160..000000000 --- a/docs/src/components/PageLink.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from "react"; -import Link from "@docusaurus/Link"; - -import type { LinkProps } from "@docusaurus/Link"; - -const PageLink = (props: React.PropsWithChildren<LinkProps>): JSX.Element => { - return <Link style={{ textDecoration: "unset" }} {...props} />; -}; - -export default PageLink; diff --git a/docs/src/components/RegexPattern.tsx b/docs/src/components/RegexPattern.tsx deleted file mode 100644 index c2b4ba397..000000000 --- a/docs/src/components/RegexPattern.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from "react"; -import { useMemo } from "react"; -import Code from "./JSXCode"; - -const PATTERN_MAP = { - aspath_asdot: String.raw`^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$`, - aspath_asplain: String.raw`^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$`, - community_decimal: String.raw`^[0-9]{1,10}$`, - community_extended: String.raw`^([0-9]{0,5})\:([0-9]{1,5})$`, - community_large: String.raw`^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$`, -}; - -type RegexPatternProps = { - pattern: keyof typeof PATTERN_MAP; -}; - -const RegexPattern = ( - props: React.PropsWithChildren<RegexPatternProps> -): JSX.Element => { - const { pattern } = props; - const thisPattern = useMemo<string>(() => PATTERN_MAP[pattern], [pattern]); - return <Code>'{thisPattern}'</Code>; -}; - -export default RegexPattern; diff --git a/docs/src/components/Required.tsx b/docs/src/components/Required.tsx deleted file mode 100644 index 14a36b538..000000000 --- a/docs/src/components/Required.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from "react"; - -const Required = (): JSX.Element => { - return ( - <span style={{ color: "var(--ifm-color-danger)", display: "inline-block" }}> - * - </span> - ); -}; - -export default Required; diff --git a/docs/src/components/fonts.module.css b/docs/src/components/fonts.module.css deleted file mode 100644 index f5b090fb0..000000000 --- a/docs/src/components/fonts.module.css +++ /dev/null @@ -1,24 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Nunito&display=swap"); -@import url("https://fonts.googleapis.com/css?family=Fira+Code|Nunito&display=swap"); - -.font { - background-color: var(--ifm-color-emphasis-200); - padding-top: 0.2rem; - padding-bottom: 0.2rem; - padding-right: 0.5rem; - padding-left: 0.5rem; - color: var(--ifm-color-emphasis-900); - border-radius: var(--ifm-code-border-radius); - width: 100%; - height: 100%; - text-decoration: none; - display: inline-block; - text-align: center; -} - -.fontBody { - font-family: Nunito, sans-serif; -} -.fontMono { - font-family: "Fira Code", monospace; -} diff --git a/docs/src/components/styles.module.css b/docs/src/components/styles.module.css deleted file mode 100644 index 1ad1ee478..000000000 --- a/docs/src/components/styles.module.css +++ /dev/null @@ -1,31 +0,0 @@ -.code { - background-color: var(--ifm-code-background); - color: var(--ifm-code-color); - font-family: var(--ifm-font-family-monospace); - font-size: var(--ifm-code-font-size); - border-radius: var(--ifm-code-border-radius); - margin: 0; - padding: var(--ifm-code-padding-vertical) var(--ifm-code-padding-horizontal); - font-style: normal; -} - -.color { - border-radius: var(--ifm-global-radius); - display: inline-flex; - height: 1.5rem; - width: 1.5rem; - margin-right: 0.5rem; - padding: var(--ifm-code-padding-vertical) var(--ifm-code-padding-horizontal); -} - -.colorCode { - display: flex; - align-items: center; - justify-content: space-between; -} - -.Native { - /* color: var(--ifm-link-color); */ - color: var(--ifm-color-success); - font-weight: bold; -} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css deleted file mode 100755 index d93719612..000000000 --- a/docs/src/css/custom.css +++ /dev/null @@ -1,291 +0,0 @@ -/** - * Any CSS included here will be global. The classic template - * bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. - */ - -/* You can override the default Infima variables here. */ -@import url("https://fonts.googleapis.com/css?family=Nunito:200,400,700&display=swap"); -@import url("https://fonts.googleapis.com/css?family=Fira+Code&display=swap"); - -:root { - --ifm-font-size-sm: 12px; - --ifm-color-secondary: #69a2b0; - --ifm-color-secondary-dark: #5796a6; - --ifm-color-secondary-darker: #528e9c; - --ifm-color-secondary-darkest: #447581; - --ifm-color-secondary-light: #7badba; - --ifm-color-secondary-lighter: #85b3bf; - --ifm-color-secondary-lightest: #a0c4cd; - --ifm-color-primary: #ff5e5b; - --ifm-color-primary-dark: #ff3c38; - --ifm-color-primary-darker: #ff2b27; - --ifm-color-primary-darkest: #f20400; - --ifm-color-primary-light: #ff807e; - --ifm-color-primary-lighter: #ff918f; - --ifm-color-primary-lightest: #ffc4c3; - --ifm-color-success: #35b246; - --ifm-color-success-dark: #30a03f; - --ifm-color-success-darker: #2d973b; - --ifm-color-success-darkest: #257d31; - --ifm-color-success-light: #3ac44d; - --ifm-color-success-lighter: #43c755; - --ifm-color-success-lightest: #5dcf6d; - --ifm-code-font-size: 95%; - --ifm-font-family-base: "Nunito", system-ui, -apple-system, Segoe UI, Roboto, - Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, "Segoe UI", - Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", - "Segoe UI Symbol"; - --ifm-font-family-monospace: "Fira Code", SFMono-Regular, Menlo, Monaco, - Consolas, "Liberation Mono", "Courier New", monospace; - --ifm-code-padding-vertical: 0.05rem; - --hgd-footer-link-color: var(--ifm-color-emphasis-700); - --hgd-link-hover-color: var(--ifm-color-secondary-darker); - --ifm-link-color: var(--ifm-color-secondary); -} - -:root { - --ifm-code-background: rgba(0, 0, 0, 0.04); - --ifm-code-color: var(--ifm-color-secondary-darkest); - --ifm-footer-color: var(--ifm-secondary-lightest); - --ifm-footer-link-color: var(--ifm-secondary-lightest); - --ifm-footer-title-color: var(--ifm-color-emphasis-800); - --ifm-footer-background-color: var(--ifm-color-emphasis-100); -} - -:root { - --ifm-menu-color-active: black; - --ifm-alert-background-color: var(--ifm-color-gray-400); -} - -html[data-theme="dark"]:root { - --ifm-background-color: #121212; - --ifm-background-surface-color: #1e2125; - --ifm-navbar-background-color: var(--ifm-background-color); - --ifm-menu-color-active: var(--ifm-color-primary); - --ifm-blockquote-color: var(--ifm-color-emphasis-400); - --ifm-link-color: var(--ifm-color-primary); - --hgd-footer-link-color: var(--ifm-color-emphasis-400); - --hgd-link-hover-color: var(--ifm-color-primary-darker); - --ifm-code-background: rgba(255, 255, 255, 0.08); - --ifm-code-color: var(--ifm-color-secondary-lightest); - --ifm-footer-link-color: ; -} - -.admonition.alert { - --ra-admonition-icon-color: currentColor; - --ifm-alert-color: var(--ifm-font-color-base); - border-color: unset; - border-style: unset; - border-width: unset; - box-shadow: var(--ifm-global-shadow-md); -} - -html[data-theme="dark"] .admonition.alert--warning { - --ifm-alert-color: var(--ifm-color-black); -} -html[data-theme="dark"] .admonition.alert--critical { - --ifm-alert-color: var(--ifm-color-black); -} -html[data-theme="dark"] .admonition.alert--info { - --ifm-alert-color: var(--ifm-color-black); -} -html[data-theme=""] .admonition.alert--success { - --ifm-alert-color: white; -} - -html[data-theme="dark"]:root .admonition.alert.admonition-note { - --ifm-alert-background-color: var(--ifm-color-gray-600); -} - -.admonition.alert.admonition-note { - --ifm-alert-background-color: var(--ifm-color-gray-300); - --ifm-alert-color: var(--ifm-color-black); -} - -.admonition.alert.admonition-note[class] .admonition-content[class] a { - color: var(--ifm-font-color-base); - opacity: 0.9; -} - -.footer.footer--dark { - --ifm-footer-color: var(--ifm-color-emphasis-600); - --ifm-footer-link-color: var(--ifm-footer-color); - --ifm-footer-title-color: var(--ifm-color-emphasis-800); - --ifm-footer-background-color: var(--ifm-color-emphasis-100); -} - -.footer.footer--dark .footer__items { - font-size: 0.8rem; -} - -.footer .footer__items .footer__item .footer__link-item { - color: var(--hgd-footer-link-color); -} - -.admonition { - --ifm-code-background: rgba(255, 255, 255, 0.5); - --ifm-code-color: var(--ifm-color-secondary-darkest); - --ifm-link-color: rgba(0, 0, 0, 0.5); -} - -.admonition.alert--success .admonition-content a, -.admonition.alert--warning .admonition-content a, -.admonition.alert--critical .admonition-content a, -.admonition.alert--info .admonition-content a { - color: var(--ra-color-link); -} - -.admonition.admonition-note .admonition-content code { - --ifm-code-color: var(--ifm-color-gray-900); -} - -.admonition .admonition-content a:hover { - color: rgba(0, 0, 0, 0.7); -} - -.docusaurus-highlight-code-line { - background-color: rgba(0, 0, 0, 0.1); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); -} - -html[data-theme="dark"] .docusaurus-highlight-code-line { - background-color: rgba(255, 255, 255, 0.1); -} - -table { - margin-top: var(--ifm-spacing-vertical); -} - -table td:nth-child(1) { - white-space: nowrap; -} - -h1 code, -h2 code, -h3 code, -h4 code, -h5 code, -h6 code { - color: var(--ifm-color-primary); -} - -.contents a.contents__link--active[class] { - color: var(--ifm-color-secondary); - font-weight: bold; -} - -a:hover { - color: var(--hgd-link-hover-color); -} - -html[data-theme="dark"] .contents a.contents__link--active[class] { - color: var(--ifm-color-primary); - font-weight: bold; -} - -html[data-theme="dark"] .navbar .navbar__items a.navbar__brand { - color: var(--ifm-color-primary); -} - -nav.navbar.navbar--fixed-top { - border-bottom: 1px solid var(--ifm-toc-border-color); -} - -.navbar .navbar__items a.navbar__brand { - color: var(--ifm-color-black); -} - -@media screen and (max-width: 997px) { - .col.footer__col[class][class] { - text-align: left; - } - .navbar .navbar__search { - padding-left: unset; - } - .navbar .navbar__search .navbar__search-input { - width: 100%; - display: block; - } - .navbar .navbar__inner .navbar__items a.navbar__brand img.logo-at-home { - display: none; - } -} - -.navbar .navbar__search .navbar__search-input { - border-radius: var(--ifm-button-border-radius); - border-width: var(--ifm-button-border-width); - border-color: var(--ifm-navbar-search-input-background-color); - border-style: solid; -} - -.admonition-content table tr:nth-child(2n) { - background-color: unset; -} - -@media screen and (min-width: 998px) { - div.table--full-width ~ table { - display: table; - width: 100%; - margin-top: 2rem; - margin-bottom: 2rem; - } -} - -.menu .menu__list .menu__list-item .menu__link.menu__link--active { - font-weight: var(--ifm-font-weight-bold); - color: var(--ifm-color-secondary); -} - -html[data-theme="dark"] - .menu - .menu__list - .menu__list-item - .menu__link.menu__link--active { - color: var(--ifm-color-primary); -} - -.menu .menu__list .menu__list-item .menu__link, -.table-of-contents .table-of-contents__link { - color: var(--ifm-font-color-base); -} - -.table-of-contents .table-of-contents__link:hover { - color: var(--hgd-link-hover-color); -} - -.footer .footer__col:first-child { - text-align: left; -} - -.footer .footer__col:not(:first-child):not(:last-child) { - text-align: center; -} - -.footer .footer__col:last-child { - text-align: right; -} - -.header-github-link { - transition: opacity 0.2s ease-in-out; -} - -.header-github-link:hover { - opacity: 0.75; -} - -.header-github-link:before { - content: ""; - width: 24px; - height: 24px; - display: flex; - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") - no-repeat; -} - -html[data-theme="dark"] .header-github-link:before { - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") - no-repeat; -} diff --git a/docs/src/hooks/index.ts b/docs/src/hooks/index.ts deleted file mode 100644 index f07999ed4..000000000 --- a/docs/src/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./useLogoSrc"; diff --git a/docs/src/hooks/useLogoSrc.ts b/docs/src/hooks/useLogoSrc.ts deleted file mode 100644 index d42c8c0d1..000000000 --- a/docs/src/hooks/useLogoSrc.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useMemo } from "react"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { useLocation } from "@docusaurus/router"; -import useMedia from "use-media"; - -type UseLogoSrc = { - sources: { light: string; dark: string }; - className: string | null; -}; - -export function useLogoSrc(): UseLogoSrc { - let className = null; - const { pathname } = useLocation(); - const isSmall = useMedia({ maxWidth: "997px" }); - if (pathname === "/") { - className = "logo-at-home"; - } - const sourcesIcon = { - light: useBaseUrl("hyperglass-icon-light.svg"), - dark: useBaseUrl("hyperglass-icon-dark.svg"), - }; - - const sourcesFull = { - light: useBaseUrl("hyperglass-light.svg"), - dark: useBaseUrl("hyperglass-dark.svg"), - }; - - const sources = useMemo(() => { - if (isSmall) { - return sourcesFull; - } - if (!isSmall && pathname === "/") { - return sourcesIcon; - } - return sourcesFull; - }, [isSmall, pathname]); - return { sources, className }; -} diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js deleted file mode 100644 index 171002142..000000000 --- a/docs/src/pages/index.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import Layout from "@theme/Layout"; -import Link from "@docusaurus/Link"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import styles from "./styles.module.css"; - -function Home() { - return ( - <Layout - description="hyperglass is the network looking glass that tries to make the internet better." - keywords={[ - "hyperglass", - "documentation", - "docs", - "bgp", - "lg", - "looking", - "glass", - "looking glass", - "ping", - "traceroute", - "matt love", - "python", - "python3", - "react", - "reactjs", - ]} - > - <header className={clsx("hero", styles.heroBanner)}> - <div className="container"> - <h1 className={clsx("hero__title", styles.title)}>hyperglass</h1> - <h2 className={clsx("hero__subtitle", styles.subTitle)}> - the <span className={styles.tagPrimary}>network looking glass</span>{" "} - that tries to - <span className={styles.tagSecondary}> - {" "} - make the internet better - </span> - . - </h2> - <div className={styles.buttons}> - <Link - className={clsx( - "button button--outline button--secondary button--lg", - styles.homeBtn, - styles.btnSecondary - )} - to={useBaseUrl("docs/getting-started")} - > - Set up your Looking Glass - </Link> - <Link - className={clsx( - "button button--outline button--primary button--lg", - styles.homeBtn, - styles.btnPrimary - )} - to={useBaseUrl("docs/introduction")} - > - Why hyperglass? - </Link> - </div> - </div> - </header> - <main> - <section className={styles.content}> - <div className="container"> - <div className="row"> - <div className={clsx("col col--4")}> - <section className={styles.content}> - <div className="container"> - <div className="row"> - <div className={clsx("col col--12")}></div> - </div> - </div> - </section> - </div> - <div className={clsx("col col--8")}></div> - </div> - </div> - </section> - </main> - </Layout> - ); -} - -export default Home; diff --git a/docs/src/pages/screenshots.js b/docs/src/pages/screenshots.js deleted file mode 100644 index 272cf302b..000000000 --- a/docs/src/pages/screenshots.js +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from "react"; -import clsx from "clsx"; -import Layout from "@theme/Layout"; -import Link from "@docusaurus/Link"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import styles from "./styles.module.css"; - -function Screenshots() { - return ( - <Layout - description="hyperglass screenshots" - keywords={["hyperglass", "screenshots"]} - > - <header className={clsx("hero", styles.heroBanner)}> - <div className={clsx("container", styles.smallerTitleContainer)}> - <h1 className={clsx("hero__title", styles.title)}>Coming Soon!</h1> - <h2 - className={clsx( - "hero__subtitle", - styles.subTitle, - styles.smallerTitle - )} - > - <span className={styles.tagPrimary}>hyperglass 1.0</span> is still - in progress, so screenshots may not be made available until shortly - before the release. - </h2> - <div className={styles.buttons}> - <Link - className={clsx( - "button button--outline button--secondary button--lg", - styles.homeBtn, - styles.btnSecondary - )} - to={useBaseUrl("docs/getting-started")} - > - Back to the Docs - </Link> - </div> - </div> - </header> - <main> - <section className={styles.content}> - <div className="container"> - <div className="row"> - <div className={clsx("col col--4")}> - <section className={styles.content}> - <div className="container"> - <div className="row"> - <div className={clsx("col col--12")}></div> - </div> - </div> - </section> - </div> - <div className={clsx("col col--8")}></div> - </div> - </div> - </section> - </main> - </Layout> - ); -} - -export default Screenshots; diff --git a/docs/src/pages/styles.module.css b/docs/src/pages/styles.module.css deleted file mode 100644 index 8234edf8a..000000000 --- a/docs/src/pages/styles.module.css +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ -.heroBanner { - /* background-color: var(--neutron-color-dark); */ - background-color: var(--ifm-background-color); - color: var(--neutron-color-lighter); - padding: 128px; - text-align: center; - position: relative; - overflow: hidden; -} - -.title[class] { - font-weight: 200; - font-size: 4rem; -} - -.subTitle[class] { - font-weight: 200; - font-size: 2rem; - padding: 24px; -} - -h2.smallerTitle[class] { - font-size: 1.5rem; -} - -.smallerTitleContainer { - max-width: 50%; -} - -@media screen and (max-width: 966px) { - .smallerTitleContainer { - max-width: unset; - } -} - -.homeBtn { - color: unset !important; - font-size: 24px; - padding: 16px 36px; - margin: 0.25rem; - border-color: unset; -} - -.homeBtn:hover { - color: white !important; -} - -.btnPrimary:hover { - border-color: var(--ifm-color-primary); -} - -.btnSecondary:hover { - border-color: var(--ifm-color-secondary); -} - -.buttons { - margin-top: 4vh; - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; -} - -.content { - align-items: center; - display: flex; - padding: 36px 0; - width: 100%; -} - -@media screen and (max-width: 966px) { - .heroBanner { - padding: 2rem; - min-height: 80vh; - } - - .title { - font-size: 2.5rem !important; - } - - .homeBtn { - min-width: 100%; - padding-left: 1rem; - padding-right: 1rem; - font-size: var(--ifm-h3-font-size); - } -} - -.tagPrimary { - color: var(--ifm-color-primary); -} - -.tagSecondary { - color: var(--ifm-color-secondary); -} diff --git a/docs/src/prism-dracula.js b/docs/src/prism-dracula.js deleted file mode 100644 index 98310cecc..000000000 --- a/docs/src/prism-dracula.js +++ /dev/null @@ -1,100 +0,0 @@ -// @flow - -module.exports = { - plain: { - color: "rgb(241, 250, 140)", - backgroundColor: "#282A36", - }, - styles: [ - { - types: ["boolean", "number", "null"], - style: { - color: "rgb(189, 147, 249)", - }, - }, - { - types: [ - "inserted", - "selector", - "attr-name", - "char", - "builtin", - "inserted", - "function", - ], - style: { - color: "rgb(80, 250, 123)", - }, - }, - { - types: ["deleted"], - style: { - color: "rgb(255, 85, 85)", - }, - }, - { - types: ["regex"], - style: { - color: "rgb(255, 184, 108)", - }, - }, - { - types: ["operator", "entity", "url", "variable"], - style: { - color: "rgb(248, 248, 242)", - }, - }, - { - types: [ - "constant", - "tag", - "selector", - "shell-symbol", - "symbol", - "deleted", - "punctuation", - ], - style: { - color: "rgb(255, 121, 198)", - }, - }, - { - types: ["atrule", "property", "language-bash"], - style: { color: "rgb(139, 233, 253)" }, - }, - { - types: ["keyword"], - style: { - color: "rgb(139, 233, 253)", - fontStyle: "italic", - }, - }, - { types: ["entity"], style: { cursor: "help" } }, - { - types: ["bold"], - style: { - fontStyle: "bold", - }, - }, - { - types: ["comment", "output"], - style: { - color: "rgb(98, 114, 164)", - }, - }, - { - types: ["attr-value", "class-name", "string"], - style: { - color: "rgb(241, 250, 140)", - }, - }, - { - types: ["important"], - style: { - color: "rgb(80, 250, 123)", - fontStyle: "italic", - textDecoration: "underline", - }, - }, - ], -}; diff --git a/docs/src/theme/Logo/index.tsx b/docs/src/theme/Logo/index.tsx deleted file mode 100644 index 25b4755d1..000000000 --- a/docs/src/theme/Logo/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React from "react"; -import type { Props } from "@theme/Logo"; - -import clsx from "clsx"; -import Link from "@docusaurus/Link"; -import ThemedImage from "@theme/ThemedImage"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import { useThemeConfig } from "@docusaurus/theme-common"; -import { useLogoSrc } from "../../hooks"; - -const Logo = (props: Props): JSX.Element => { - const { sources, className } = useLogoSrc(); - - const { isClient } = useDocusaurusContext(); - const { - navbar: { title, logo = { src: "" } }, - } = useThemeConfig(); - - const { imageClassName, titleClassName, ...propsRest } = props; - const logoLink = useBaseUrl(logo.href || "/"); - - return ( - <Link - to={logoLink} - {...propsRest} - {...(logo.target && { target: logo.target })} - > - {sources.light && ( - <ThemedImage - key={isClient} - className={clsx(imageClassName, className)} - sources={sources} - alt={logo.alt || title || "Logo"} - /> - )} - {title != null && <strong className={titleClassName}>{title}</strong>} - </Link> - ); -}; - -export default Logo; diff --git a/docs/theme.config.tsx b/docs/theme.config.tsx new file mode 100644 index 000000000..97d17d9c1 --- /dev/null +++ b/docs/theme.config.tsx @@ -0,0 +1,144 @@ +import { useRouter } from "next/router"; +import { type DocsThemeConfig, useConfig } from "nextra-theme-docs"; +import "nextra-theme-docs/style.css"; +import faviconFormats from "./favicon-formats"; +import styles from "./global.module.css"; + +const NO_INDEX_FOLLOW = process.env.CF_PAGES_BRANCH !== "main"; + +const config: DocsThemeConfig = { + logo: ( + <span className={styles.logo}> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2759.9 544.87" width={200}> + <title>hyperglass + + + + + + + + + + + + + + + + + + + + + + ), + head: () => { + const { asPath, locale, defaultLocale } = useRouter(); + const { frontMatter } = useConfig(); + const url = `https://hyperglass.dev${ + defaultLocale === locale ? asPath : `/${locale}${asPath}` + }`; + let title = frontMatter.title || "hyperglass"; + if (title !== "hyperglass") { + title = `${title} | hyperglass`; + } + const description = frontMatter.description || "hyperglass Documentation"; + const index = NO_INDEX_FOLLOW ? "noindex, nofollow" : "index, follow"; + const favicons = faviconFormats.map((fmt) => { + const { image_format, dimensions, prefix, rel } = fmt; + const [w, h] = dimensions; + const href = `/img/${prefix}-${w}x${h}.${image_format}`; + return { rel: rel ?? "", href, type: `image/${image_format}` }; + }); + + return ( + + {title} + + + + + + + + + + + + + + {favicons.map((props) => ( + + ))} + + ); + }, + docsRepositoryBase: "https://github.com/thatmattlove/hyperglass/tree/main/docs", + banner: { + dismissible: true, + content: "🎉 hyperglass 2.0 is here! This documentation is still in development, though.", + }, + feedback: { content: null }, + footer: { content: `© ${new Date().getFullYear()} hyperglass` }, + editLink: { component: null }, + chat: { + link: "https://netdev.chat/", + icon: ( + + NetDev Chat + + + ), + }, + project: { + link: "https://github.com/thatmattlove/hyperglass", + }, +}; + +export default config; diff --git a/docs/tsconfig.json b/docs/tsconfig.json index 4360f0da1..4fe9021ba 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -1,4 +1,26 @@ { - "extends": "@tsconfig/docusaurus/tsconfig.json", - "include": ["src/"] + "compilerOptions": { + "target": "ESNext", + "module": "esnext", + "downlevelIteration": true, + "strict": true, + "baseUrl": ".", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "noEmit": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "~/*": ["./*"] + } + }, + + "exclude": ["node_modules", ".next"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.mjs"] } diff --git a/docs/yarn.lock b/docs/yarn.lock deleted file mode 100644 index d488929d2..000000000 --- a/docs/yarn.lock +++ /dev/null @@ -1,9192 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@algolia/autocomplete-core@1.0.0-alpha.44": - version "1.0.0-alpha.44" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.0.0-alpha.44.tgz#e626dba45f5f3950d6beb0ab055395ef0f7e8bb2" - integrity sha512-2iMXthldMIDXtlbg9omRKLgg1bLo2ZzINAEqwhNjUeyj1ceEyL1ck6FY0VnJpf2LsjmNthHCz2BuFk+nYUeDNA== - dependencies: - "@algolia/autocomplete-shared" "1.0.0-alpha.44" - -"@algolia/autocomplete-preset-algolia@1.0.0-alpha.44": - version "1.0.0-alpha.44" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.0.0-alpha.44.tgz#0ea0b255d0be10fbe262e281472dd6e4619b62ba" - integrity sha512-DCHwo5ovzg9k2ejUolGNTLFnIA7GpsrkbNJTy1sFbMnYfBmeK8egZPZnEl7lBTr27OaZu7IkWpTepLVSztZyng== - dependencies: - "@algolia/autocomplete-shared" "1.0.0-alpha.44" - -"@algolia/autocomplete-shared@1.0.0-alpha.44": - version "1.0.0-alpha.44" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.0.0-alpha.44.tgz#db13902ad1667e455711b77d08cae1a0feafaa48" - integrity sha512-2oQZPERYV+yNx/yoVWYjZZdOqsitJ5dfxXJjL18yczOXH6ujnsq+DTczSrX+RjzjQdVeJ1UAG053EJQF/FOiMg== - -"@algolia/cache-browser-local-storage@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.9.1.tgz#784e91580dcca00a8280b0905197f5abbbdf4b48" - integrity sha512-bAUU9vKCy45uTTlzJw0LYu1IjoZsmzL6lgjaVFaW1crhX/4P+JD5ReQv3n/wpiXSFaHq1WEO3WyH2g3ymzeipQ== - dependencies: - "@algolia/cache-common" "4.9.1" - -"@algolia/cache-common@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.9.1.tgz#2d5f37ba7aab7db76627c4a4fce51a7fd137fa65" - integrity sha512-tcvw4mOfFy44V4ZxDEy9wNGr6vFROZKRpXKTEBgdw/WBn6mX51H1ar4RWtceDEcDU4H5fIv5tsY3ip2hU+fTPg== - -"@algolia/cache-in-memory@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.9.1.tgz#3fd1d67aec804b6cc8439015b8b9c712a45c7ae0" - integrity sha512-IEJrHonvdymW2CnRfJtsTVWyfAH05xPEFkGXGCw00+6JNCj8Dln3TeaRLiaaY1srlyGedkemekQm1/Xb46CGOQ== - dependencies: - "@algolia/cache-common" "4.9.1" - -"@algolia/client-account@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.9.1.tgz#f2c1b3e49de2ee1fca44b8b5e64e1ce0dbdff0db" - integrity sha512-Shpjeuwb7i2LR5QuWREb6UbEQLGB+Pl/J5+wPgILJDP/uWp7jpl0ase9mYNQGKj7TjztpSpQCPZ3dSHPnzZPfw== - dependencies: - "@algolia/client-common" "4.9.1" - "@algolia/client-search" "4.9.1" - "@algolia/transporter" "4.9.1" - -"@algolia/client-analytics@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.9.1.tgz#56972496526910c53c5ce7844f4571efba63eb5f" - integrity sha512-/g6OkOSIA+A0t/tjvbL6iG/zV4El4LPFgv/tcAYHTH27BmlNtnEXw+iFpGjeUlQoPily9WVB3QNLMJkaNwL3HA== - dependencies: - "@algolia/client-common" "4.9.1" - "@algolia/client-search" "4.9.1" - "@algolia/requester-common" "4.9.1" - "@algolia/transporter" "4.9.1" - -"@algolia/client-common@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.9.1.tgz#ae313b65d3249efcb4fafd2e92ed1fa2fd075482" - integrity sha512-UziRTZ8km3qwoVPIyEre8TV6V+MX7UtbfVqPmSafZ0xu41UUZ+sL56YoKjOXkbKuybeIC9prXMGy/ID5bXkTqg== - dependencies: - "@algolia/requester-common" "4.9.1" - "@algolia/transporter" "4.9.1" - -"@algolia/client-recommendation@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/client-recommendation/-/client-recommendation-4.9.1.tgz#217af2a38d37ab12cf23a419cc9a576af9d15b13" - integrity sha512-Drtvvm1PNIOpYf4HFlkPFstFQ3IsN+TRmxur2F7y6Faplb5ybISa8ithu1tmlTdyTf3A78hQUQjgJet6qD2XZw== - dependencies: - "@algolia/client-common" "4.9.1" - "@algolia/requester-common" "4.9.1" - "@algolia/transporter" "4.9.1" - -"@algolia/client-search@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.9.1.tgz#a2fbc47a1b343dade9a8b06310231d51ff675b1b" - integrity sha512-r9Cw2r8kJr45iYncFDht6EshARghU265wuY8Q8oHrpFHjAziEYdsUOdNmQKbsSH5J3gLjDPx1EI5DzVd6ivn3w== - dependencies: - "@algolia/client-common" "4.9.1" - "@algolia/requester-common" "4.9.1" - "@algolia/transporter" "4.9.1" - -"@algolia/logger-common@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.9.1.tgz#3323834095f2916338d2535d2df91c4723ac19f2" - integrity sha512-9mPrbFlFyPT7or/7PXTiJjyOewWB9QRkZKVXkt5zHAUiUzGxmmdpJIGpPv3YQnDur8lXrXaRI0MHXUuIDMY1ng== - -"@algolia/logger-console@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.9.1.tgz#c324ef26843dbed06b44586309331dbb949744ad" - integrity sha512-74VUwjtFjFpjZpi3QoHIPv0kcr3vWUSHX/Vs8PJW3lPsD4CgyhFenQbG9v+ZnyH0JrJwiYTtzfmrVh7IMWZGrQ== - dependencies: - "@algolia/logger-common" "4.9.1" - -"@algolia/requester-browser-xhr@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.9.1.tgz#0812f3c7c4105a4646c0fba8429b172b2d0e01c5" - integrity sha512-zc46tk5o0ikOAz3uYiRAMxC2iVKAMFKT7nNZnLB5IzT0uqAh7pz/+D/UvIxP4bKmsllpBSnPcpfQF+OI4Ag/BA== - dependencies: - "@algolia/requester-common" "4.9.1" - -"@algolia/requester-common@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.9.1.tgz#50fcf4c7c1ed7ae13159167ac1da2844d036a630" - integrity sha512-9hPgXnlCSbqJqF69M5x5WN3h51Dc+mk/iWNeJSVxExHGvCDfBBZd0v6S15i8q2a9cD1I2RnhMpbnX5BmGtabVA== - -"@algolia/requester-node-http@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.9.1.tgz#70054a0aa5643072404fcb68042eec97c7abd1c8" - integrity sha512-vYNVbSCuyrCSCjHBQJk+tLZtWCjvvDf5tSbRJjyJYMqpnXuIuP7gZm24iHil4NPYBhbBj5NU2ZDAhc/gTn75Ag== - dependencies: - "@algolia/requester-common" "4.9.1" - -"@algolia/transporter@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.9.1.tgz#63ef3d9ae3b6556fa1ff1e6265bbab482bd084b7" - integrity sha512-AbjFfGzX+cAuj7Qyc536OxIQzjFOA5FU2ANGStx8LBH+AKXScwfkx67C05riuaRR5adSCLMSEbVvUscH0nF+6A== - dependencies: - "@algolia/cache-common" "4.9.1" - "@algolia/logger-common" "4.9.1" - "@algolia/requester-common" "4.9.1" - -"@babel/code-frame@7.10.4", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.5.5": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" - integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== - dependencies: - "@babel/highlight" "^7.12.13" - -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.4": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.4.tgz#45720fe0cecf3fd42019e1d12cc3d27fadc98d58" - integrity sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ== - -"@babel/core@7.12.9": - version "7.12.9" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" - integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.5" - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helpers" "^7.12.5" - "@babel/parser" "^7.12.7" - "@babel/template" "^7.12.7" - "@babel/traverse" "^7.12.9" - "@babel/types" "^7.12.7" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.19" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/core@^7.12.16", "@babel/core@^7.12.3": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.3.tgz#5395e30405f0776067fbd9cf0884f15bfb770a38" - integrity sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.14.3" - "@babel/helper-compilation-targets" "^7.13.16" - "@babel/helper-module-transforms" "^7.14.2" - "@babel/helpers" "^7.14.0" - "@babel/parser" "^7.14.3" - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.14.2" - "@babel/types" "^7.14.2" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.1.2" - semver "^6.3.0" - source-map "^0.5.0" - -"@babel/generator@^7.12.15", "@babel/generator@^7.12.5", "@babel/generator@^7.14.2", "@babel/generator@^7.14.3": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.3.tgz#0c2652d91f7bddab7cccc6ba8157e4f40dcedb91" - integrity sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA== - dependencies: - "@babel/types" "^7.14.2" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-annotate-as-pure@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" - integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-annotate-as-pure@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" - integrity sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-annotate-as-pure@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" - integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.12.13.tgz#6bc20361c88b0a74d05137a65cac8d3cbf6f61fc" - integrity sha512-CZOv9tGphhDRlVjVkAgm8Nhklm9RzSmWpX2my+t7Ua/KT616pEzXsQCjinzvkRvHWJ9itO4f296efroX23XCMA== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.12.13" - "@babel/types" "^7.12.13" - -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.16", "@babel/helper-compilation-targets@^7.14.4": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.4.tgz#33ebd0ffc34248051ee2089350a929ab02f2a516" - integrity sha512-JgdzOYZ/qGaKTVkn5qEDV/SXAh8KcyUVkCoSWGN8T3bwrgd6m+/dJa2kVGi6RJYJgEYPBdZ84BZp9dUjNWkBaA== - dependencies: - "@babel/compat-data" "^7.14.4" - "@babel/helper-validator-option" "^7.12.17" - browserslist "^4.16.6" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.14.0", "@babel/helper-create-class-features-plugin@^7.14.3", "@babel/helper-create-class-features-plugin@^7.14.4": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.4.tgz#abf888d836a441abee783c75229279748705dc42" - integrity sha512-idr3pthFlDCpV+p/rMgGLGYIVtazeatrSOQk8YzO2pAepIjQhCN3myeihVg58ax2bbbGK9PUE1reFi7axOYIOw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - "@babel/helper-function-name" "^7.14.2" - "@babel/helper-member-expression-to-functions" "^7.13.12" - "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/helper-replace-supers" "^7.14.4" - "@babel/helper-split-export-declaration" "^7.12.13" - -"@babel/helper-create-regexp-features-plugin@^7.12.13": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.3.tgz#149aa6d78c016e318c43e2409a0ae9c136a86688" - integrity sha512-JIB2+XJrb7v3zceV2XzDhGIB902CmKGSpSl4q2C6agU9SNLG/2V1RtFRGPG1Ajh9STj3+q6zJMOC+N/pp2P9DA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - regexpu-core "^4.7.1" - -"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" - integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-regex" "^7.8.3" - regexpu-core "^4.7.0" - -"@babel/helper-define-polyfill-provider@^0.2.2": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz#0525edec5094653a282688d34d846e4c75e9c0b6" - integrity sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew== - dependencies: - "@babel/helper-compilation-targets" "^7.13.0" - "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/traverse" "^7.13.0" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-explode-assignable-expression@^7.12.13": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.13.0.tgz#17b5c59ff473d9f956f40ef570cf3a76ca12657f" - integrity sha512-qS0peLTDP8kOisG1blKbaoBg/o9OSa1qoumMjTK5pM+KDTtpxpsiubnCGP34vK8BXGcb2M9eigwgvoJryrzwWA== - dependencies: - "@babel/types" "^7.13.0" - -"@babel/helper-function-name@^7.12.13", "@babel/helper-function-name@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz#397688b590760b6ef7725b5f0860c82427ebaac2" - integrity sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ== - dependencies: - "@babel/helper-get-function-arity" "^7.12.13" - "@babel/template" "^7.12.13" - "@babel/types" "^7.14.2" - -"@babel/helper-get-function-arity@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" - integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-hoist-variables@^7.13.0": - version "7.13.16" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.16.tgz#1b1651249e94b51f8f0d33439843e33e39775b30" - integrity sha512-1eMtTrXtrwscjcAeO4BVK+vvkxaLJSPFz1w1KLawz6HLNi9bPFGBNwwDyVfiu1Tv/vRRFYfoGaKhmAQPGPn5Wg== - dependencies: - "@babel/traverse" "^7.13.15" - "@babel/types" "^7.13.16" - -"@babel/helper-member-expression-to-functions@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72" - integrity sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw== - dependencies: - "@babel/types" "^7.13.12" - -"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" - integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA== - dependencies: - "@babel/types" "^7.13.12" - -"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.13.0", "@babel/helper-module-transforms@^7.14.0", "@babel/helper-module-transforms@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz#ac1cc30ee47b945e3e0c4db12fa0c5389509dfe5" - integrity sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA== - dependencies: - "@babel/helper-module-imports" "^7.13.12" - "@babel/helper-replace-supers" "^7.13.12" - "@babel/helper-simple-access" "^7.13.12" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/helper-validator-identifier" "^7.14.0" - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.14.2" - "@babel/types" "^7.14.2" - -"@babel/helper-optimise-call-expression@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea" - integrity sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-plugin-utils@7.10.4", "@babel/helper-plugin-utils@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" - integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" - integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== - -"@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af" - integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ== - -"@babel/helper-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" - integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== - dependencies: - lodash "^4.17.13" - -"@babel/helper-remap-async-to-generator@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.13.0.tgz#376a760d9f7b4b2077a9dd05aa9c3927cadb2209" - integrity sha512-pUQpFBE9JvC9lrQbpX0TmeNIy5s7GnZjna2lhhcHC7DzgBs6fWn722Y5cfwgrtrqc7NAJwMvOa0mKhq6XaE4jg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - "@babel/helper-wrap-function" "^7.13.0" - "@babel/types" "^7.13.0" - -"@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.12", "@babel/helper-replace-supers@^7.14.4": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.14.4.tgz#b2ab16875deecfff3ddfcd539bc315f72998d836" - integrity sha512-zZ7uHCWlxfEAAOVDYQpEf/uyi1dmeC7fX4nCf2iz9drnCwi1zvwXL3HwWWNXUQEJ1k23yVn3VbddiI9iJEXaTQ== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.13.12" - "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/traverse" "^7.14.2" - "@babel/types" "^7.14.4" - -"@babel/helper-simple-access@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6" - integrity sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA== - dependencies: - "@babel/types" "^7.13.12" - -"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" - integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-split-export-declaration@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" - integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/helper-validator-identifier@^7.12.11", "@babel/helper-validator-identifier@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" - integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A== - -"@babel/helper-validator-identifier@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" - integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== - -"@babel/helper-validator-option@^7.12.17": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" - integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw== - -"@babel/helper-wrap-function@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.13.0.tgz#bdb5c66fda8526ec235ab894ad53a1235c79fcc4" - integrity sha512-1UX9F7K3BS42fI6qd2A4BjKzgGjToscyZTdp1DjknHLCIvpgne6918io+aL5LXFcER/8QWiwpoY902pVEqgTXA== - dependencies: - "@babel/helper-function-name" "^7.12.13" - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.13.0" - "@babel/types" "^7.13.0" - -"@babel/helpers@^7.12.5", "@babel/helpers@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.0.tgz#ea9b6be9478a13d6f961dbb5f36bf75e2f3b8f62" - integrity sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg== - dependencies: - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.14.0" - "@babel/types" "^7.14.0" - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.12.13": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf" - integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg== - dependencies: - "@babel/helper-validator-identifier" "^7.14.0" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.12.13", "@babel/parser@^7.12.16", "@babel/parser@^7.12.7", "@babel/parser@^7.14.2", "@babel/parser@^7.14.3": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.4.tgz#a5c560d6db6cd8e6ed342368dea8039232cbab18" - integrity sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA== - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz#a3484d84d0b549f3fc916b99ee4783f26fabad2a" - integrity sha512-d0u3zWKcoZf379fOeJdr1a5WPDny4aOFZ6hlfKivgK0LY7ZxNfoaHL2fWwdGtHyVvra38FC+HVYkO+byfSA8AQ== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" - "@babel/plugin-proposal-optional-chaining" "^7.13.12" - -"@babel/plugin-proposal-async-generator-functions@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.2.tgz#3a2085abbf5d5f962d480dbc81347385ed62eb1e" - integrity sha512-b1AM4F6fwck4N8ItZ/AtC4FP/cqZqmKRQ4FaTDutwSYyjuhtvsGEMLK4N/ztV/ImP40BjIDyMgBQAeAMsQYVFQ== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-remap-async-to-generator" "^7.13.0" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz#146376000b94efd001e57a40a88a525afaab9f37" - integrity sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.13.0" - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-proposal-class-static-block@^7.14.3": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.3.tgz#5a527e2cae4a4753119c3a3e7f64ecae8ccf1360" - integrity sha512-HEjzp5q+lWSjAgJtSluFDrGGosmwTgKwCXdDQZvhKsRlwv3YdkUEqxNrrjesJd+B9E9zvr1PVPVBvhYZ9msjvQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.14.3" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-class-static-block" "^7.12.13" - -"@babel/plugin-proposal-dynamic-import@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.2.tgz#01ebabd7c381cff231fa43e302939a9de5be9d9f" - integrity sha512-oxVQZIWFh91vuNEMKltqNsKLFWkOIyJc95k2Gv9lWVyDfPUQGSSlbDEgWuJUU1afGE9WwlzpucMZ3yDRHIItkA== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.2.tgz#62542f94aa9ce8f6dba79eec698af22112253791" - integrity sha512-sRxW3z3Zp3pFfLAgVEvzTFutTXax837oOatUIvSG9o5gRj9mKwm3br1Se5f4QalTQs9x4AzlA/HrCWbQIHASUQ== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.2.tgz#830b4e2426a782e8b2878fbfe2cba85b70cbf98c" - integrity sha512-w2DtsfXBBJddJacXMBhElGEYqCZQqN99Se1qeYn8DVLB33owlrlLftIbMzn5nz1OITfDVknXF433tBrLEAOEjA== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.2.tgz#222348c080a1678e0e74ea63fe76f275882d1fd7" - integrity sha512-1JAZtUrqYyGsS7IDmFeaem+/LJqujfLZ2weLR9ugB0ufUPjzf8cguyVT1g5im7f7RXxuLq1xUxEzvm68uYRtGg== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.2.tgz#425b11dc62fc26939a2ab42cbba680bdf5734546" - integrity sha512-ebR0zU9OvI2N4qiAC38KIAK75KItpIPTpAtd2r4OZmMFeKbKJpUFLYP2EuDut82+BmYi8sz42B+TfTptJ9iG5Q== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.2.tgz#82b4cc06571143faf50626104b335dd71baa4f9e" - integrity sha512-DcTQY9syxu9BpU3Uo94fjCB3LN9/hgPS8oUL7KrSW3bA2ePrKZZPJcc5y0hoJAM9dft3pGfErtEUvxXQcfLxUg== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069" - integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.12.1" - -"@babel/plugin-proposal-object-rest-spread@^7.14.4": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.4.tgz#0e2b4de419915dc0b409378e829412e2031777c4" - integrity sha512-AYosOWBlyyXEagrPRfLJ1enStufsr7D1+ddpj8OLi9k7B6+NdZ0t/9V7Fh+wJ4g2Jol8z2JkgczYqtWrZd4vbA== - dependencies: - "@babel/compat-data" "^7.14.4" - "@babel/helper-compilation-targets" "^7.14.4" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.14.2" - -"@babel/plugin-proposal-optional-catch-binding@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.2.tgz#150d4e58e525b16a9a1431bd5326c4eed870d717" - integrity sha512-XtkJsmJtBaUbOxZsNk0Fvrv8eiqgneug0A6aqLFZ4TSkar2L5dSXWcnUKHgmjJt49pyB/6ZHvkr3dPgl9MOWRQ== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.13.12", "@babel/plugin-proposal-optional-chaining@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.2.tgz#df8171a8b9c43ebf4c1dabe6311b432d83e1b34e" - integrity sha512-qQByMRPwMZJainfig10BoaDldx/+VDtNcrA7qdNaEOAj6VXud+gfrkA8j4CRAU5HjnWREXqIpSpH30qZX1xivA== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz#04bd4c6d40f6e6bbfa2f57e2d8094bad900ef787" - integrity sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.13.0" - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-proposal-private-property-in-object@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.0.tgz#b1a1f2030586b9d3489cc26179d2eb5883277636" - integrity sha512-59ANdmEwwRUkLjB7CRtwJxxwtjESw+X2IePItA+RGQh+oy5RmpCh/EvVVvh5XQc3yxsm5gtv0+i9oBZhaDNVTg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - "@babel/helper-create-class-features-plugin" "^7.14.0" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-private-property-in-object" "^7.14.0" - -"@babel/plugin-proposal-unicode-property-regex@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.13.tgz#bebde51339be829c17aaaaced18641deb62b39ba" - integrity sha512-XyJmZidNfofEkqFV5VC/bLabGmO5QzenPO/YOfGuEbgU+2sSwMmio3YLb4WtBgcmmdwZHyVyv8on77IUjQ5Gvg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" - integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.8" - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.12.13.tgz#8e3d674b0613e67975ceac2776c97b60cafc5c9c" - integrity sha512-ZmKQ0ZXR0nYpHZIIuj9zE7oIqCx2hw9TKi+lIo73NNrMPAZGHfS92/VRV0ZmPj6H2ffBgyFHXvJ5NYsNeEaP2A== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926" - integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-jsx@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz#044fb81ebad6698fe62c478875575bcbb9b70f15" - integrity sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@7.8.3", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.0.tgz#762a4babec61176fec6c88480dec40372b140c0b" - integrity sha512-bda3xF8wGl5/5btF794utNOL0Jw+9jE5C1sLZcoK7c4uonE/y3iQiyG+KbkF3WBV/paX58VCpjhxLPkdj5Fe4w== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-syntax-top-level-await@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz#c5f0fa6e249f5b739727f923540cf7a806130178" - integrity sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-typescript@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz#9dff111ca64154cef0f4dc52cf843d9f12ce4474" - integrity sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-arrow-functions@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz#10a59bebad52d637a027afa692e8d5ceff5e3dae" - integrity sha512-96lgJagobeVmazXFaDrbmCLQxBysKu7U6Do3mLsx27gf5Dk85ezysrs2BZUpXD703U/Su1xTBDxxar2oa4jAGg== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-async-to-generator@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz#8e112bf6771b82bf1e974e5e26806c5c99aa516f" - integrity sha512-3j6E004Dx0K3eGmhxVJxwwI89CTJrce7lg3UrtFuDAVQ/2+SJ/h/aSFOeE6/n0WB1GsOffsJp6MnPQNQ8nmwhg== - dependencies: - "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-remap-async-to-generator" "^7.13.0" - -"@babel/plugin-transform-block-scoped-functions@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.13.tgz#a9bf1836f2a39b4eb6cf09967739de29ea4bf4c4" - integrity sha512-zNyFqbc3kI/fVpqwfqkg6RvBgFpC4J18aKKMmv7KdQ/1GgREapSJAykLMVNwfRGO3BtHj3YQZl8kxCXPcVMVeg== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-block-scoping@^7.14.4": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.4.tgz#caf140b0b2e2462c509553d140e6d0abefb61ed8" - integrity sha512-5KdpkGxsZlTk+fPleDtGKsA+pon28+ptYmMO8GBSa5fHERCJWAzj50uAfCKBqq42HO+Zot6JF1x37CRprwmN4g== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-classes@^7.14.4": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.4.tgz#a83c15503fc71a0f99e876fdce7dadbc6575ec3a" - integrity sha512-p73t31SIj6y94RDVX57rafVjttNr8MvKEgs5YFatNB/xC68zM3pyosuOEcQmYsYlyQaGY9R7rAULVRcat5FKJQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - "@babel/helper-function-name" "^7.14.2" - "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-replace-supers" "^7.14.4" - "@babel/helper-split-export-declaration" "^7.12.13" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.13.0.tgz#845c6e8b9bb55376b1fa0b92ef0bdc8ea06644ed" - integrity sha512-RRqTYTeZkZAz8WbieLTvKUEUxZlUTdmL5KGMyZj7FnMfLNKV4+r5549aORG/mgojRmFlQMJDUupwAMiF2Q7OUg== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-destructuring@^7.14.4": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.4.tgz#acbec502e9951f30f4441eaca1d2f29efade59ed" - integrity sha512-JyywKreTCGTUsL1OKu1A3ms/R1sTP0WxbpXlALeGzF53eB3bxtNkYdMj9SDgK7g6ImPy76J5oYYKoTtQImlhQA== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-dotall-regex@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.13.tgz#3f1601cc29905bfcb67f53910f197aeafebb25ad" - integrity sha512-foDrozE65ZFdUC2OfgeOCrEPTxdB3yjqxpXh8CH+ipd9CHd4s/iq81kcUpyH8ACGNEPdFqbtzfgzbT/ZGlbDeQ== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" - integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-transform-duplicate-keys@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.13.tgz#6f06b87a8b803fd928e54b81c258f0a0033904de" - integrity sha512-NfADJiiHdhLBW3pulJlJI2NB0t4cci4WTZ8FtdIuNc2+8pslXdPtRRAEWqUY+m9kNOk2eRYbTAOipAxlrOcwwQ== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-exponentiation-operator@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.13.tgz#4d52390b9a273e651e4aba6aee49ef40e80cd0a1" - integrity sha512-fbUelkM1apvqez/yYx1/oICVnGo2KM5s63mhGylrmXUxK/IAXSIf87QIxVfZldWf4QsOafY6vV3bX8aMHSvNrA== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-for-of@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.13.0.tgz#c799f881a8091ac26b54867a845c3e97d2696062" - integrity sha512-IHKT00mwUVYE0zzbkDgNRP6SRzvfGCYsOxIRz8KsiaaHCcT9BWIkO+H9QRJseHBLOGBZkHUdHiqj6r0POsdytg== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-function-name@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.13.tgz#bb024452f9aaed861d374c8e7a24252ce3a50051" - integrity sha512-6K7gZycG0cmIwwF7uMK/ZqeCikCGVBdyP2J5SKNCXO5EOHcqi+z7Jwf8AmyDNcBgxET8DrEtCt/mPKPyAzXyqQ== - dependencies: - "@babel/helper-function-name" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-literals@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.13.tgz#2ca45bafe4a820197cf315794a4d26560fe4bdb9" - integrity sha512-FW+WPjSR7hiUxMcKqyNjP05tQ2kmBCdpEpZHY1ARm96tGQCCBvXKnpjILtDplUnJ/eHZ0lALLM+d2lMFSpYJrQ== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-member-expression-literals@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.13.tgz#5ffa66cd59b9e191314c9f1f803b938e8c081e40" - integrity sha512-kxLkOsg8yir4YeEPHLuO2tXP9R/gTjpuTOjshqSpELUN3ZAg2jfDnKUvzzJxObun38sw3wm4Uu69sX/zA7iRvg== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-modules-amd@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.2.tgz#6622806fe1a7c07a1388444222ef9535f2ca17b0" - integrity sha512-hPC6XBswt8P3G2D1tSV2HzdKvkqOpmbyoy+g73JG0qlF/qx2y3KaMmXb1fLrpmWGLZYA0ojCvaHdzFWjlmV+Pw== - dependencies: - "@babel/helper-module-transforms" "^7.14.2" - "@babel/helper-plugin-utils" "^7.13.0" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz#52bc199cb581e0992edba0f0f80356467587f161" - integrity sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ== - dependencies: - "@babel/helper-module-transforms" "^7.14.0" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-simple-access" "^7.13.12" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-systemjs@^7.13.8": - version "7.13.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz#6d066ee2bff3c7b3d60bf28dec169ad993831ae3" - integrity sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A== - dependencies: - "@babel/helper-hoist-variables" "^7.13.0" - "@babel/helper-module-transforms" "^7.13.0" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-validator-identifier" "^7.12.11" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-umd@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.0.tgz#2f8179d1bbc9263665ce4a65f305526b2ea8ac34" - integrity sha512-nPZdnWtXXeY7I87UZr9VlsWme3Y0cfFFE41Wbxz4bbaexAjNMInXPFUpRRUJ8NoMm0Cw+zxbqjdPmLhcjfazMw== - dependencies: - "@babel/helper-module-transforms" "^7.14.0" - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.13.tgz#2213725a5f5bbbe364b50c3ba5998c9599c5c9d9" - integrity sha512-Xsm8P2hr5hAxyYblrfACXpQKdQbx4m2df9/ZZSQ8MAhsadw06+jW7s9zsSw6he+mJZXRlVMyEnVktJo4zjk1WA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" - -"@babel/plugin-transform-new-target@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.13.tgz#e22d8c3af24b150dd528cbd6e685e799bf1c351c" - integrity sha512-/KY2hbLxrG5GTQ9zzZSc3xWiOy379pIETEhbtzwZcw9rvuaVV4Fqy7BYGYOWZnaoXIQYbbJ0ziXLa/sKcGCYEQ== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-object-super@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.13.tgz#b4416a2d63b8f7be314f3d349bd55a9c1b5171f7" - integrity sha512-JzYIcj3XtYspZDV8j9ulnoMPZZnF/Cj0LUxPOjR89BdBVx+zYJI9MdMIlUZjbXDX+6YVeS6I3e8op+qQ3BYBoQ== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - "@babel/helper-replace-supers" "^7.12.13" - -"@babel/plugin-transform-parameters@^7.12.1", "@babel/plugin-transform-parameters@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.2.tgz#e4290f72e0e9e831000d066427c4667098decc31" - integrity sha512-NxoVmA3APNCC1JdMXkdYXuQS+EMdqy0vIwyDHeKHiJKRxmp1qGSdb0JLEIoPRhkx6H/8Qi3RJ3uqOCYw8giy9A== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-property-literals@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.13.tgz#4e6a9e37864d8f1b3bc0e2dce7bf8857db8b1a81" - integrity sha512-nqVigwVan+lR+g8Fj8Exl0UQX2kymtjcWfMOYM1vTYEKujeyv2SkMgazf2qNcK7l4SDiKyTA/nHCPqL4e2zo1A== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-react-constant-elements@^7.12.1": - version "7.13.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.13.13.tgz#0208b1d942bf939cd4f7aa5b255d42602aa4a920" - integrity sha512-SNJU53VM/SjQL0bZhyU+f4kJQz7bQQajnrZRSaU21hruG/NWY41AEM9AWXeXX90pYr/C2yAmTgI6yW3LlLrAUQ== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-react-display-name@^7.12.13": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.14.2.tgz#2e854544d42ab3bb9c21f84e153d62e800fbd593" - integrity sha512-zCubvP+jjahpnFJvPaHPiGVfuVUjXHhFvJKQdNnsmSsiU9kR/rCZ41jHc++tERD2zV+p7Hr6is+t5b6iWTCqSw== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-react-jsx-development@^7.12.17": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.17.tgz#f510c0fa7cd7234153539f9a362ced41a5ca1447" - integrity sha512-BPjYV86SVuOaudFhsJR1zjgxxOhJDt6JHNoD48DxWEIxUCAMjV1ys6DYw4SDYZh0b1QsS2vfIA9t/ZsQGsDOUQ== - dependencies: - "@babel/plugin-transform-react-jsx" "^7.12.17" - -"@babel/plugin-transform-react-jsx@^7.12.17", "@babel/plugin-transform-react-jsx@^7.13.12": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.3.tgz#0e26597805cf0862da735f264550933c38babb66" - integrity sha512-uuxuoUNVhdgYzERiHHFkE4dWoJx+UFVyuAl0aqN8P2/AKFHwqgUC5w2+4/PjpKXJsFgBlYAFXlUmDQ3k3DUkXw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - "@babel/helper-module-imports" "^7.13.12" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-jsx" "^7.12.13" - "@babel/types" "^7.14.2" - -"@babel/plugin-transform-react-pure-annotations@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42" - integrity sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-regenerator@^7.13.15": - version "7.13.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz#e5eb28945bf8b6563e7f818945f966a8d2997f39" - integrity sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ== - dependencies: - regenerator-transform "^0.14.2" - -"@babel/plugin-transform-reserved-words@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.13.tgz#7d9988d4f06e0fe697ea1d9803188aa18b472695" - integrity sha512-xhUPzDXxZN1QfiOy/I5tyye+TRz6lA7z6xaT4CLOjPRMVg1ldRf0LHw0TDBpYL4vG78556WuHdyO9oi5UmzZBg== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-runtime@^7.12.15": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.3.tgz#1fd885a2d0de1d3c223795a4e9be72c2db4515cf" - integrity sha512-t960xbi8wpTFE623ef7sd+UpEC5T6EEguQlTBJDEO05+XwnIWVfuqLw/vdLWY6IdFmtZE+65CZAfByT39zRpkg== - dependencies: - "@babel/helper-module-imports" "^7.13.12" - "@babel/helper-plugin-utils" "^7.13.0" - babel-plugin-polyfill-corejs2 "^0.2.0" - babel-plugin-polyfill-corejs3 "^0.2.0" - babel-plugin-polyfill-regenerator "^0.2.0" - semver "^6.3.0" - -"@babel/plugin-transform-shorthand-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.13.tgz#db755732b70c539d504c6390d9ce90fe64aff7ad" - integrity sha512-xpL49pqPnLtf0tVluuqvzWIgLEhuPpZzvs2yabUHSKRNlN7ScYU7aMlmavOeyXJZKgZKQRBlh8rHbKiJDraTSw== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-spread@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.13.0.tgz#84887710e273c1815ace7ae459f6f42a5d31d5fd" - integrity sha512-V6vkiXijjzYeFmQTr3dBxPtZYLPcUfY34DebOU27jIl2M/Y8Egm52Hw82CSjjPqd54GTlJs5x+CR7HeNr24ckg== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" - -"@babel/plugin-transform-sticky-regex@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.13.tgz#760ffd936face73f860ae646fb86ee82f3d06d1f" - integrity sha512-Jc3JSaaWT8+fr7GRvQP02fKDsYk4K/lYwWq38r/UGfaxo89ajud321NH28KRQ7xy1Ybc0VUE5Pz8psjNNDUglg== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-template-literals@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.13.0.tgz#a36049127977ad94438dee7443598d1cefdf409d" - integrity sha512-d67umW6nlfmr1iehCcBv69eSUSySk1EsIS8aTDX4Xo9qajAh6mYtcl4kJrBkGXuxZPEgVr7RVfAvNW6YQkd4Mw== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - -"@babel/plugin-transform-typeof-symbol@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.13.tgz#785dd67a1f2ea579d9c2be722de8c84cb85f5a7f" - integrity sha512-eKv/LmUJpMnu4npgfvs3LiHhJua5fo/CysENxa45YCQXZwKnGCQKAg87bvoqSW1fFT+HA32l03Qxsm8ouTY3ZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-typescript@^7.13.0": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.4.tgz#1c48829fa6d5f2de646060cd08abb6cda4b521a7" - integrity sha512-WYdcGNEO7mCCZ2XzRlxwGj3PgeAr50ifkofOUC/+IN/GzKLB+biDPVBUAQN2C/dVZTvEXCp80kfQ1FFZPrwykQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.14.4" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/plugin-syntax-typescript" "^7.12.13" - -"@babel/plugin-transform-unicode-escapes@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz#840ced3b816d3b5127dd1d12dcedc5dead1a5e74" - integrity sha512-0bHEkdwJ/sN/ikBHfSmOXPypN/beiGqjo+o4/5K+vxEFNPRPdImhviPakMKG4x96l85emoa0Z6cDflsdBusZbw== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-unicode-regex@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.13.tgz#b52521685804e155b1202e83fc188d34bb70f5ac" - integrity sha512-mDRzSNY7/zopwisPZ5kM9XKCfhchqIYwAKRERtEnhYscZB79VRekuRSoYbN0+KVe3y8+q1h6A4svXtP7N+UoCA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/preset-env@^7.12.1", "@babel/preset-env@^7.12.16": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.14.4.tgz#73fc3228c59727e5e974319156f304f0d6685a2d" - integrity sha512-GwMMsuAnDtULyOtuxHhzzuSRxFeP0aR/LNzrHRzP8y6AgDNgqnrfCCBm/1cRdTU75tRs28Eh76poHLcg9VF0LA== - dependencies: - "@babel/compat-data" "^7.14.4" - "@babel/helper-compilation-targets" "^7.14.4" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-validator-option" "^7.12.17" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.13.12" - "@babel/plugin-proposal-async-generator-functions" "^7.14.2" - "@babel/plugin-proposal-class-properties" "^7.13.0" - "@babel/plugin-proposal-class-static-block" "^7.14.3" - "@babel/plugin-proposal-dynamic-import" "^7.14.2" - "@babel/plugin-proposal-export-namespace-from" "^7.14.2" - "@babel/plugin-proposal-json-strings" "^7.14.2" - "@babel/plugin-proposal-logical-assignment-operators" "^7.14.2" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.2" - "@babel/plugin-proposal-numeric-separator" "^7.14.2" - "@babel/plugin-proposal-object-rest-spread" "^7.14.4" - "@babel/plugin-proposal-optional-catch-binding" "^7.14.2" - "@babel/plugin-proposal-optional-chaining" "^7.14.2" - "@babel/plugin-proposal-private-methods" "^7.13.0" - "@babel/plugin-proposal-private-property-in-object" "^7.14.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.12.13" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.12.13" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.0" - "@babel/plugin-syntax-top-level-await" "^7.12.13" - "@babel/plugin-transform-arrow-functions" "^7.13.0" - "@babel/plugin-transform-async-to-generator" "^7.13.0" - "@babel/plugin-transform-block-scoped-functions" "^7.12.13" - "@babel/plugin-transform-block-scoping" "^7.14.4" - "@babel/plugin-transform-classes" "^7.14.4" - "@babel/plugin-transform-computed-properties" "^7.13.0" - "@babel/plugin-transform-destructuring" "^7.14.4" - "@babel/plugin-transform-dotall-regex" "^7.12.13" - "@babel/plugin-transform-duplicate-keys" "^7.12.13" - "@babel/plugin-transform-exponentiation-operator" "^7.12.13" - "@babel/plugin-transform-for-of" "^7.13.0" - "@babel/plugin-transform-function-name" "^7.12.13" - "@babel/plugin-transform-literals" "^7.12.13" - "@babel/plugin-transform-member-expression-literals" "^7.12.13" - "@babel/plugin-transform-modules-amd" "^7.14.2" - "@babel/plugin-transform-modules-commonjs" "^7.14.0" - "@babel/plugin-transform-modules-systemjs" "^7.13.8" - "@babel/plugin-transform-modules-umd" "^7.14.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.13" - "@babel/plugin-transform-new-target" "^7.12.13" - "@babel/plugin-transform-object-super" "^7.12.13" - "@babel/plugin-transform-parameters" "^7.14.2" - "@babel/plugin-transform-property-literals" "^7.12.13" - "@babel/plugin-transform-regenerator" "^7.13.15" - "@babel/plugin-transform-reserved-words" "^7.12.13" - "@babel/plugin-transform-shorthand-properties" "^7.12.13" - "@babel/plugin-transform-spread" "^7.13.0" - "@babel/plugin-transform-sticky-regex" "^7.12.13" - "@babel/plugin-transform-template-literals" "^7.13.0" - "@babel/plugin-transform-typeof-symbol" "^7.12.13" - "@babel/plugin-transform-unicode-escapes" "^7.12.13" - "@babel/plugin-transform-unicode-regex" "^7.12.13" - "@babel/preset-modules" "^0.1.4" - "@babel/types" "^7.14.4" - babel-plugin-polyfill-corejs2 "^0.2.0" - babel-plugin-polyfill-corejs3 "^0.2.0" - babel-plugin-polyfill-regenerator "^0.2.0" - core-js-compat "^3.9.0" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" - integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/preset-react@^7.12.13", "@babel/preset-react@^7.12.5": - version "7.13.13" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.13.13.tgz#fa6895a96c50763fe693f9148568458d5a839761" - integrity sha512-gx+tDLIE06sRjKJkVtpZ/t3mzCDOnPG+ggHZG9lffUbX8+wC739x20YQc9V35Do6ZAxaUc/HhVHIiOzz5MvDmA== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-validator-option" "^7.12.17" - "@babel/plugin-transform-react-display-name" "^7.12.13" - "@babel/plugin-transform-react-jsx" "^7.13.12" - "@babel/plugin-transform-react-jsx-development" "^7.12.17" - "@babel/plugin-transform-react-pure-annotations" "^7.12.1" - -"@babel/preset-typescript@^7.12.16": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz#ab107e5f050609d806fbb039bec553b33462c60a" - integrity sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-validator-option" "^7.12.17" - "@babel/plugin-transform-typescript" "^7.13.0" - -"@babel/runtime-corejs3@^7.12.13": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz#6bf5fbc0b961f8e3202888cb2cd0fb7a0a9a3f66" - integrity sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg== - dependencies: - core-js-pure "^3.0.0" - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.1.2", "@babel/runtime@^7.8.4": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" - integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" - integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/template@^7.12.13", "@babel/template@^7.12.7": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" - integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/parser" "^7.12.13" - "@babel/types" "^7.12.13" - -"@babel/traverse@^7.12.13", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.15", "@babel/traverse@^7.14.0", "@babel/traverse@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.2.tgz#9201a8d912723a831c2679c7ebbf2fe1416d765b" - integrity sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.14.2" - "@babel/helper-function-name" "^7.14.2" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.14.2" - "@babel/types" "^7.14.2" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.4.tgz#369517188352e18219981efd156bfdb199fff1ee" - integrity sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.12.6", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.16", "@babel/types@^7.14.0", "@babel/types@^7.14.2", "@babel/types@^7.14.4": - version "7.14.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.4.tgz#bfd6980108168593b38b3eb48a24aa026b919bc0" - integrity sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw== - dependencies: - "@babel/helper-validator-identifier" "^7.14.0" - to-fast-properties "^2.0.0" - -"@babel/types@^7.4.4", "@babel/types@^7.8.3": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" - integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== - dependencies: - "@babel/helper-validator-identifier" "^7.9.5" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@docsearch/css@3.0.0-alpha.36": - version "3.0.0-alpha.36" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.0.0-alpha.36.tgz#0af69a86b845974d0f8cab62db0218f66b6ad2d6" - integrity sha512-zSN2SXuZPDqQaSFzYa1kOwToukqzhLHG7c66iO+/PlmWb6/RZ5cjTkG6VCJynlohRWea7AqZKWS/ptm8kM2Dmg== - -"@docsearch/react@^3.0.0-alpha.33": - version "3.0.0-alpha.36" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.0.0-alpha.36.tgz#f2dbd53ba9c389bc19aea89a3ad21782fa6b4bb5" - integrity sha512-synYZDHalvMzesFiy7kK+uoz4oTdWSTbe2cU+iiUjwFMyQ+WWjWwGVnvcvk+cjj9pRCVaZo5y5WpqNXq1j8k9Q== - dependencies: - "@algolia/autocomplete-core" "1.0.0-alpha.44" - "@algolia/autocomplete-preset-algolia" "1.0.0-alpha.44" - "@docsearch/css" "3.0.0-alpha.36" - algoliasearch "^4.0.0" - -"@docusaurus/core@2.0.0-beta.0", "@docusaurus/core@^2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.0.0-beta.0.tgz#05506ee02e7d40e9f4c8d7b4f918d26d3b191159" - integrity sha512-xWwpuEwFRKJmZvNGOpr/dyRDnx/psckLPsozQTg2hu3u81Wqu9gigWgYK/C2fPlEjxMcVw0/2WH+zwpbyWmF2Q== - dependencies: - "@babel/core" "^7.12.16" - "@babel/generator" "^7.12.15" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.12.15" - "@babel/preset-env" "^7.12.16" - "@babel/preset-react" "^7.12.13" - "@babel/preset-typescript" "^7.12.16" - "@babel/runtime" "^7.12.5" - "@babel/runtime-corejs3" "^7.12.13" - "@babel/traverse" "^7.12.13" - "@docusaurus/cssnano-preset" "2.0.0-beta.0" - "@docusaurus/react-loadable" "5.5.0" - "@docusaurus/types" "2.0.0-beta.0" - "@docusaurus/utils" "2.0.0-beta.0" - "@docusaurus/utils-validation" "2.0.0-beta.0" - "@endiliey/static-site-generator-webpack-plugin" "^4.0.0" - "@svgr/webpack" "^5.5.0" - autoprefixer "^10.2.5" - babel-loader "^8.2.2" - babel-plugin-dynamic-import-node "2.3.0" - boxen "^5.0.0" - chalk "^4.1.0" - chokidar "^3.5.1" - clean-css "^5.1.1" - commander "^5.1.0" - copy-webpack-plugin "^8.1.0" - core-js "^3.9.1" - css-loader "^5.1.1" - css-minimizer-webpack-plugin "^2.0.0" - cssnano "^5.0.1" - del "^6.0.0" - detect-port "^1.3.0" - eta "^1.12.1" - express "^4.17.1" - file-loader "^6.2.0" - fs-extra "^9.1.0" - github-slugger "^1.3.0" - globby "^11.0.2" - html-minifier-terser "^5.1.1" - html-tags "^3.1.0" - html-webpack-plugin "^5.2.0" - import-fresh "^3.3.0" - is-root "^2.1.0" - leven "^3.1.0" - lodash "^4.17.20" - mini-css-extract-plugin "^1.4.0" - module-alias "^2.2.2" - nprogress "^0.2.0" - postcss "^8.2.10" - postcss-loader "^5.2.0" - prompts "^2.4.0" - react-dev-utils "^11.0.1" - react-error-overlay "^6.0.9" - react-helmet "^6.1.0" - react-loadable "^5.5.0" - react-loadable-ssr-addon-v5-slorber "^1.0.1" - react-router "^5.2.0" - react-router-config "^5.1.1" - react-router-dom "^5.2.0" - resolve-pathname "^3.0.0" - rtl-detect "^1.0.2" - semver "^7.3.4" - serve-handler "^6.1.3" - shelljs "^0.8.4" - std-env "^2.2.1" - strip-ansi "^6.0.0" - terser-webpack-plugin "^5.1.1" - tslib "^2.1.0" - update-notifier "^5.1.0" - url-loader "^4.1.1" - wait-on "^5.2.1" - webpack "^5.28.0" - webpack-bundle-analyzer "^4.4.0" - webpack-dev-server "^3.11.2" - webpack-merge "^5.7.3" - webpackbar "^5.0.0-3" - -"@docusaurus/cssnano-preset@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.0.0-beta.0.tgz#a79223479666059565d60a505bed2bbcac770384" - integrity sha512-gqQHeQCDHZDd5NaiKZwDiyg75sBCqDyAsvmFukkDAty8xE7u9IhzbOQKvCAtwseuvzu2BNN41gnJ8bz7vZzQiw== - dependencies: - cssnano-preset-advanced "^5.0.0" - postcss "^8.2.10" - postcss-sort-media-queries "^3.8.9" - -"@docusaurus/mdx-loader@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.0.0-beta.0.tgz#7a58933994b2face62e34698db2f9c88c53c6d61" - integrity sha512-oQLS2ZeUnqw79CV37glglZpaYgFfA5Az5lT83m5tJfMUZjoK4ehG1XWBeUzWy8QQNI452yAID8jz8jihEQeCcw== - dependencies: - "@babel/parser" "^7.12.16" - "@babel/traverse" "^7.12.13" - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/utils" "2.0.0-beta.0" - "@mdx-js/mdx" "^1.6.21" - "@mdx-js/react" "^1.6.21" - escape-html "^1.0.3" - file-loader "^6.2.0" - fs-extra "^9.1.0" - github-slugger "^1.3.0" - gray-matter "^4.0.2" - mdast-util-to-string "^2.0.0" - remark-emoji "^2.1.0" - stringify-object "^3.3.0" - unist-util-visit "^2.0.2" - url-loader "^4.1.1" - webpack "^5.28.0" - -"@docusaurus/module-type-aliases@^2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.0.0-beta.0.tgz#e018adfe3c8834c526ca687eed3cbc87569a47a7" - integrity sha512-Yj+7hjy5pCK4DQ3l2JDGBF4bMY63MV/z18WK7REHniyqC66IJQT+0Gpb6Q+Oz8e2Og8AsyWIbfTXvsFWK0UhJA== - -"@docusaurus/plugin-content-blog@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-beta.0.tgz#ea7d3679ab252e8f0e58aaf80f1fc6001c72c755" - integrity sha512-lz63i5k/23RJ3Rk/2fIsYAoD8Wua3b5b0AbH2JoOhQu1iAIQiV8m91Z3XALBSzA3nBtAOIweNI7yzWL+JFSTvw== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/mdx-loader" "2.0.0-beta.0" - "@docusaurus/types" "2.0.0-beta.0" - "@docusaurus/utils" "2.0.0-beta.0" - "@docusaurus/utils-validation" "2.0.0-beta.0" - chalk "^4.1.0" - feed "^4.2.2" - fs-extra "^9.1.0" - globby "^11.0.2" - loader-utils "^2.0.0" - lodash "^4.17.20" - reading-time "^1.3.0" - remark-admonitions "^1.2.1" - tslib "^2.1.0" - webpack "^5.28.0" - -"@docusaurus/plugin-content-docs@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.0.0-beta.0.tgz#a5a1e0e95e499eefee53e4f61aeb99ac4a669648" - integrity sha512-WdDQUh2rRCbfJswVc0vY9EaAspxgziqpVEZja8+BmQR/TZh7HuLplT6GJbiFbE4RvwM3+PwG/jHMPglYDK60kw== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/mdx-loader" "2.0.0-beta.0" - "@docusaurus/types" "2.0.0-beta.0" - "@docusaurus/utils" "2.0.0-beta.0" - "@docusaurus/utils-validation" "2.0.0-beta.0" - chalk "^4.1.0" - combine-promises "^1.1.0" - execa "^5.0.0" - fs-extra "^9.1.0" - globby "^11.0.2" - import-fresh "^3.2.2" - js-yaml "^4.0.0" - loader-utils "^1.2.3" - lodash "^4.17.20" - remark-admonitions "^1.2.1" - shelljs "^0.8.4" - tslib "^2.1.0" - utility-types "^3.10.0" - webpack "^5.28.0" - -"@docusaurus/plugin-content-pages@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.0.0-beta.0.tgz#1cab3ebe0a08be74576f10c95675291bf84f848e" - integrity sha512-mk5LVVSvn+HJPKBaAs/Pceq/hTGxF2LVBvJEquuQz0NMAW3QdBWaYRRpOrL9CO8v+ygn5RuLslXsyZBsDNuhww== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/mdx-loader" "2.0.0-beta.0" - "@docusaurus/types" "2.0.0-beta.0" - "@docusaurus/utils" "2.0.0-beta.0" - "@docusaurus/utils-validation" "2.0.0-beta.0" - globby "^11.0.2" - lodash "^4.17.20" - minimatch "^3.0.4" - remark-admonitions "^1.2.1" - slash "^3.0.0" - tslib "^2.1.0" - webpack "^5.28.0" - -"@docusaurus/plugin-debug@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.0.0-beta.0.tgz#bee672b8858d88bdb229d4301785ff4692ebd17f" - integrity sha512-m75sZdF8Yccxfih3qfdQg9DucMTrYBnmeTA8GNmdVaK701Ip8t50d1pDJchtu0FSEh6vzVB9C6D2YD5YgVFp8A== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/types" "2.0.0-beta.0" - "@docusaurus/utils" "2.0.0-beta.0" - react-json-view "^1.21.1" - tslib "^2.1.0" - -"@docusaurus/plugin-google-analytics@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.0.0-beta.0.tgz#ee287fb991202d8e9b792129dcc5542ef3ccd6c9" - integrity sha512-7lHrg1L+adc8VbiaLexa15i4fdq4MRPUTLMxRPAWz+QskhisW89Ryi2/gDmfMNqLblX84Qg2RASa+2gqO4wepw== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - -"@docusaurus/plugin-google-gtag@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.0.0-beta.0.tgz#4836770130cf54ff2cd83affbff9644ee7293e9e" - integrity sha512-V7zaYbhAMv0jexm5H/5sAnoM1GHibcn9QQk5UWC++x1kE0KRuLDZHV+9OyvW5wr0wWFajod/b88SpUpSMF5u+g== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - -"@docusaurus/plugin-sitemap@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.0.0-beta.0.tgz#985d4cc3af86499f616ced024ba1fab8329e601d" - integrity sha512-dvmk8Sr+6pBkiKDb7Rjdp0GeFDWPUlayoJWK3fN3g0Fno6uxFfYhNZyXJ+ObyCA7HoW5rzeBMiO+uAja19JXTg== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/types" "2.0.0-beta.0" - "@docusaurus/utils" "2.0.0-beta.0" - "@docusaurus/utils-validation" "2.0.0-beta.0" - fs-extra "^9.1.0" - sitemap "^6.3.6" - tslib "^2.1.0" - -"@docusaurus/preset-classic@^2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.0.0-beta.0.tgz#79eb4366e6b5eb7061370019127e40172432d770" - integrity sha512-cFpR0UaAeUt5qVx1bpidhlar6tiRNITIQlxP4bOVsjbxVTZhZ/cNuIz7C+2zFPCuKIflGXdTIQOrucPmd7z51Q== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/plugin-content-blog" "2.0.0-beta.0" - "@docusaurus/plugin-content-docs" "2.0.0-beta.0" - "@docusaurus/plugin-content-pages" "2.0.0-beta.0" - "@docusaurus/plugin-debug" "2.0.0-beta.0" - "@docusaurus/plugin-google-analytics" "2.0.0-beta.0" - "@docusaurus/plugin-google-gtag" "2.0.0-beta.0" - "@docusaurus/plugin-sitemap" "2.0.0-beta.0" - "@docusaurus/theme-classic" "2.0.0-beta.0" - "@docusaurus/theme-search-algolia" "2.0.0-beta.0" - -"@docusaurus/react-loadable@5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.0.tgz#6d6f0c8fd9a434b62a1ab1f8645ee7bde5a9ec21" - integrity sha512-Ld/kwUE6yATIOTLq3JCsWiTa/drisajwKqBQ2Rw6IcT+sFsKfYek8F2jSH8f68AT73xX97UehduZeCSlnuCBIg== - dependencies: - prop-types "^15.6.2" - -"@docusaurus/theme-classic@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.0.0-beta.0.tgz#0ad74264dc592590bd7d8a6f6327cb83bbabc665" - integrity sha512-cBNtwAyg3be7Gk41FazMtgyibAcfuYaGHhGHIDRsXfc/qp3RhbiGiei7tyh200QT0NgKZxiVQy/r4d0mtjC++Q== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/plugin-content-blog" "2.0.0-beta.0" - "@docusaurus/plugin-content-docs" "2.0.0-beta.0" - "@docusaurus/plugin-content-pages" "2.0.0-beta.0" - "@docusaurus/theme-common" "2.0.0-beta.0" - "@docusaurus/types" "2.0.0-beta.0" - "@docusaurus/utils" "2.0.0-beta.0" - "@docusaurus/utils-validation" "2.0.0-beta.0" - "@mdx-js/mdx" "^1.6.21" - "@mdx-js/react" "^1.6.21" - chalk "^4.1.0" - clsx "^1.1.1" - copy-text-to-clipboard "^3.0.0" - fs-extra "^9.1.0" - globby "^11.0.2" - infima "0.2.0-alpha.23" - lodash "^4.17.20" - parse-numeric-range "^1.2.0" - postcss "^8.2.10" - prism-react-renderer "^1.1.1" - prismjs "^1.23.0" - prop-types "^15.7.2" - react-router-dom "^5.2.0" - rtlcss "^3.1.2" - -"@docusaurus/theme-common@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.0.0-beta.0.tgz#3674ef6482cc39efa034fd8d8b1c831588896329" - integrity sha512-2rcVmQpvbdAgnzTWuM7Bfpu+2TQm928bhlvxn226jQy7IYz8ySRlIode63HhCtpx03hpdMCkrK6HxhfEcvHjQg== - dependencies: - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/plugin-content-blog" "2.0.0-beta.0" - "@docusaurus/plugin-content-docs" "2.0.0-beta.0" - "@docusaurus/plugin-content-pages" "2.0.0-beta.0" - "@docusaurus/types" "2.0.0-beta.0" - tslib "^2.1.0" - -"@docusaurus/theme-search-algolia@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.0.0-beta.0.tgz#bfdee3981d8da72377b9045459950686d28a01fd" - integrity sha512-/GhgAm4yuwqTXWTsWnqpFYxpjTv+t45Wk8q/LmTVINa+A7b6jkMkch2lygagIt69/ufDm2Uw6eYhgrmF4DJqfQ== - dependencies: - "@docsearch/react" "^3.0.0-alpha.33" - "@docusaurus/core" "2.0.0-beta.0" - "@docusaurus/theme-common" "2.0.0-beta.0" - "@docusaurus/utils" "2.0.0-beta.0" - "@docusaurus/utils-validation" "2.0.0-beta.0" - algoliasearch "^4.8.4" - algoliasearch-helper "^3.3.4" - clsx "^1.1.1" - eta "^1.12.1" - lodash "^4.17.20" - -"@docusaurus/types@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.0.0-beta.0.tgz#f473f417bdf690cfd52611ddf6d89ff939d1f2a4" - integrity sha512-z9PI+GbtYwqTXnkX4/a/A6psDX2p8N2uWlN2f4ifrm8WY4WhR9yiTOh0uo0pIqqaUQQvkEq3o5hOXuXLECEs+w== - dependencies: - commander "^5.1.0" - joi "^17.4.0" - querystring "0.2.0" - webpack "^5.28.0" - webpack-merge "^5.7.3" - -"@docusaurus/utils-validation@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.0.0-beta.0.tgz#3e6491c269a397fed29717a1cb69109df9483461" - integrity sha512-ELl/FVJ6xBz35TisZ1NmJhjbiVXDeU++K531PEFPCPmwnQPh7S6hZXdPnR71/Kc3BmuN9X2ZkwGOqNKVfys2Bg== - dependencies: - "@docusaurus/utils" "2.0.0-beta.0" - chalk "^4.1.0" - joi "^17.4.0" - tslib "^2.1.0" - -"@docusaurus/utils@2.0.0-beta.0": - version "2.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.0.0-beta.0.tgz#6f2690fd6fcd942f0d690db1dffb96742762deb3" - integrity sha512-bvrT1EQu0maavr0Hb/lke9jmpzgVL/9tn5VQtbyahf472eJFY0bQDExllDrHK+l784SUvucqX0iaQeg0q6ySUw== - dependencies: - "@docusaurus/types" "2.0.0-beta.0" - "@types/github-slugger" "^1.3.0" - chalk "^4.1.0" - escape-string-regexp "^4.0.0" - fs-extra "^9.1.0" - gray-matter "^4.0.2" - lodash "^4.17.20" - resolve-pathname "^3.0.0" - tslib "^2.1.0" - -"@endiliey/static-site-generator-webpack-plugin@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@endiliey/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.0.tgz#94bfe58fd83aeda355de797fcb5112adaca3a6b1" - integrity sha512-3MBqYCs30qk1OBRC697NqhGouYbs71D1B8hrk/AFJC6GwF2QaJOQZtA1JYAaGSe650sZ8r5ppRTtCRXepDWlng== - dependencies: - bluebird "^3.7.1" - cheerio "^0.22.0" - eval "^0.1.4" - url "^0.11.0" - webpack-sources "^1.4.3" - -"@hapi/hoek@^9.0.0": - version "9.2.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" - integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== - -"@hapi/topo@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7" - integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@mdx-js/mdx@^1.6.21": - version "1.6.22" - resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" - integrity sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA== - dependencies: - "@babel/core" "7.12.9" - "@babel/plugin-syntax-jsx" "7.12.1" - "@babel/plugin-syntax-object-rest-spread" "7.8.3" - "@mdx-js/util" "1.6.22" - babel-plugin-apply-mdx-type-prop "1.6.22" - babel-plugin-extract-import-names "1.6.22" - camelcase-css "2.0.1" - detab "2.0.4" - hast-util-raw "6.0.1" - lodash.uniq "4.5.0" - mdast-util-to-hast "10.0.1" - remark-footnotes "2.0.0" - remark-mdx "1.6.22" - remark-parse "8.0.3" - remark-squeeze-paragraphs "4.0.0" - style-to-object "0.3.0" - unified "9.2.0" - unist-builder "2.0.3" - unist-util-visit "2.0.3" - -"@mdx-js/react@^1.6.21", "@mdx-js/react@^1.6.22": - version "1.6.22" - resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-1.6.22.tgz#ae09b4744fddc74714ee9f9d6f17a66e77c43573" - integrity sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg== - -"@mdx-js/util@1.6.22": - version "1.6.22" - resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" - integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== - -"@nodelib/fs.scandir@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" - integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== - dependencies: - "@nodelib/fs.stat" "2.0.3" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" - integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" - integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== - dependencies: - "@nodelib/fs.scandir" "2.1.3" - fastq "^1.6.0" - -"@polka/url@^1.0.0-next.15": - version "1.0.0-next.15" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.15.tgz#6a9d143f7f4f49db2d782f9e1c8839a29b43ae23" - integrity sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA== - -"@sideway/address@^4.1.0": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.2.tgz#811b84333a335739d3969cfc434736268170cad1" - integrity sha512-idTz8ibqWFrPU8kMirL0CoPH/A29XOzzAzpyN3zQ4kAWnzmNfFmRaoMNN6VI8ske5M73HZyhIaW4OuSFIdM4oA== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" - integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== - -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - -"@svgr/babel-plugin-add-jsx-attribute@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906" - integrity sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg== - -"@svgr/babel-plugin-remove-jsx-attribute@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz#6b2c770c95c874654fd5e1d5ef475b78a0a962ef" - integrity sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg== - -"@svgr/babel-plugin-remove-jsx-empty-expression@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz#25621a8915ed7ad70da6cea3d0a6dbc2ea933efd" - integrity sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA== - -"@svgr/babel-plugin-replace-jsx-attribute-value@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz#0b221fc57f9fcd10e91fe219e2cd0dd03145a897" - integrity sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ== - -"@svgr/babel-plugin-svg-dynamic-title@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz#139b546dd0c3186b6e5db4fefc26cb0baea729d7" - integrity sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg== - -"@svgr/babel-plugin-svg-em-dimensions@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz#6543f69526632a133ce5cabab965deeaea2234a0" - integrity sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw== - -"@svgr/babel-plugin-transform-react-native-svg@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz#00bf9a7a73f1cad3948cdab1f8dfb774750f8c80" - integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q== - -"@svgr/babel-plugin-transform-svg-component@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz#583a5e2a193e214da2f3afeb0b9e8d3250126b4a" - integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ== - -"@svgr/babel-preset@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-5.5.0.tgz#8af54f3e0a8add7b1e2b0fcd5a882c55393df327" - integrity sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig== - dependencies: - "@svgr/babel-plugin-add-jsx-attribute" "^5.4.0" - "@svgr/babel-plugin-remove-jsx-attribute" "^5.4.0" - "@svgr/babel-plugin-remove-jsx-empty-expression" "^5.0.1" - "@svgr/babel-plugin-replace-jsx-attribute-value" "^5.0.1" - "@svgr/babel-plugin-svg-dynamic-title" "^5.4.0" - "@svgr/babel-plugin-svg-em-dimensions" "^5.4.0" - "@svgr/babel-plugin-transform-react-native-svg" "^5.4.0" - "@svgr/babel-plugin-transform-svg-component" "^5.5.0" - -"@svgr/core@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-5.5.0.tgz#82e826b8715d71083120fe8f2492ec7d7874a579" - integrity sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ== - dependencies: - "@svgr/plugin-jsx" "^5.5.0" - camelcase "^6.2.0" - cosmiconfig "^7.0.0" - -"@svgr/hast-util-to-babel-ast@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz#5ee52a9c2533f73e63f8f22b779f93cd432a5461" - integrity sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ== - dependencies: - "@babel/types" "^7.12.6" - -"@svgr/plugin-jsx@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz#1aa8cd798a1db7173ac043466d7b52236b369000" - integrity sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA== - dependencies: - "@babel/core" "^7.12.3" - "@svgr/babel-preset" "^5.5.0" - "@svgr/hast-util-to-babel-ast" "^5.5.0" - svg-parser "^2.0.2" - -"@svgr/plugin-svgo@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz#02da55d85320549324e201c7b2e53bf431fcc246" - integrity sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ== - dependencies: - cosmiconfig "^7.0.0" - deepmerge "^4.2.2" - svgo "^1.2.2" - -"@svgr/webpack@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-5.5.0.tgz#aae858ee579f5fa8ce6c3166ef56c6a1b381b640" - integrity sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g== - dependencies: - "@babel/core" "^7.12.3" - "@babel/plugin-transform-react-constant-elements" "^7.12.1" - "@babel/preset-env" "^7.12.1" - "@babel/preset-react" "^7.12.5" - "@svgr/core" "^5.5.0" - "@svgr/plugin-jsx" "^5.5.0" - "@svgr/plugin-svgo" "^5.5.0" - loader-utils "^2.0.0" - -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - -"@trysound/sax@0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669" - integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow== - -"@tsconfig/docusaurus@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@tsconfig/docusaurus/-/docusaurus-1.0.2.tgz#400ef146d3e7da2f78bf9749c943d123dce5fd97" - integrity sha512-x4rRVb346vjyym6QbeB1Tv01XXwhbkujOmvDmtf0bT20oc2gbDhbmwpskKqZ5Of2Q/Vk4jNk1WMiLsZdJ9t7Dw== - -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - -"@types/eslint-scope@^3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86" - integrity sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "7.2.12" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.12.tgz#fefaa48a4db2415b621fe315e4baeedde525927e" - integrity sha512-HjikV/jX6e0Pg4DcB+rtOBKSrG6w5IaxWpmi3efL/eLxMz5lZTK+W1DKERrX5a+mNzL78axfsDNXu7JHFP4uLg== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^0.0.47": - version "0.0.47" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4" - integrity sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg== - -"@types/events@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" - integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== - -"@types/github-slugger@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@types/github-slugger/-/github-slugger-1.3.0.tgz#16ab393b30d8ae2a111ac748a015ac05a1fc5524" - integrity sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g== - -"@types/glob@^7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" - integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== - dependencies: - "@types/events" "*" - "@types/minimatch" "*" - "@types/node" "*" - -"@types/hast@^2.0.0": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.1.tgz#b16872f2a6144c7025f296fb9636a667ebb79cd9" - integrity sha512-viwwrB+6xGzw+G1eWpF9geV3fnsDgXqHG+cqgiHrvQfDUW5hzhCyV7Sy3UJxhfRFBsgky2SSW33qi/YrIkjX5Q== - dependencies: - "@types/unist" "*" - -"@types/history@*": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/html-minifier-terser@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.0.0.tgz#7532440c138605ced1b555935c3115ddd20e8bef" - integrity sha512-q95SP4FdkmF0CwO0F2q0H6ZgudsApaY/yCtAQNRn1gduef5fGpyEphzy0YCq/N0UFvDSnLg5V8jFK/YGXlDiCw== - -"@types/json-schema@*", "@types/json-schema@^7.0.6": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" - integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== - -"@types/mdast@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" - integrity sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw== - dependencies: - "@types/unist" "*" - -"@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== - -"@types/node@*": - version "13.11.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7" - integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g== - -"@types/node@^14.14.28": - version "14.17.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.1.tgz#5e07e0cb2ff793aa7a1b41deae76221e6166049f" - integrity sha512-/tpUyFD7meeooTRwl3sYlihx2BrJE7q9XF71EguPFIySj9B7qgnRtHsHTho+0AUm4m1SvWGm6uSncrR94q6Vtw== - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/parse5@^5.0.0": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" - integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== - -"@types/prop-types@*": - version "15.7.3" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" - integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== - -"@types/q@^1.5.1": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" - integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== - -"@types/react-helmet@^6.1.1": - version "6.1.1" - resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.1.tgz#4fde22cbcaa1b461642e1d719cc6162d95acb110" - integrity sha512-VmSCMz6jp/06DABoY60vQa++h1YFt0PfAI23llxBJHbowqFgLUL0dhS1AQeVPNqYfRp9LAfokrfWACTNeobOrg== - dependencies: - "@types/react" "*" - -"@types/react-router-dom@^5.1.7": - version "5.1.7" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.7.tgz#a126d9ea76079ffbbdb0d9225073eb5797ab7271" - integrity sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg== - dependencies: - "@types/history" "*" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router@*": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" - integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react@*", "@types/react@^17.0.8": - version "17.0.8" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.8.tgz#fe76e3ba0fbb5602704110fd1e3035cf394778e3" - integrity sha512-3sx4c0PbXujrYAKwXxNONXUtRp9C+hE2di0IuxFyf5BELD+B+AXL8G7QrmSKhVwKZDbv0igiAjQAMhXj8Yg3aw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/sax@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.1.tgz#e0248be936ece791a82db1a57f3fb5f7c87e8172" - integrity sha512-dqYdvN7Sbw8QT/0Ci5rhjE4/iCMJEM0Y9rHpCu+gGXD9Lwbz28t6HI2yegsB6BoV1sShRMU6lAmAcgRjmFy7LA== - dependencies: - "@types/node" "*" - -"@types/scheduler@*": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" - integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== - -"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" - integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== - -"@webassemblyjs/ast@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.0.tgz#a5aa679efdc9e51707a4207139da57920555961f" - integrity sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - -"@webassemblyjs/floating-point-hex-parser@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz#34d62052f453cd43101d72eab4966a022587947c" - integrity sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA== - -"@webassemblyjs/helper-api-error@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz#aaea8fb3b923f4aaa9b512ff541b013ffb68d2d4" - integrity sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w== - -"@webassemblyjs/helper-buffer@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz#d026c25d175e388a7dbda9694e91e743cbe9b642" - integrity sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA== - -"@webassemblyjs/helper-numbers@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz#7ab04172d54e312cc6ea4286d7d9fa27c88cd4f9" - integrity sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.0" - "@webassemblyjs/helper-api-error" "1.11.0" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz#85fdcda4129902fe86f81abf7e7236953ec5a4e1" - integrity sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA== - -"@webassemblyjs/helper-wasm-section@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz#9ce2cc89300262509c801b4af113d1ca25c1a75b" - integrity sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-buffer" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/wasm-gen" "1.11.0" - -"@webassemblyjs/ieee754@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz#46975d583f9828f5d094ac210e219441c4e6f5cf" - integrity sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.0.tgz#f7353de1df38aa201cba9fb88b43f41f75ff403b" - integrity sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.0.tgz#86e48f959cf49e0e5091f069a709b862f5a2cadf" - integrity sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw== - -"@webassemblyjs/wasm-edit@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz#ee4a5c9f677046a210542ae63897094c2027cb78" - integrity sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-buffer" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/helper-wasm-section" "1.11.0" - "@webassemblyjs/wasm-gen" "1.11.0" - "@webassemblyjs/wasm-opt" "1.11.0" - "@webassemblyjs/wasm-parser" "1.11.0" - "@webassemblyjs/wast-printer" "1.11.0" - -"@webassemblyjs/wasm-gen@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz#3cdb35e70082d42a35166988dda64f24ceb97abe" - integrity sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/ieee754" "1.11.0" - "@webassemblyjs/leb128" "1.11.0" - "@webassemblyjs/utf8" "1.11.0" - -"@webassemblyjs/wasm-opt@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz#1638ae188137f4bb031f568a413cd24d32f92978" - integrity sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-buffer" "1.11.0" - "@webassemblyjs/wasm-gen" "1.11.0" - "@webassemblyjs/wasm-parser" "1.11.0" - -"@webassemblyjs/wasm-parser@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz#3e680b8830d5b13d1ec86cc42f38f3d4a7700754" - integrity sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-api-error" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/ieee754" "1.11.0" - "@webassemblyjs/leb128" "1.11.0" - "@webassemblyjs/utf8" "1.11.0" - -"@webassemblyjs/wast-printer@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz#680d1f6a5365d6d401974a8e949e05474e1fab7e" - integrity sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -acorn-walk@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.1.0.tgz#d3c6a9faf00987a5e2b9bdb506c2aa76cd707f83" - integrity sha512-mjmzmv12YIG/G8JQdQuz2MUDShEJ6teYpT5bmWA4q7iwoGen8xtt3twF3OvzIUl+Q06aWIjvnwQUKvQ6TtMRjg== - -acorn@^8.0.4, acorn@^8.2.1: - version "8.2.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.2.4.tgz#caba24b08185c3b56e3168e97d15ed17f4d31fd0" - integrity sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg== - -address@1.1.2, address@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" - integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== - -aggregate-error@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" - integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" - integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== - -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv@^6.1.0, ajv@^6.12.0: - version "6.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" - integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -algoliasearch-helper@^3.3.4: - version "3.4.4" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.4.4.tgz#f2eb46bc4d2f6fed82c7201b8ac4ce0a1988ae67" - integrity sha512-OjyVLjykaYKCMxxRMZNiwLp8CS310E0qAeIY2NaublcmLAh8/SL19+zYHp7XCLtMem2ZXwl3ywMiA32O9jszuw== - dependencies: - events "^1.1.1" - -algoliasearch@^4.0.0, algoliasearch@^4.8.4: - version "4.9.1" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.9.1.tgz#1fa8ece3f9808e465226176b88b953801c2274e0" - integrity sha512-EeJUYXzBEhZSsL6tXc3hseLBCtlNLa1MZ4mlMK6EeX38yRjY5vgnFcNNml6uUhlOjvheKxgkKRpPWkxgL8Cqkg== - dependencies: - "@algolia/cache-browser-local-storage" "4.9.1" - "@algolia/cache-common" "4.9.1" - "@algolia/cache-in-memory" "4.9.1" - "@algolia/client-account" "4.9.1" - "@algolia/client-analytics" "4.9.1" - "@algolia/client-common" "4.9.1" - "@algolia/client-recommendation" "4.9.1" - "@algolia/client-search" "4.9.1" - "@algolia/logger-common" "4.9.1" - "@algolia/logger-console" "4.9.1" - "@algolia/requester-browser-xhr" "4.9.1" - "@algolia/requester-common" "4.9.1" - "@algolia/requester-node-http" "4.9.1" - "@algolia/transporter" "4.9.1" - -alphanum-sort@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" - integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= - -ansi-align@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" - integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== - dependencies: - string-width "^3.0.0" - -ansi-colors@^3.0.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" - integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== - -ansi-escapes@^4.3.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-html@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" - integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== - dependencies: - "@types/color-name" "^1.1.1" - color-convert "^2.0.1" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arg@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.0.tgz#a20e2bb5710e82950a516b3f933fee5ed478be90" - integrity sha512-4P8Zm2H+BRS+c/xX1LrHw0qKpEhdlZjLCgWy+d78T9vqa2Z2SiD2wMrYuWIAFy5IZUD7nnNXroRttz+0RzlrzQ== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-flatten@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - -async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -autoprefixer@^10.2.0, autoprefixer@^10.2.5: - version "10.2.6" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.6.tgz#aadd9ec34e1c98d403e01950038049f0eb252949" - integrity sha512-8lChSmdU6dCNMCQopIf4Pe5kipkAGj/fvTMslCsih0uHpOrXOPUEVOmYMMqmw3cekQkSD7EhIeuYl5y0BLdKqg== - dependencies: - browserslist "^4.16.6" - caniuse-lite "^1.0.30001230" - colorette "^1.2.2" - fraction.js "^4.1.1" - normalize-range "^0.1.2" - postcss-value-parser "^4.1.0" - -axios@^0.21.1: - version "0.21.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" - integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== - dependencies: - follow-redirects "^1.10.0" - -babel-loader@^8.2.2: - version "8.2.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81" - integrity sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g== - dependencies: - find-cache-dir "^3.3.1" - loader-utils "^1.4.0" - make-dir "^3.1.0" - schema-utils "^2.6.5" - -babel-plugin-apply-mdx-type-prop@1.6.22: - version "1.6.22" - resolved "https://registry.yarnpkg.com/babel-plugin-apply-mdx-type-prop/-/babel-plugin-apply-mdx-type-prop-1.6.22.tgz#d216e8fd0de91de3f1478ef3231e05446bc8705b" - integrity sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ== - dependencies: - "@babel/helper-plugin-utils" "7.10.4" - "@mdx-js/util" "1.6.22" - -babel-plugin-dynamic-import-node@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" - integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-extract-import-names@1.6.22: - version "1.6.22" - resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz#de5f9a28eb12f3eb2578bf74472204e66d1a13dc" - integrity sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ== - dependencies: - "@babel/helper-plugin-utils" "7.10.4" - -babel-plugin-polyfill-corejs2@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz#e9124785e6fd94f94b618a7954e5693053bf5327" - integrity sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ== - dependencies: - "@babel/compat-data" "^7.13.11" - "@babel/helper-define-polyfill-provider" "^0.2.2" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.2.tgz#7424a1682ee44baec817327710b1b094e5f8f7f5" - integrity sha512-l1Cf8PKk12eEk5QP/NQ6TH8A1pee6wWDJ96WjxrMXFLHLOBFzYM4moG80HFgduVhTqAFez4alnZKEhP/bYHg0A== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.2.2" - core-js-compat "^3.9.1" - -babel-plugin-polyfill-regenerator@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz#b310c8d642acada348c1fa3b3e6ce0e851bee077" - integrity sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.2.2" - -bail@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" - integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base16@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" - integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA= - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" - integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bluebird@^3.7.1: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= - dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" - dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" - -boolbase@^1.0.0, boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - -boxen@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.0.1.tgz#657528bdd3f59a772b8279b831f27ec2c744664b" - integrity sha512-49VBlw+PrWEF51aCmy7QIteYPIFZxSpvqBdP/2itCPPlJ49kj9zg/XPRFrdkne2W+CfwXUls8exMvu1RysZpKA== - dependencies: - ansi-align "^3.0.0" - camelcase "^6.2.0" - chalk "^4.1.0" - cli-boxes "^2.2.1" - string-width "^4.2.0" - type-fest "^0.20.2" - widest-line "^3.1.0" - wrap-ansi "^7.0.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@^3.0.1, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browserslist@4.14.2: - version "4.14.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.2.tgz#1b3cec458a1ba87588cc5e9be62f19b6d48813ce" - integrity sha512-HI4lPveGKUR0x2StIz+2FXfDk9SfVMrxn6PLh1JeGUwcuoDkdKZebWiyLRJ68iIPDpMI4JLVDf7S7XzslgWOhw== - dependencies: - caniuse-lite "^1.0.30001125" - electron-to-chromium "^1.3.564" - escalade "^3.0.2" - node-releases "^1.1.61" - -browserslist@^4.0.0: - version "4.11.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.1.tgz#92f855ee88d6e050e7e7311d987992014f1a1f1b" - integrity sha512-DCTr3kDrKEYNw6Jb9HFxVLQNaue8z+0ZfRBRjmCunKDEXEBajKDj2Y+Uelg+Pi29OnvaSGwjOsnRyNEkXzHg5g== - dependencies: - caniuse-lite "^1.0.30001038" - electron-to-chromium "^1.3.390" - node-releases "^1.1.53" - pkg-up "^2.0.0" - -browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.6: - version "4.16.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" - integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== - dependencies: - caniuse-lite "^1.0.30001219" - colorette "^1.2.2" - electron-to-chromium "^1.3.723" - escalade "^3.1.1" - node-releases "^1.1.71" - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camel-case@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.1.tgz#1fc41c854f00e2f7d0139dfeba1542d6896fe547" - integrity sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q== - dependencies: - pascal-case "^3.1.1" - tslib "^1.10.0" - -camelcase-css@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" - integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001038: - version "1.0.30001040" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001040.tgz#103fc8e6eb1d7397e95134cd0e996743353d58ea" - integrity sha512-Ep0tEPeI5wCvmJNrXjE3etgfI+lkl1fTDU6Y3ZH1mhrjkPlVI9W4pcKbMo+BQLpEWKVYYp2EmYaRsqpPC3k7lQ== - -caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001230: - version "1.0.30001230" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71" - integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ== - -ccount@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" - integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== - -ccount@^1.0.3: - version "1.0.5" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" - integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== - -chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" - integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -character-entities-legacy@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" - integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== - -character-entities@^1.0.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" - integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== - -character-reference-invalid@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" - integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== - -cheerio@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" - integrity sha1-qbqoYKP5tZWmuBsahocxIe06Jp4= - dependencies: - css-select "~1.2.0" - dom-serializer "~0.1.0" - entities "~1.1.1" - htmlparser2 "^3.9.1" - lodash.assignin "^4.0.9" - lodash.bind "^4.1.4" - lodash.defaults "^4.0.1" - lodash.filter "^4.4.0" - lodash.flatten "^4.2.0" - lodash.foreach "^4.3.0" - lodash.map "^4.4.0" - lodash.merge "^4.4.0" - lodash.pick "^4.2.1" - lodash.reduce "^4.4.0" - lodash.reject "^4.4.0" - lodash.some "^4.4.0" - -chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chokidar@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - -chrome-trace-event@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" - integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== - dependencies: - tslib "^1.9.0" - -ci-info@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -clean-css@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" - integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== - dependencies: - source-map "~0.6.0" - -clean-css@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.1.2.tgz#6ea0da7286b4ddc2469a1b776e2461a5007eed54" - integrity sha512-QcaGg9OuMo+0Ds933yLOY+gHPWbxhxqF0HDexmToPf8pczvmvZGYzd+QqWp9/mkucAOKViI+dSFOqoZIvXbeBw== - dependencies: - source-map "~0.6.0" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-boxes@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" - integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== - -clipboard@^2.0.0: - version "2.0.6" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376" - integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -clsx@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" - integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== - -coa@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" - integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== - dependencies: - "@types/q" "^1.5.1" - chalk "^2.4.1" - q "^1.1.2" - -collapse-white-space@^1.0.2: - version "1.0.6" - resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" - integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colord@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.0.1.tgz#1e7fb1f9fa1cf74f42c58cb9c20320bab8435aa0" - integrity sha512-vm5YpaWamD0Ov6TSG0GGmUIwstrWcfKQV/h2CmbR7PbNu41+qdB5PW9lpzhjedrpm08uuYvcXi0Oel1RLZIJuA== - -colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - -combine-promises@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.1.0.tgz#72db90743c0ca7aab7d0d8d2052fd7b0f674de71" - integrity sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg== - -comma-separated-tokens@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" - integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== - -commander@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== - -commander@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -configstore@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - -connect-history-api-fallback@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" - integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== - -consola@^2.15.0: - version "2.15.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" - integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== - -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -convert-source-map@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -copy-text-to-clipboard@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.0.1.tgz#8cbf8f90e0a47f12e4a24743736265d157bce69c" - integrity sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q== - -copy-webpack-plugin@^8.1.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-8.1.1.tgz#3f697e162764925c2f0d235f380676125508fd26" - integrity sha512-rYM2uzRxrLRpcyPqGceRBDpxxUV8vcDqIKxAUKfcnFpcrPxT5+XvhTxv7XLjo5AvEJFPdAE3zCogG2JVahqgSQ== - dependencies: - fast-glob "^3.2.5" - glob-parent "^5.1.1" - globby "^11.0.3" - normalize-path "^3.0.0" - p-limit "^3.1.0" - schema-utils "^3.0.0" - serialize-javascript "^5.0.1" - -core-js-compat@^3.9.0, core-js-compat@^3.9.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.13.1.tgz#05444caa8f153be0c67db03cf8adb8ec0964e58e" - integrity sha512-mdrcxc0WznfRd8ZicEZh1qVeJ2mu6bwQFh8YVUK48friy/FOwFV5EJj9/dlh+nMQ74YusdVfBFDuomKgUspxWQ== - dependencies: - browserslist "^4.16.6" - semver "7.0.0" - -core-js-pure@^3.0.0: - version "3.13.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.13.1.tgz#5d139d346780f015f67225f45ee2362a6bed6ba1" - integrity sha512-wVlh0IAi2t1iOEh16y4u1TRk6ubd4KvLE8dlMi+3QUI6SfKphQUh7tAwihGGSQ8affxEXpVIPpOdf9kjR4v4Pw== - -core-js@^3.9.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.13.1.tgz#30303fabd53638892062d8b4e802cac7599e9fb7" - integrity sha512-JqveUc4igkqwStL2RTRn/EPFGBOfEZHxJl/8ej1mXJR75V3go2mFF4bmUYkEIT1rveHKnkUlcJX/c+f1TyIovQ== - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - -cross-fetch@^3.0.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" - integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ== - dependencies: - node-fetch "2.6.1" - -cross-spawn@7.0.3, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - -css-color-names@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= - -css-color-names@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67" - integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA== - -css-declaration-sorter@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz#9dfd8ea0df4cc7846827876fafb52314890c21a9" - integrity sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw== - dependencies: - timsort "^0.3.0" - -css-loader@^5.1.1: - version "5.2.6" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.6.tgz#c3c82ab77fea1f360e587d871a6811f4450cc8d1" - integrity sha512-0wyN5vXMQZu6BvjbrPdUJvkCzGEO24HC7IS7nW4llc6BBFC+zwR9CKtYGv63Puzsg10L/o12inMY5/2ByzfD6w== - dependencies: - icss-utils "^5.1.0" - loader-utils "^2.0.0" - postcss "^8.2.15" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" - postcss-value-parser "^4.1.0" - schema-utils "^3.0.0" - semver "^7.3.5" - -css-minimizer-webpack-plugin@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-2.0.0.tgz#3c42f6624ed4cf4780dd963e23ee649e5a25c1a8" - integrity sha512-cG/uc94727tx5pBNtb1Sd7gvUPzwmcQi1lkpfqTpdkuNq75hJCw7bIVsCNijLm4dhDcr1atvuysl2rZqOG8Txw== - dependencies: - cssnano "^5.0.0" - jest-worker "^26.3.0" - p-limit "^3.0.2" - postcss "^8.2.9" - schema-utils "^3.0.0" - serialize-javascript "^5.0.1" - source-map "^0.6.1" - -css-select-base-adapter@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" - integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== - -css-select@^1.1.0, css-select@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= - dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" - -css-select@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" - integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== - dependencies: - boolbase "^1.0.0" - css-what "^3.2.1" - domutils "^1.7.0" - nth-check "^1.0.2" - -css-select@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" - integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== - dependencies: - boolbase "^1.0.0" - css-what "^4.0.0" - domhandler "^4.0.0" - domutils "^2.4.3" - nth-check "^2.0.0" - -css-tree@1.0.0-alpha.37: - version "1.0.0-alpha.37" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" - integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== - dependencies: - mdn-data "2.0.4" - source-map "^0.6.1" - -css-tree@1.0.0-alpha.39: - version "1.0.0-alpha.39" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" - integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== - dependencies: - mdn-data "2.0.6" - source-map "^0.6.1" - -css-tree@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-what@2.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== - -css-what@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" - integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== - -css-what@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" - integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-advanced@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/cssnano-preset-advanced/-/cssnano-preset-advanced-5.1.2.tgz#859bb643e86b32d9fdd5f56ff47f2ec7035da1af" - integrity sha512-Joym8pdrIKqzASYvyTwJ9FpkmEcrYToWKWMGVFSggindrEDOpe+FgNpWhWcv6Z7GDZ4kCC3p7PE/oPSGTc8/kw== - dependencies: - autoprefixer "^10.2.0" - cssnano-preset-default "^5.1.2" - postcss-discard-unused "^5.0.1" - postcss-merge-idents "^5.0.1" - postcss-reduce-idents "^5.0.1" - postcss-zindex "^5.0.1" - -cssnano-preset-default@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.2.tgz#5d4877a91769823c5da6bcebd54996ecdf8aca12" - integrity sha512-spilp8LRw0sacuxiN9A/dyyPr6G/WISKMBKcBD4NMoPV0ENx4DeuWvIIrSx9PII2nJIDCO3kywkqTPreECBVOg== - dependencies: - css-declaration-sorter "^6.0.3" - cssnano-utils "^2.0.1" - postcss-calc "^8.0.0" - postcss-colormin "^5.2.0" - postcss-convert-values "^5.0.1" - postcss-discard-comments "^5.0.1" - postcss-discard-duplicates "^5.0.1" - postcss-discard-empty "^5.0.1" - postcss-discard-overridden "^5.0.1" - postcss-merge-longhand "^5.0.2" - postcss-merge-rules "^5.0.2" - postcss-minify-font-values "^5.0.1" - postcss-minify-gradients "^5.0.1" - postcss-minify-params "^5.0.1" - postcss-minify-selectors "^5.1.0" - postcss-normalize-charset "^5.0.1" - postcss-normalize-display-values "^5.0.1" - postcss-normalize-positions "^5.0.1" - postcss-normalize-repeat-style "^5.0.1" - postcss-normalize-string "^5.0.1" - postcss-normalize-timing-functions "^5.0.1" - postcss-normalize-unicode "^5.0.1" - postcss-normalize-url "^5.0.1" - postcss-normalize-whitespace "^5.0.1" - postcss-ordered-values "^5.0.1" - postcss-reduce-initial "^5.0.1" - postcss-reduce-transforms "^5.0.1" - postcss-svgo "^5.0.2" - postcss-unique-selectors "^5.0.1" - -cssnano-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2" - integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ== - -cssnano@^5.0.0, cssnano@^5.0.1: - version "5.0.5" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.5.tgz#6b8787123bf4cd5a220a2fa6cb5bc036b0854b48" - integrity sha512-L2VtPXnq6rmcMC9vkBOP131sZu3ccRQI27ejKZdmQiPDpUlFkUbpXHgKN+cibeO1U4PItxVZp1zTIn5dHsXoyg== - dependencies: - cosmiconfig "^7.0.0" - cssnano-preset-default "^5.1.2" - is-resolvable "^1.1.0" - -csso@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903" - integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ== - dependencies: - css-tree "1.0.0-alpha.39" - -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -csstype@^3.0.2: - version "3.0.8" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" - integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== - -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.0.0, debug@^3.1.1: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -debug@^3.2.6: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.1.0, debug@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - -deep-equal@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - -default-gateway@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" - integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== - dependencies: - execa "^1.0.0" - ip-regex "^2.1.0" - -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - -define-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -del@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" - integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== - dependencies: - "@types/glob" "^7.1.1" - globby "^6.1.0" - is-path-cwd "^2.0.0" - is-path-in-cwd "^2.0.0" - p-map "^2.0.0" - pify "^4.0.1" - rimraf "^2.6.3" - -del@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" - integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detab@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.4.tgz#b927892069aff405fbb9a186fe97a44a92a94b43" - integrity sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g== - dependencies: - repeat-string "^1.5.4" - -detect-node@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" - integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== - -detect-port-alt@1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" - integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== - dependencies: - address "^1.0.1" - debug "^2.6.0" - -detect-port@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1" - integrity sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ== - dependencies: - address "^1.0.1" - debug "^2.6.0" - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= - -dns-packet@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" - integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== - dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= - dependencies: - buffer-indexof "^1.0.0" - -dom-converter@^0.2: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" - integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== - dependencies: - utila "~0.4" - -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - -dom-serializer@^1.0.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" - integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -dom-serializer@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" - integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== - dependencies: - domelementtype "^1.3.0" - entities "^1.1.1" - -domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" - integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== - -domelementtype@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domhandler@^4.0.0, domhandler@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" - integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== - dependencies: - domelementtype "^2.2.0" - -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^1.5.1, domutils@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^2.4.3: - version "2.6.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7" - integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -dot-case@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.3.tgz#21d3b52efaaba2ea5fda875bb1aa8124521cf4aa" - integrity sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA== - dependencies: - no-case "^3.0.3" - tslib "^1.10.0" - -dot-prop@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== - dependencies: - is-obj "^2.0.0" - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -duplexer@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= - -duplexer@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -electron-to-chromium@^1.3.390: - version "1.3.403" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.403.tgz#c8bab4e2e72bf78bc28bad1cc355c061f9cc1918" - integrity sha512-JaoxV4RzdBAZOnsF4dAlZ2ijJW72MbqO5lNfOBHUWiBQl3Rwe+mk2RCUMrRI3rSClLJ8HSNQNqcry12H+0ZjFw== - -electron-to-chromium@^1.3.564, electron-to-chromium@^1.3.723: - version "1.3.742" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.742.tgz#7223215acbbd3a5284962ebcb6df85d88b95f200" - integrity sha512-ihL14knI9FikJmH2XUIDdZFWJxvr14rPSdOhJ7PpS27xbz8qmaRwCwyg/bmFwjWKmWK9QyamiCZVCvXm5CH//Q== - -"emoji-regex@>=6.0.0 <=6.1.1": - version "6.1.1" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e" - integrity sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4= - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -emoticon@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-3.2.0.tgz#c008ca7d7620fac742fe1bf4af8ff8fed154ae7f" - integrity sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^5.8.0: - version "5.8.2" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b" - integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -entities@^1.1.1, entities@~1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" - integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== - -errno@^0.1.3: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== - dependencies: - prr "~1.0.1" - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: - version "1.17.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" - integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.1.5" - is-regex "^1.0.5" - object-inspect "^1.7.0" - object-keys "^1.1.1" - object.assign "^4.1.0" - string.prototype.trimleft "^2.1.1" - string.prototype.trimright "^2.1.1" - -es-module-lexer@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.4.1.tgz#dda8c6a14d8f340a24e34331e0fab0cb50438e0e" - integrity sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA== - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escalade@^3.0.2, escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-goat@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" - integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== - -escape-html@^1.0.3, escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-scope@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -eta@^1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/eta/-/eta-1.12.1.tgz#d985766591676c323f02dfc48a67539212745937" - integrity sha512-H8npoci2J/7XiPnVcCVulBSPsTNGvGaINyMjQDU8AFqp9LGsEYS88g2CiU+d01Sg44WtX7o4nb8wUJ9vnI+tiA== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -eval@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/eval/-/eval-0.1.4.tgz#e05dbe0dab4b9330215cbb7bf4886eb24bd58700" - integrity sha512-npGsebJejyjMRnLdFu+T/97dnigqIU0Ov3IGrZ8ygd1v7RL1vGkEKtvyWZobqUH1AQgKlg0Yqqe2BtMA9/QZLw== - dependencies: - require-like ">= 0.1.1" - -eventemitter3@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" - integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== - -events@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -eventsource@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== - dependencies: - original "^1.0.0" - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376" - integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -fast-deep-equal@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" - integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== - -fast-glob@^3.1.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" - integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" - merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" - -fast-glob@^3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" - merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-url-parser@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0= - dependencies: - punycode "^1.3.2" - -fastq@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.7.0.tgz#fcd79a08c5bd7ec5b55cd3f5c4720db551929801" - integrity sha512-YOadQRnHd5q6PogvAR/x62BGituF2ufiEA6s8aavQANw5YKHERI4AREboX6KotzP8oX2klxYF2wcV/7bn1clfQ== - dependencies: - reusify "^1.0.4" - -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -fbemitter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-3.0.0.tgz#00b2a1af5411254aab416cd75f9e6289bee4bff3" - integrity sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw== - dependencies: - fbjs "^3.0.0" - -fbjs-css-vars@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" - integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== - -fbjs@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.0.tgz#0907067fb3f57a78f45d95f1eacffcacd623c165" - integrity sha512-dJd4PiDOFuhe7vk4F80Mba83Vr2QuK86FoxtgPmzBqEJahncp+13YCmfoa53KHCo6OnlXLG7eeMWPfB5CrpVKg== - dependencies: - cross-fetch "^3.0.4" - fbjs-css-vars "^1.0.0" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.18" - -feed@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" - integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ== - dependencies: - xml-js "^1.6.11" - -figures@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-loader@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" - integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -filesize@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" - integrity sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -find-cache-dir@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" - integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-up@4.1.0, find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flux@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/flux/-/flux-4.0.1.tgz#7843502b02841d4aaa534af0b373034a1f75ee5c" - integrity sha512-emk4RCvJ8RzNP2lNpphKnG7r18q8elDYNAPx7xn+bDeOIo9FFfxEfIQ2y6YbQNmnsGD3nH1noxtLE64Puz1bRQ== - dependencies: - fbemitter "^3.0.0" - fbjs "^3.0.0" - -follow-redirects@^1.0.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb" - integrity sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA== - dependencies: - debug "^3.0.0" - -follow-redirects@^1.10.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" - integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg== - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -fork-ts-checker-webpack-plugin@4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz#5055c703febcf37fa06405d400c122b905167fc5" - integrity sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw== - dependencies: - "@babel/code-frame" "^7.5.5" - chalk "^2.4.1" - micromatch "^3.1.10" - minimatch "^3.0.4" - semver "^5.6.0" - tapable "^1.0.0" - worker-rpc "^0.1.0" - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fraction.js@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.1.tgz#ac4e520473dae67012d618aab91eda09bcb400ff" - integrity sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg== - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.7: - version "1.2.12" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c" - integrity sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -gensync@^1.0.0-beta.1: - version "1.0.0-beta.1" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" - integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-own-enumerable-property-symbols@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" - integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== - -get-stream@^4.0.0, get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -github-slugger@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9" - integrity sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q== - dependencies: - emoji-regex ">=6.0.0 <=6.1.1" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@^5.1.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob-parent@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@^7.0.0, glob@^7.0.3, glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-dirs@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" - integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== - dependencies: - ini "2.0.0" - -global-modules@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globby@11.0.1, globby@^11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" - integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -globby@^11.0.2, globby@^11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" - integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= - dependencies: - delegate "^3.1.2" - -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -graceful-fs@^4.2.4: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -gray-matter@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.2.tgz#9aa379e3acaf421193fce7d2a28cebd4518ac454" - integrity sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw== - dependencies: - js-yaml "^3.11.0" - kind-of "^6.0.2" - section-matter "^1.0.0" - strip-bom-string "^1.0.0" - -gzip-size@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" - integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== - dependencies: - duplexer "^0.1.1" - pify "^4.0.1" - -gzip-size@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" - integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== - dependencies: - duplexer "^0.1.2" - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.0, has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has-yarn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" - integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hast-to-hyperscript@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" - integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA== - dependencies: - "@types/unist" "^2.0.3" - comma-separated-tokens "^1.0.0" - property-information "^5.3.0" - space-separated-tokens "^1.0.0" - style-to-object "^0.3.0" - unist-util-is "^4.0.0" - web-namespaces "^1.0.0" - -hast-util-from-parse5@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz#3089dc0ee2ccf6ec8bc416919b51a54a589e097c" - integrity sha512-gOc8UB99F6eWVWFtM9jUikjN7QkWxB3nY0df5Z0Zq1/Nkwl5V4hAAsl0tmwlgWl/1shlTF8DnNYLO8X6wRV9pA== - dependencies: - ccount "^1.0.3" - hastscript "^5.0.0" - property-information "^5.0.0" - web-namespaces "^1.1.2" - xtend "^4.0.1" - -hast-util-from-parse5@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz#554e34abdeea25ac76f5bd950a1f0180e0b3bc2a" - integrity sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA== - dependencies: - "@types/parse5" "^5.0.0" - hastscript "^6.0.0" - property-information "^5.0.0" - vfile "^4.0.0" - vfile-location "^3.2.0" - web-namespaces "^1.0.0" - -hast-util-parse-selector@^2.0.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz#60c99d0b519e12ab4ed32e58f150ec3f61ed1974" - integrity sha512-gW3sxfynIvZApL4L07wryYF4+C9VvH3AUi7LAnVXV4MneGEgwOByXvFo18BgmTWnm7oHAe874jKbIB1YhHSIzA== - -hast-util-raw@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-6.0.1.tgz#973b15930b7529a7b66984c98148b46526885977" - integrity sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig== - dependencies: - "@types/hast" "^2.0.0" - hast-util-from-parse5 "^6.0.0" - hast-util-to-parse5 "^6.0.0" - html-void-elements "^1.0.0" - parse5 "^6.0.0" - unist-util-position "^3.0.0" - vfile "^4.0.0" - web-namespaces "^1.0.0" - xtend "^4.0.0" - zwitch "^1.0.0" - -hast-util-to-parse5@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz#1ec44650b631d72952066cea9b1445df699f8479" - integrity sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ== - dependencies: - hast-to-hyperscript "^9.0.0" - property-information "^5.0.0" - web-namespaces "^1.0.0" - xtend "^4.0.0" - zwitch "^1.0.0" - -hastscript@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.1.2.tgz#bde2c2e56d04c62dd24e8c5df288d050a355fb8a" - integrity sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ== - dependencies: - comma-separated-tokens "^1.0.0" - hast-util-parse-selector "^2.0.0" - property-information "^5.0.0" - space-separated-tokens "^1.0.0" - -hastscript@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" - integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w== - dependencies: - "@types/hast" "^2.0.0" - comma-separated-tokens "^1.0.0" - hast-util-parse-selector "^2.0.0" - property-information "^5.0.0" - space-separated-tokens "^1.0.0" - -he@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -hex-color-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" - integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== - -history@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - -hoist-non-react-statics@^3.1.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -hsl-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" - integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= - -hsla-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" - integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= - -html-entities@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" - integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== - -html-minifier-terser@^5.0.1: - version "5.0.5" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.0.5.tgz#8f12f639789f04faa9f5cf2ff9b9f65607f21f8b" - integrity sha512-cBSFFghQh/uHcfSiL42KxxIRMF7A144+3E44xdlctIjxEmkEfCvouxNyFH2wysXk1fCGBPwtcr3hDWlGTfkDew== - dependencies: - camel-case "^4.1.1" - clean-css "^4.2.3" - commander "^4.1.1" - he "^1.2.0" - param-case "^3.0.3" - relateurl "^0.2.7" - terser "^4.6.3" - -html-minifier-terser@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054" - integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg== - dependencies: - camel-case "^4.1.1" - clean-css "^4.2.3" - commander "^4.1.1" - he "^1.2.0" - param-case "^3.0.3" - relateurl "^0.2.7" - terser "^4.6.3" - -html-tags@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" - integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== - -html-void-elements@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" - integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w== - -html-webpack-plugin@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.3.1.tgz#8797327548e3de438e3494e0c6d06f181a7f20d1" - integrity sha512-rZsVvPXUYFyME0cuGkyOHfx9hmkFa4pWfxY/mdY38PsBEaVNsRoA+Id+8z6DBDgyv3zaw6XQszdF8HLwfQvcdQ== - dependencies: - "@types/html-minifier-terser" "^5.0.0" - html-minifier-terser "^5.0.1" - lodash "^4.17.20" - pretty-error "^2.1.1" - tapable "^2.0.0" - -htmlparser2@^3.3.0, htmlparser2@^3.9.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -"http-parser-js@>=0.4.0 <0.4.11": - version "0.4.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" - integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= - -http-parser-js@>=0.5.1: - version "0.5.3" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" - integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== - -http-proxy-middleware@0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" - integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== - dependencies: - http-proxy "^1.17.0" - is-glob "^4.0.0" - lodash "^4.17.11" - micromatch "^3.1.10" - -http-proxy@^1.17.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" - integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== - -immer@8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" - integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== - -import-fresh@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-fresh@^3.2.2, import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= - -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -infima@0.2.0-alpha.23: - version "0.2.0-alpha.23" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.23.tgz#2c17b473784ae8244fd985f126f9c27a49b24523" - integrity sha512-V0RTjB1otjpH3E2asbydx3gz7ovdSJsuV7r9JTdBggqRilnelTJUcXxLawBQQKsjQi5qPcRTjxnlaV8xyyKhhw== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -ini@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - -ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inline-style-parser@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" - integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== - -internal-ip@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" - integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== - dependencies: - default-gateway "^4.2.0" - ipaddr.js "^1.9.0" - -interpret@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" - integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== - -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - -ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -ipaddr.js@1.9.1, ipaddr.js@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-absolute-url@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" - integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-alphabetical@1.0.4, is-alphabetical@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" - integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== - -is-alphanumerical@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" - integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== - dependencies: - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" - -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-buffer@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" - integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== - -is-callable@^1.1.4, is-callable@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" - integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - -is-color-stop@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" - integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= - dependencies: - css-color-names "^0.0.4" - hex-color-regex "^1.1.0" - hsl-regex "^1.0.0" - hsla-regex "^1.0.0" - rgb-regex "^1.0.1" - rgba-regex "^1.0.0" - -is-core-module@^2.2.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" - integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== - dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-decimal@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" - integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-docker@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" - integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-hexadecimal@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" - integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== - -is-installed-globally@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - -is-npm@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" - integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-cwd@^2.0.0, is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-in-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" - integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== - dependencies: - is-path-inside "^2.1.0" - -is-path-inside@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" - integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== - dependencies: - path-is-inside "^1.0.2" - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regex@^1.0.4, is-regex@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" - integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== - dependencies: - has "^1.0.3" - -is-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" - integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= - -is-resolvable@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" - integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== - -is-root@2.1.0, is-root@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" - integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-whitespace-character@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" - integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-word-character@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" - integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -is-wsl@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d" - integrity sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog== - -is-yarn-global@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" - integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -jest-worker@^26.3.0, jest-worker@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" - integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^7.0.0" - -joi@^17.3.0, joi@^17.4.0: - version "17.4.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.0.tgz#b5c2277c8519e016316e49ababd41a1908d9ef20" - integrity sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg== - dependencies: - "@hapi/hoek" "^9.0.0" - "@hapi/topo" "^5.0.0" - "@sideway/address" "^4.1.0" - "@sideway/formula" "^3.0.0" - "@sideway/pinpoint" "^2.0.0" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.11.0, js-yaml@^3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json3@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" - integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== - dependencies: - minimist "^1.2.5" - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - -killable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" - integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -klona@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" - integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== - -latest-version@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= - -loader-runner@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" - integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== - -loader-utils@2.0.0, loader-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" - integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@^1.2.3, loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.assignin@^4.0.9: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" - integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI= - -lodash.bind@^4.1.4: - version "4.2.1" - resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" - integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU= - -lodash.curry@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" - integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA= - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= - -lodash.defaults@^4.0.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - -lodash.filter@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" - integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4= - -lodash.flatten@^4.2.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - -lodash.flow@^3.3.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" - integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o= - -lodash.foreach@^4.3.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" - integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM= - -lodash.map@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" - integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= - -lodash.merge@^4.4.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.pick@^4.2.1: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= - -lodash.reduce@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" - integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= - -lodash.reject@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" - integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU= - -lodash.some@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" - integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= - -lodash.toarray@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" - integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= - -lodash.uniq@4.5.0, lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= - -lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -loglevel@^1.6.8: - version "1.6.8" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" - integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lower-case@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.1.tgz#39eeb36e396115cc05e29422eaea9e692c9408c7" - integrity sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ== - dependencies: - tslib "^1.10.0" - -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -make-dir@^3.0.0, make-dir@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-dir@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" - integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== - dependencies: - semver "^6.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -markdown-escapes@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" - integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== - -mdast-squeeze-paragraphs@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97" - integrity sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ== - dependencies: - unist-util-remove "^2.0.0" - -mdast-util-definitions@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" - integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ== - dependencies: - unist-util-visit "^2.0.0" - -mdast-util-to-hast@10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb" - integrity sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - mdast-util-definitions "^4.0.0" - mdurl "^1.0.0" - unist-builder "^2.0.0" - unist-util-generated "^1.0.0" - unist-util-position "^3.0.0" - unist-util-visit "^2.0.0" - -mdast-util-to-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b" - integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w== - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -mdn-data@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" - integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== - -mdn-data@2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" - integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== - -mdurl@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -memory-fs@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" - integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -microevent.ts@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" - integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== - -micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - -mime-db@1.43.0, "mime-db@>= 1.43.0 < 2": - version "1.43.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" - integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== - -mime-db@1.47.0: - version "1.47.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c" - integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw== - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== - -mime-types@2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== - dependencies: - mime-db "~1.33.0" - -mime-types@^2.1.27: - version "2.1.30" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" - integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg== - dependencies: - mime-db "1.47.0" - -mime-types@~2.1.17, mime-types@~2.1.24: - version "2.1.26" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" - integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== - dependencies: - mime-db "1.43.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.3.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" - integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== - -mime@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" - integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -mini-create-react-context@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e" - integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== - dependencies: - "@babel/runtime" "^7.12.1" - tiny-warning "^1.0.3" - -mini-css-extract-plugin@^1.4.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.0.tgz#b4db2525af2624899ed64a23b0016e0036411893" - integrity sha512-nPFKI7NSy6uONUo9yn2hIfb9vyYvkFu95qki0e21DQ9uaqNKDP15DGpK0KnV6wDroWxPHtExrdEwx/yDQ8nVRw== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - webpack-sources "^1.1.0" - -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimatch@3.0.4, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@^0.5.1, mkdirp@~0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -module-alias@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" - integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== - dependencies: - dns-packet "^1.3.1" - thunky "^1.0.2" - -nan@^2.12.1: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== - -nanoid@^3.1.23: - version "3.1.23" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" - integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -no-case@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.3.tgz#c21b434c1ffe48b39087e86cfb4d2582e9df18f8" - integrity sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw== - dependencies: - lower-case "^2.0.1" - tslib "^1.10.0" - -node-emoji@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" - integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw== - dependencies: - lodash.toarray "^4.4.0" - -node-fetch@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-forge@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" - integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== - -node-releases@^1.1.53: - version "1.1.53" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4" - integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ== - -node-releases@^1.1.61, node-releases@^1.1.71: - version "1.1.72" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" - integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -normalize-url@^4.1.0, normalize-url@^4.5.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -nprogress@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" - integrity sha1-y480xTIT2JVyP8urkH6UIq28r7E= - -nth-check@^1.0.2, nth-check@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - -nth-check@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" - integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== - dependencies: - boolbase "^1.0.0" - -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-inspect@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" - integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== - -object-is@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" - integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== - -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.getownpropertydescriptors@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" - integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -object.values@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" - integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/open/-/open-7.0.3.tgz#db551a1af9c7ab4c7af664139930826138531c48" - integrity sha512-sP2ru2v0P290WFfv49Ap8MF6PkzGNnGlAwHweB4WR4mr5d2d0woiCluUeJ218w7/+PmoBy9JmYgD5A4mLcWOFA== - dependencies: - is-docker "^2.0.0" - is-wsl "^2.1.1" - -opener@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" - integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== - -opn@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" - integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== - dependencies: - is-wsl "^1.1.0" - -original@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== - dependencies: - url-parse "^1.4.3" - -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2, p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-retry@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" - integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== - dependencies: - retry "^0.12.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - -param-case@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.3.tgz#4be41f8399eff621c56eebb829a5e451d9801238" - integrity sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA== - dependencies: - dot-case "^3.0.3" - tslib "^1.10.0" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-entities@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" - integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== - dependencies: - character-entities "^1.0.0" - character-entities-legacy "^1.0.0" - character-reference-invalid "^1.0.0" - is-alphanumerical "^1.0.0" - is-decimal "^1.0.0" - is-hexadecimal "^1.0.0" - -parse-json@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" - integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - lines-and-columns "^1.1.6" - -parse-numeric-range@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.2.0.tgz#aa70b00f29624ed13e9f943e9461b306e386b0fa" - integrity sha512-1q2tXpAOplPxcl8vrIGPWz1dJxxfmdRkCFcpxxMBerDnGuuHalOWF/xj9L8Nn5XoTUoB/6F0CeQBp2fMgkOYFg== - -parse5@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" - integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== - -parse5@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascal-case@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.1.tgz#5ac1975133ed619281e88920973d2cd1f279de5f" - integrity sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA== - dependencies: - no-case "^3.0.3" - tslib "^1.10.0" - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@1.0.2, path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path-to-regexp@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" - integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== - -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -pkg-up@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== - dependencies: - find-up "^3.0.0" - -pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" - integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= - dependencies: - find-up "^2.1.0" - -portfinder@^1.0.26: - version "1.0.26" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" - integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.1" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss-calc@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a" - integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g== - dependencies: - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.0.2" - -postcss-colormin@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.0.tgz#2b620b88c0ff19683f3349f4cf9e24ebdafb2c88" - integrity sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw== - dependencies: - browserslist "^4.16.6" - caniuse-api "^3.0.0" - colord "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-convert-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz#4ec19d6016534e30e3102fdf414e753398645232" - integrity sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-discard-comments@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe" - integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg== - -postcss-discard-duplicates@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d" - integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA== - -postcss-discard-empty@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8" - integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw== - -postcss-discard-overridden@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6" - integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q== - -postcss-discard-unused@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-5.0.1.tgz#63e35a74a154912f93d4e75a1e6ff3cc146f934b" - integrity sha512-tD6xR/xyZTwfhKYRw0ylfCY8wbfhrjpKAMnDKRTLMy2fNW5hl0hoV6ap5vo2JdCkuHkP3CHw72beO4Y8pzFdww== - dependencies: - postcss-selector-parser "^6.0.5" - -postcss-loader@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-5.3.0.tgz#1657f869e48d4fdb018a40771c235e499ee26244" - integrity sha512-/+Z1RAmssdiSLgIZwnJHwBMnlABPgF7giYzTN2NOfr9D21IJZ4mQC1R2miwp80zno9M4zMD/umGI8cR+2EL5zw== - dependencies: - cosmiconfig "^7.0.0" - klona "^2.0.4" - semver "^7.3.4" - -postcss-merge-idents@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-5.0.1.tgz#6b5856fc28f2571f28ecce49effb9b0e64be9437" - integrity sha512-xu8ueVU0RszbI2gKkxR6mluupsOSSLvt8q4gA2fcKFkA+x6SlH3cb4cFHpDvcRCNFbUmCR/VUub+Y6zPOjPx+Q== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-merge-longhand@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz#277ada51d9a7958e8ef8cf263103c9384b322a41" - integrity sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw== - dependencies: - css-color-names "^1.0.1" - postcss-value-parser "^4.1.0" - stylehacks "^5.0.1" - -postcss-merge-rules@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a" - integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg== - dependencies: - browserslist "^4.16.6" - caniuse-api "^3.0.0" - cssnano-utils "^2.0.1" - postcss-selector-parser "^6.0.5" - vendors "^1.0.3" - -postcss-minify-font-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf" - integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-minify-gradients@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.1.tgz#2dc79fd1a1afcb72a9e727bc549ce860f93565d2" - integrity sha512-odOwBFAIn2wIv+XYRpoN2hUV3pPQlgbJ10XeXPq8UY2N+9ZG42xu45lTn/g9zZ+d70NKSQD6EOi6UiCMu3FN7g== - dependencies: - cssnano-utils "^2.0.1" - is-color-stop "^1.1.0" - postcss-value-parser "^4.1.0" - -postcss-minify-params@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c" - integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw== - dependencies: - alphanum-sort "^1.0.2" - browserslist "^4.16.0" - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - uniqs "^2.0.0" - -postcss-minify-selectors@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54" - integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og== - dependencies: - alphanum-sort "^1.0.2" - postcss-selector-parser "^6.0.5" - -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== - -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-normalize-charset@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0" - integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg== - -postcss-normalize-display-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd" - integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-normalize-positions@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5" - integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-normalize-repeat-style@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5" - integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-normalize-string@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0" - integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-normalize-timing-functions@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c" - integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-normalize-unicode@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37" - integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA== - dependencies: - browserslist "^4.16.0" - postcss-value-parser "^4.1.0" - -postcss-normalize-url@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.1.tgz#ffa9fe545935d8b57becbbb7934dd5e245513183" - integrity sha512-hkbG0j58Z1M830/CJ73VsP7gvlG1yF+4y7Fd1w4tD2c7CaA2Psll+pQ6eQhth9y9EaqZSLzamff/D0MZBMbYSg== - dependencies: - is-absolute-url "^3.0.3" - normalize-url "^4.5.0" - postcss-value-parser "^4.1.0" - -postcss-normalize-whitespace@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a" - integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-ordered-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.1.tgz#79ef6e2bd267ccad3fc0c4f4a586dfd01c131f64" - integrity sha512-6mkCF5BQ25HvEcDfrMHCLLFHlraBSlOXFnQMHYhSpDO/5jSR1k8LdEXOkv+7+uzW6o6tBYea1Km0wQSRkPJkwA== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-reduce-idents@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-5.0.1.tgz#99b49ce8ee6f9c179447671cc9693e198e877bb7" - integrity sha512-6Rw8iIVFbqtaZExgWK1rpVgP7DPFRPh0DDFZxJ/ADNqPiH10sPCoq5tgo6kLiTyfh9sxjKYjXdc8udLEcPOezg== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-reduce-initial@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946" - integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw== - dependencies: - browserslist "^4.16.0" - caniuse-api "^3.0.0" - -postcss-reduce-transforms@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640" - integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-selector-parser@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" - integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5: - version "6.0.6" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" - integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-sort-media-queries@^3.8.9: - version "3.10.11" - resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-3.10.11.tgz#9e06c220c752c69d3ea4a6d55ac7d4960e201c4a" - integrity sha512-78Ak5YSnalr+UTdZa2OCSNAxvEnHg3GRqWccStljJW7MqeU0cJtMA5OzaMmn+upM+iI5vykWzibVEAYaaAlSzw== - dependencies: - sort-css-media-queries "1.5.4" - -postcss-svgo@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.2.tgz#bc73c4ea4c5a80fbd4b45e29042c34ceffb9257f" - integrity sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A== - dependencies: - postcss-value-parser "^4.1.0" - svgo "^2.3.0" - -postcss-unique-selectors@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc" - integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w== - dependencies: - alphanum-sort "^1.0.2" - postcss-selector-parser "^6.0.5" - uniqs "^2.0.0" - -postcss-value-parser@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d" - integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg== - -postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss-zindex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-5.0.1.tgz#c585724beb69d356af8c7e68847b28d6298ece03" - integrity sha512-nwgtJJys+XmmSGoYCcgkf/VczP8Mp/0OfSv3v0+fw0uABY4yxw+eFs0Xp9nAZHIKnS5j+e9ywQ+RD+ONyvl5pA== - -postcss@^8.2.10, postcss@^8.2.15, postcss@^8.2.4, postcss@^8.2.9: - version "8.3.0" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f" - integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ== - dependencies: - colorette "^1.2.2" - nanoid "^3.1.23" - source-map-js "^0.6.2" - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -prettier@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" - integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== - -pretty-error@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" - integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= - dependencies: - renderkid "^2.0.1" - utila "~0.4" - -pretty-time@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" - integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== - -prism-react-renderer@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.2.1.tgz#392460acf63540960e5e3caa699d851264e99b89" - integrity sha512-w23ch4f75V1Tnz8DajsYKvY5lF7H1+WvzvLUcF0paFxkTHSp42RS0H5CttdN2Q8RR3DRGZ9v5xD/h3n8C8kGmg== - -prismjs@^1.19.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03" - integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ== - optionalDependencies: - clipboard "^2.0.0" - -prismjs@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" - integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA== - optionalDependencies: - clipboard "^2.0.0" - -private@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== - dependencies: - asap "~2.0.3" - -prompts@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" - integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -prompts@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" - integrity sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.7.2: - version "15.7.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.8.1" - -property-information@^5.0.0, property-information@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.4.0.tgz#16e08f13f4e5c4a7be2e4ec431c01c4f8dba869a" - integrity sha512-nmMWAm/3vKFGmmOWOcdLjgq/Hlxa+hsuR/px1Lp/UGEyc5A22A6l78Shc2C0E71sPmAqglni+HrS7L7VJ7AUCA== - dependencies: - xtend "^4.0.0" - -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.1" - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.3.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -pupa@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" - integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== - dependencies: - escape-goat "^2.0.0" - -pure-color@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" - integrity sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4= - -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -querystringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" - integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -react-base16-styling@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c" - integrity sha1-7yFW1mz0E5aVyKFniGy2nqZgeSw= - dependencies: - base16 "^1.0.0" - lodash.curry "^4.0.1" - lodash.flow "^3.3.0" - pure-color "^1.2.0" - -react-dev-utils@^11.0.1: - version "11.0.4" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.4.tgz#a7ccb60257a1ca2e0efe7a83e38e6700d17aa37a" - integrity sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A== - dependencies: - "@babel/code-frame" "7.10.4" - address "1.1.2" - browserslist "4.14.2" - chalk "2.4.2" - cross-spawn "7.0.3" - detect-port-alt "1.1.6" - escape-string-regexp "2.0.0" - filesize "6.1.0" - find-up "4.1.0" - fork-ts-checker-webpack-plugin "4.1.6" - global-modules "2.0.0" - globby "11.0.1" - gzip-size "5.1.1" - immer "8.0.1" - is-root "2.1.0" - loader-utils "2.0.0" - open "^7.0.2" - pkg-up "3.1.0" - prompts "2.4.0" - react-error-overlay "^6.0.9" - recursive-readdir "2.2.2" - shell-quote "1.7.2" - strip-ansi "6.0.0" - text-table "0.2.0" - -react-dom@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" - -react-error-overlay@^6.0.9: - version "6.0.9" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" - integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== - -react-fast-compare@^3.1.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" - integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== - -react-helmet@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" - integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== - dependencies: - object-assign "^4.1.1" - prop-types "^15.7.2" - react-fast-compare "^3.1.1" - react-side-effect "^2.1.0" - -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-json-view@^1.21.1: - version "1.21.3" - resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.21.3.tgz#f184209ee8f1bf374fb0c41b0813cff54549c475" - integrity sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw== - dependencies: - flux "^4.0.1" - react-base16-styling "^0.6.0" - react-lifecycles-compat "^3.0.4" - react-textarea-autosize "^8.3.2" - -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-loadable-ssr-addon-v5-slorber@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" - integrity sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A== - dependencies: - "@babel/runtime" "^7.10.3" - -react-loadable@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/react-loadable/-/react-loadable-5.5.0.tgz#582251679d3da86c32aae2c8e689c59f1196d8c4" - integrity sha512-C8Aui0ZpMd4KokxRdVAm2bQtI03k2RMRNzOB+IipV3yxFTSVICv7WoUr5L9ALB5BmKO1iHgZtWM8EvYG83otdg== - dependencies: - prop-types "^15.5.0" - -react-router-config@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" - integrity sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg== - dependencies: - "@babel/runtime" "^7.1.2" - -react-router-dom@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" - integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== - dependencies: - "@babel/runtime" "^7.1.2" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.2.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-router@5.2.0, react-router@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" - integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== - dependencies: - "@babel/runtime" "^7.1.2" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - mini-create-react-context "^0.4.0" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-side-effect@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.0.tgz#1ce4a8b4445168c487ed24dab886421f74d380d3" - integrity sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg== - -react-textarea-autosize@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.2.tgz#4f9374d357b0a6f6469956726722549124a1b2db" - integrity sha512-JrMWVgQSaExQByP3ggI1eA8zF4mF0+ddVuX7acUeK2V7bmrpjVOY72vmLz2IXFJSAXoY3D80nEzrn0GWajWK3Q== - dependencies: - "@babel/runtime" "^7.10.2" - use-composed-ref "^1.0.0" - use-latest "^1.0.0" - -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -readable-stream@^2.0.1, readable-stream@^2.0.2: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6, readable-stream@^3.1.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - -reading-time@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.3.0.tgz#d13e74431589a4a9038669f24d5acbc08bbb015d" - integrity sha512-RJ8J5O6UvrclfZpcPSPuKusrdRfoY7uXXoYOOdeswZNtSkQaewT3919yz6RyloDBR+iwcUyz5zGOUjhgvfuv3g== - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= - dependencies: - resolve "^1.1.6" - -recursive-readdir@2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" - integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== - dependencies: - minimatch "3.0.4" - -regenerate-unicode-properties@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" - integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== - dependencies: - regenerate "^1.4.0" - -regenerate@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" - integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== - -regenerator-runtime@^0.13.4: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== - -regenerator-transform@^0.14.2: - version "0.14.4" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7" - integrity sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw== - dependencies: - "@babel/runtime" "^7.8.4" - private "^0.1.8" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexp.prototype.flags@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -regexpu-core@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" - integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== - dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" - -regexpu-core@^4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" - integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== - dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" - -registry-auth-token@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250" - integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw== - dependencies: - rc "^1.2.8" - -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== - dependencies: - rc "^1.2.8" - -regjsgen@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" - integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== - -regjsparser@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" - integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== - dependencies: - jsesc "~0.5.0" - -rehype-parse@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-6.0.2.tgz#aeb3fdd68085f9f796f1d3137ae2b85a98406964" - integrity sha512-0S3CpvpTAgGmnz8kiCyFLGuW5yA4OQhyNTm/nwPopZ7+PI11WnGl1TTWTGv/2hPEe/g2jRLlhVVSsoDH8waRug== - dependencies: - hast-util-from-parse5 "^5.0.0" - parse5 "^5.0.0" - xtend "^4.0.0" - -relateurl@^0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= - -remark-admonitions@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/remark-admonitions/-/remark-admonitions-1.2.1.tgz#87caa1a442aa7b4c0cafa04798ed58a342307870" - integrity sha512-Ji6p68VDvD+H1oS95Fdx9Ar5WA2wcDA4kwrrhVU7fGctC6+d3uiMICu7w7/2Xld+lnU7/gi+432+rRbup5S8ow== - dependencies: - rehype-parse "^6.0.2" - unified "^8.4.2" - unist-util-visit "^2.0.1" - -remark-emoji@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-2.1.0.tgz#69165d1181b98a54ad5d9ef811003d53d7ebc7db" - integrity sha512-lDddGsxXURV01WS9WAiS9rO/cedO1pvr9tahtLhr6qCGFhHG4yZSJW3Ha4Nw9Uk1hLNmUBtPC0+m45Ms+xEitg== - dependencies: - emoticon "^3.2.0" - node-emoji "^1.10.0" - unist-util-visit "^2.0.2" - -remark-footnotes@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/remark-footnotes/-/remark-footnotes-2.0.0.tgz#9001c4c2ffebba55695d2dd80ffb8b82f7e6303f" - integrity sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ== - -remark-mdx@1.6.22: - version "1.6.22" - resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-1.6.22.tgz#06a8dab07dcfdd57f3373af7f86bd0e992108bbd" - integrity sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ== - dependencies: - "@babel/core" "7.12.9" - "@babel/helper-plugin-utils" "7.10.4" - "@babel/plugin-proposal-object-rest-spread" "7.12.1" - "@babel/plugin-syntax-jsx" "7.12.1" - "@mdx-js/util" "1.6.22" - is-alphabetical "1.0.4" - remark-parse "8.0.3" - unified "9.2.0" - -remark-parse@8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1" - integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q== - dependencies: - ccount "^1.0.0" - collapse-white-space "^1.0.2" - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" - is-whitespace-character "^1.0.0" - is-word-character "^1.0.0" - markdown-escapes "^1.0.0" - parse-entities "^2.0.0" - repeat-string "^1.5.4" - state-toggle "^1.0.0" - trim "0.0.1" - trim-trailing-lines "^1.0.0" - unherit "^1.0.4" - unist-util-remove-position "^2.0.0" - vfile-location "^3.0.0" - xtend "^4.0.1" - -remark-squeeze-paragraphs@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz#76eb0e085295131c84748c8e43810159c5653ead" - integrity sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw== - dependencies: - mdast-squeeze-paragraphs "^4.0.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -renderkid@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" - integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== - dependencies: - css-select "^1.1.0" - dom-converter "^0.2" - htmlparser2 "^3.3.0" - strip-ansi "^3.0.0" - utila "^0.4.0" - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.5.4, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -replace-ext@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" - integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -"require-like@>= 0.1.1": - version "0.1.2" - resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" - integrity sha1-rW8wwTvs15cBDEaK+ndcDAprR/o= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.1.6, resolve@^1.3.2: - version "1.15.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" - integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== - dependencies: - path-parse "^1.0.6" - -resolve@^1.14.2: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= - -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rtl-detect@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.3.tgz#42145b9a4f9cf0b94c4542aba90d57f0d18559bf" - integrity sha512-2sMcZO60tL9YDEFe24gqddg3hJ+xSmJFN8IExcQUxeHxQzydQrN6GHPL+yAWgzItXSI7es53hcZC9pJneuZDKA== - -rtlcss@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-3.2.0.tgz#2139734ac45302891182f23f13448c51c07fccaf" - integrity sha512-nV3UmaTmA5TkP2dYOR16ULu6FkMOqZRbiXbFZnmWIN9coPfx3gin31VGOPV7vrVMPjNds7pCS2UYy0mwQUdFCQ== - dependencies: - chalk "^4.1.0" - find-up "^5.0.0" - mkdirp "^1.0.4" - postcss "^8.2.4" - strip-json-comments "^3.1.1" - -run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== - -rxjs@^6.6.3: - version "6.6.7" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== - dependencies: - tslib "^1.9.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sax@^1.2.4, sax@~1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -schema-utils@^2.6.5: - version "2.6.5" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.5.tgz#c758f0a7e624263073d396e29cd40aa101152d8a" - integrity sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ== - dependencies: - ajv "^6.12.0" - ajv-keywords "^3.4.1" - -schema-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" - integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== - dependencies: - "@types/json-schema" "^7.0.6" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -section-matter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" - integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== - dependencies: - extend-shallow "^2.0.1" - kind-of "^6.0.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= - -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= - -selfsigned@^1.10.8: - version "1.10.11" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.11.tgz#24929cd906fe0f44b6d01fb23999a739537acbe9" - integrity sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA== - dependencies: - node-forge "^0.10.0" - -semver-diff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" - integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== - dependencies: - semver "^6.3.0" - -semver@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serialize-javascript@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" - integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== - dependencies: - randombytes "^2.1.0" - -serve-handler@^6.1.3: - version "6.1.3" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8" - integrity sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w== - dependencies: - bytes "3.0.0" - content-disposition "0.5.2" - fast-url-parser "1.1.3" - mime-types "2.1.18" - minimatch "3.0.4" - path-is-inside "1.0.2" - path-to-regexp "2.2.1" - range-parser "1.2.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== - -shelljs@^0.8.4: - version "0.8.4" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" - integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -sirv@^1.0.7: - version "1.0.12" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.12.tgz#d816c882b35489b3c63290e2f455ae3eccd5f652" - integrity sha512-+jQoCxndz7L2tqQL4ZyzfDhky0W/4ZJip3XoOuxyQWnAwMxindLl3Xv1qT4x1YX/re0leShvTm8Uk0kQspGhBg== - dependencies: - "@polka/url" "^1.0.0-next.15" - mime "^2.3.1" - totalist "^1.0.0" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -sitemap@^6.3.6: - version "6.4.0" - resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-6.4.0.tgz#b4bc4edf36de742405a7572bc3e467ba484b852e" - integrity sha512-DoPKNc2/apQZTUnfiOONWctwq7s6dZVspxAZe2VPMNtoqNq7HgXRvlRnbIpKjf+8+piQdWncwcy+YhhTGY5USQ== - dependencies: - "@types/node" "^14.14.28" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -sockjs-client@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.1.tgz#256908f6d5adfb94dabbdbd02c66362cca0f9ea6" - integrity sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ== - dependencies: - debug "^3.2.6" - eventsource "^1.0.7" - faye-websocket "^0.11.3" - inherits "^2.0.4" - json3 "^3.3.3" - url-parse "^1.5.1" - -sockjs@^0.3.21: - version "0.3.21" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417" - integrity sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw== - dependencies: - faye-websocket "^0.11.3" - uuid "^3.4.0" - websocket-driver "^0.7.4" - -sort-css-media-queries@1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-1.5.4.tgz#24182b12002a13d01ba943ddf74f5098d7c244ce" - integrity sha512-YP5W/h4Sid/YP7Lp87ejJ5jP13/Mtqt2vx33XyhO+IAugKlufRPbOrPlIiEUuxmpNBSBd3EeeQpFhdu3RfI2Ag== - -source-list-map@^2.0.0, source-list-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-js@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" - integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@~0.5.12: - version "0.5.16" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@~0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.0, source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@~0.7.2: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -space-separated-tokens@^1.0.0: - version "1.1.5" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" - integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -state-toggle@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" - integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -std-env@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-2.2.1.tgz#2ffa0fdc9e2263e0004c1211966e960948a40f6b" - integrity sha512-IjYQUinA3lg5re/YMlwlfhqNRTzMZMqE+pezevdcTaHceqx8ngEi1alX9nNCk9Sc81fy1fLDeQoaCzeiW1yBOQ== - dependencies: - ci-info "^1.6.0" - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.0.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string-width@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string.prototype.trimend@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trimleft@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc" - integrity sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - string.prototype.trimstart "^1.0.0" - -string.prototype.trimright@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz#c76f1cef30f21bbad8afeb8db1511496cfb0f2a3" - integrity sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - string.prototype.trimend "^1.0.0" - -string.prototype.trimstart@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stringify-object@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" - integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== - dependencies: - get-own-enumerable-property-symbols "^3.0.0" - is-obj "^1.0.1" - is-regexp "^1.0.0" - -strip-ansi@6.0.0, strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-bom-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" - integrity sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI= - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -style-to-object@0.3.0, style-to-object@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" - integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== - dependencies: - inline-style-parser "0.1.1" - -stylehacks@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb" - integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA== - dependencies: - browserslist "^4.16.0" - postcss-selector-parser "^6.0.4" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.0.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - -svg-parser@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" - integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== - -svgo@^1.2.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" - integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== - dependencies: - chalk "^2.4.1" - coa "^2.0.2" - css-select "^2.0.0" - css-select-base-adapter "^0.1.1" - css-tree "1.0.0-alpha.37" - csso "^4.0.2" - js-yaml "^3.13.1" - mkdirp "~0.5.1" - object.values "^1.1.0" - sax "~1.2.4" - stable "^0.1.8" - unquote "~1.1.1" - util.promisify "~1.0.0" - -svgo@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.3.0.tgz#6b3af81d0cbd1e19c83f5f63cec2cb98c70b5373" - integrity sha512-fz4IKjNO6HDPgIQxu4IxwtubtbSfGEAJUq/IXyTPIkGhWck/faiiwfkvsB8LnBkKLvSoyNNIY6d13lZprJMc9Q== - dependencies: - "@trysound/sax" "0.1.1" - chalk "^4.1.0" - commander "^7.1.0" - css-select "^3.1.2" - css-tree "^1.1.2" - csso "^4.2.0" - stable "^0.1.8" - -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" - integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== - -terser-webpack-plugin@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.2.tgz#51d295eb7cc56785a67a372575fdc46e42d5c20c" - integrity sha512-6QhDaAiVHIQr5Ab3XUWZyDmrIPCHMiqJVljMF91YKyqwKkL5QHnYMkrMBy96v9Z7ev1hGhSEw1HQZc2p/s5Z8Q== - dependencies: - jest-worker "^26.6.2" - p-limit "^3.1.0" - schema-utils "^3.0.0" - serialize-javascript "^5.0.1" - source-map "^0.6.1" - terser "^5.7.0" - -terser@^4.6.3: - version "4.6.11" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.11.tgz#12ff99fdd62a26de2a82f508515407eb6ccd8a9f" - integrity sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -terser@^5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.0.tgz#a761eeec206bc87b605ab13029876ead938ae693" - integrity sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g== - dependencies: - commander "^2.20.0" - source-map "~0.7.2" - source-map-support "~0.5.19" - -text-table@0.2.0, text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -timsort@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" - integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= - -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - -tiny-invariant@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" - integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== - -tiny-warning@^1.0.0, tiny-warning@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -totalist@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" - integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== - -trim-trailing-lines@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz#7f0739881ff76657b7776e10874128004b625a94" - integrity sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA== - -trim@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" - integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= - -trough@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" - integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== - -ts-essentials@^2.0.3: - version "2.0.12" - resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745" - integrity sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w== - -tslib@^1.10.0, tslib@^1.9.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" - integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== - -tslib@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" - integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typescript@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" - integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== - -ua-parser-js@^0.7.18: - version "0.7.28" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" - integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== - -unherit@^1.0.4: - version "1.1.3" - resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" - integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ== - dependencies: - inherits "^2.0.0" - xtend "^4.0.0" - -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== - -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== - dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" - -unicode-match-property-value-ecmascript@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" - integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== - -unicode-property-aliases-ecmascript@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" - integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== - -unified@9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8" - integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg== - dependencies: - bail "^1.0.0" - extend "^3.0.0" - is-buffer "^2.0.0" - is-plain-obj "^2.0.0" - trough "^1.0.0" - vfile "^4.0.0" - -unified@^8.4.2: - version "8.4.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-8.4.2.tgz#13ad58b4a437faa2751a4a4c6a16f680c500fff1" - integrity sha512-JCrmN13jI4+h9UAyKEoGcDZV+i1E7BLFuG7OsaDvTXI5P0qhHX+vZO/kOhz9jn8HGENDKbwSeB0nVOg4gVStGA== - dependencies: - bail "^1.0.0" - extend "^3.0.0" - is-plain-obj "^2.0.0" - trough "^1.0.0" - vfile "^4.0.0" - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= - -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - -unist-builder@2.0.3, unist-builder@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" - integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== - -unist-util-generated@^1.0.0: - version "1.1.5" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.5.tgz#1e903e68467931ebfaea386dae9ea253628acd42" - integrity sha512-1TC+NxQa4N9pNdayCYA1EGUOCAO0Le3fVp7Jzns6lnua/mYgwHo0tz5WUAfrdpNch1RZLHc61VZ1SDgrtNXLSw== - -unist-util-is@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.2.tgz#c7d1341188aa9ce5b3cff538958de9895f14a5de" - integrity sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ== - -unist-util-position@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" - integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== - -unist-util-remove-position@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc" - integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA== - dependencies: - unist-util-visit "^2.0.0" - -unist-util-remove@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unist-util-remove/-/unist-util-remove-2.1.0.tgz#b0b4738aa7ee445c402fda9328d604a02d010588" - integrity sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q== - dependencies: - unist-util-is "^4.0.0" - -unist-util-stringify-position@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" - integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== - dependencies: - "@types/unist" "^2.0.2" - -unist-util-visit-parents@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.0.2.tgz#d4076af3011739c71d2ce99d05de37d545f4351d" - integrity sha512-yJEfuZtzFpQmg1OSCyS9M5NJRrln/9FbYosH3iW0MG402QbdbaB8ZESwUv9RO6nRfLAKvWcMxCwdLWOov36x/g== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^4.0.0" - -unist-util-visit@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" - integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^4.0.0" - unist-util-visit-parents "^3.0.0" - -unist-util-visit@^2.0.0, unist-util-visit@^2.0.1, unist-util-visit@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.2.tgz#3843782a517de3d2357b4c193b24af2d9366afb7" - integrity sha512-HoHNhGnKj6y+Sq+7ASo2zpVdfdRifhTgX2KTU3B/sO/TTlZchp7E3S4vjRzDJ7L60KmrCPsQkVK3lEF3cz36XQ== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^4.0.0" - unist-util-visit-parents "^3.0.0" - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unquote@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" - integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -update-notifier@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" - integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== - dependencies: - boxen "^5.0.0" - chalk "^4.1.0" - configstore "^5.0.1" - has-yarn "^2.1.0" - import-lazy "^2.1.0" - is-ci "^2.0.0" - is-installed-globally "^0.4.0" - is-npm "^5.0.0" - is-yarn-global "^0.3.0" - latest-version "^5.1.0" - pupa "^2.1.1" - semver "^7.3.4" - semver-diff "^3.1.1" - xdg-basedir "^4.0.0" - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-loader@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" - integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== - dependencies: - loader-utils "^2.0.0" - mime-types "^2.1.27" - schema-utils "^3.0.0" - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -url-parse@^1.4.3: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url-parse@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" - integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use-composed-ref@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.1.0.tgz#9220e4e94a97b7b02d7d27eaeab0b37034438bbc" - integrity sha512-my1lNHGWsSDAhhVAT4MKs6IjBUtG6ZG11uUqexPH9PptiIZDQOzaF4f5tEbJ2+7qvNbtXNBbU3SfmN+fXlWDhg== - dependencies: - ts-essentials "^2.0.3" - -use-isomorphic-layout-effect@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz#7bb6589170cd2987a152042f9084f9effb75c225" - integrity sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ== - -use-latest@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.0.tgz#a44f6572b8288e0972ec411bdd0840ada366f232" - integrity sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw== - dependencies: - use-isomorphic-layout-effect "^1.0.0" - -use-media@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/use-media/-/use-media-1.4.0.tgz#e777bf1f382a7aacabbd1f9ce3da2b62e58b2a98" - integrity sha512-XsgyUAf3nhzZmEfhc5MqLHwyaPjs78bgytpVJ/xDl0TF4Bptf3vEpBNBBT/EIKOmsOc8UbuECq3mrP3mt1QANA== - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util.promisify@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" - integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.2" - has-symbols "^1.0.1" - object.getownpropertydescriptors "^2.1.0" - -utila@^0.4.0, utila@~0.4: - version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" - integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= - -utility-types@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" - integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^3.3.2, uuid@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -vendors@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" - integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== - -vfile-location@^3.0.0, vfile-location@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.2.0.tgz#d8e41fbcbd406063669ebf6c33d56ae8721d0f3c" - integrity sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA== - -vfile-message@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" - integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^2.0.0" - -vfile@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.1.0.tgz#d79248957f43225d57ff67a56effc67bef08946e" - integrity sha512-BaTPalregj++64xbGK6uIlsurN3BCRNM/P2Pg8HezlGzKd1O9PrwIac6bd9Pdx2uTb0QHoioZ+rXKolbVXEgJg== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - replace-ext "1.0.0" - unist-util-stringify-position "^2.0.0" - vfile-message "^2.0.0" - -wait-on@^5.2.1: - version "5.3.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-5.3.0.tgz#584e17d4b3fe7b46ac2b9f8e5e102c005c2776c7" - integrity sha512-DwrHrnTK+/0QFaB9a8Ol5Lna3k7WvUR4jzSKmz0YaPBpuN2sACyiPVKVfj6ejnjcajAcvn3wlbTyMIn9AZouOg== - dependencies: - axios "^0.21.1" - joi "^17.3.0" - lodash "^4.17.21" - minimist "^1.2.5" - rxjs "^6.6.3" - -watchpack@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce" - integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -web-namespaces@^1.0.0, web-namespaces@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" - integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== - -webpack-bundle-analyzer@^4.4.0: - version "4.4.2" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.2.tgz#39898cf6200178240910d629705f0f3493f7d666" - integrity sha512-PIagMYhlEzFfhMYOzs5gFT55DkUdkyrJi/SxJp8EF3YMWhS+T9vvs2EoTetpk5qb6VsCq02eXTlRDOydRhDFAQ== - dependencies: - acorn "^8.0.4" - acorn-walk "^8.0.0" - chalk "^4.1.0" - commander "^6.2.0" - gzip-size "^6.0.0" - lodash "^4.17.20" - opener "^1.5.2" - sirv "^1.0.7" - ws "^7.3.1" - -webpack-dev-middleware@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" - integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== - dependencies: - memory-fs "^0.4.1" - mime "^2.4.4" - mkdirp "^0.5.1" - range-parser "^1.2.1" - webpack-log "^2.0.0" - -webpack-dev-server@^3.11.2: - version "3.11.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz#695ebced76a4929f0d5de7fd73fafe185fe33708" - integrity sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ== - dependencies: - ansi-html "0.0.7" - bonjour "^3.5.0" - chokidar "^2.1.8" - compression "^1.7.4" - connect-history-api-fallback "^1.6.0" - debug "^4.1.1" - del "^4.1.1" - express "^4.17.1" - html-entities "^1.3.1" - http-proxy-middleware "0.19.1" - import-local "^2.0.0" - internal-ip "^4.3.0" - ip "^1.1.5" - is-absolute-url "^3.0.3" - killable "^1.0.1" - loglevel "^1.6.8" - opn "^5.5.0" - p-retry "^3.0.1" - portfinder "^1.0.26" - schema-utils "^1.0.0" - selfsigned "^1.10.8" - semver "^6.3.0" - serve-index "^1.9.1" - sockjs "^0.3.21" - sockjs-client "^1.5.0" - spdy "^4.0.2" - strip-ansi "^3.0.1" - supports-color "^6.1.0" - url "^0.11.0" - webpack-dev-middleware "^3.7.2" - webpack-log "^2.0.0" - ws "^6.2.1" - yargs "^13.3.2" - -webpack-log@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" - integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== - dependencies: - ansi-colors "^3.0.0" - uuid "^3.3.2" - -webpack-merge@^5.7.3: - version "5.7.3" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.7.3.tgz#2a0754e1877a25a8bbab3d2475ca70a052708213" - integrity sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA== - dependencies: - clone-deep "^4.0.1" - wildcard "^2.0.0" - -webpack-sources@^1.1.0, webpack-sources@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack-sources@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.0.tgz#9ed2de69b25143a4c18847586ad9eccb19278cfa" - integrity sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ== - dependencies: - source-list-map "^2.0.1" - source-map "^0.6.1" - -webpack@^5.28.0: - version "5.38.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.38.1.tgz#5224c7f24c18e729268d3e3bc97240d6e880258e" - integrity sha512-OqRmYD1OJbHZph6RUMD93GcCZy4Z4wC0ele4FXyYF0J6AxO1vOSuIlU1hkS/lDlR9CDYBz64MZRmdbdnFFoT2g== - dependencies: - "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.47" - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/wasm-edit" "1.11.0" - "@webassemblyjs/wasm-parser" "1.11.0" - acorn "^8.2.1" - browserslist "^4.14.5" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.0" - es-module-lexer "^0.4.0" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.4" - json-parse-better-errors "^1.0.2" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.0.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.1.1" - watchpack "^2.2.0" - webpack-sources "^2.3.0" - -webpackbar@^5.0.0-3: - version "5.0.0-3" - resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.0-3.tgz#f4f96c8fb13001b2bb1348252db4c980ab93aaac" - integrity sha512-viW6KCYjMb0NPoDrw2jAmLXU2dEOhRrtku28KmOfeE1vxbfwCYuTbTaMhnkrCZLFAFyY9Q49Z/jzYO80Dw5b8g== - dependencies: - ansi-escapes "^4.3.1" - chalk "^4.1.0" - consola "^2.15.0" - figures "^3.2.0" - pretty-time "^1.1.0" - std-env "^2.2.1" - text-table "^0.2.0" - wrap-ansi "^7.0.0" - -websocket-driver@>=0.5.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" - integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== - dependencies: - http-parser-js ">=0.4.0 <0.4.11" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.9, which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" - -wildcard@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" - integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== - -worker-rpc@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5" - integrity sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg== - dependencies: - microevent.ts "~0.1.1" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -ws@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" - -ws@^7.3.1: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== - -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== - -xml-js@^1.6.11: - version "1.6.11" - resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" - integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== - dependencies: - sax "^1.2.4" - -xtend@^4.0.0, xtend@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zwitch@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" - integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== diff --git a/hyperglass/__init__.py b/hyperglass/__init__.py index d19fd108f..94e20181f 100644 --- a/hyperglass/__init__.py +++ b/hyperglass/__init__.py @@ -1,10 +1,10 @@ -"""hyperglass is a modern, customizable network looking glass written in Python 3. +"""hyperglass is a modern, customizable network looking glass. https://github.com/thatmattlove/hyperglass The Clear BSD License -Copyright (c) 2021 Matthew Love +Copyright (c) 2023 Matthew Love All rights reserved. Redistribution and use in source and binary forms, with or without @@ -40,12 +40,8 @@ import uvloop # Project -from hyperglass.util import set_app_path from hyperglass.constants import METADATA -# Find hyperglass application directory. -set_app_path() - # Use Uvloop for performance. uvloop.install() diff --git a/hyperglass/api/__init__.py b/hyperglass/api/__init__.py index 3221802ee..65458f962 100644 --- a/hyperglass/api/__init__.py +++ b/hyperglass/api/__init__.py @@ -1,271 +1,72 @@ -"""hyperglass REST API & Web UI.""" +"""hyperglass API.""" # Standard Library -import sys -from typing import List -from pathlib import Path +import logging # Third Party -from fastapi import FastAPI -from fastapi.exceptions import ValidationError, RequestValidationError -from starlette.responses import JSONResponse -from starlette.exceptions import HTTPException as StarletteHTTPException -from fastapi.openapi.utils import get_openapi -from starlette.staticfiles import StaticFiles -from starlette.middleware.cors import CORSMiddleware +from litestar import Litestar +from litestar.openapi import OpenAPIConfig +from litestar.exceptions import HTTPException, ValidationException +from litestar.static_files import create_static_files_router # Project -from hyperglass.log import log -from hyperglass.util import cpu_count -from hyperglass.constants import TRANSPORT_REST, __version__ -from hyperglass.api.events import on_startup, on_shutdown -from hyperglass.api.routes import ( - docs, - info, - query, - queries, - routers, - communities, - import_certificate, -) +from hyperglass.state import use_state +from hyperglass.constants import __version__ from hyperglass.exceptions import HyperglassError -from hyperglass.configuration import URL_DEV, STATIC_PATH, params, devices -from hyperglass.api.error_handlers import ( - app_handler, - http_handler, - default_handler, - validation_handler, -) -from hyperglass.models.api.response import ( - QueryError, - InfoResponse, - QueryResponse, - RoutersResponse, - CommunityResponse, - SupportedQueryResponse, -) -WORKING_DIR = Path(__file__).parent -EXAMPLES_DIR = WORKING_DIR / "examples" +# Local +from .events import check_redis +from .routes import info, query, device, devices, queries +from .middleware import COMPRESSION_CONFIG, create_cors_config +from .error_handlers import app_handler, http_handler, default_handler, validation_handler -UI_DIR = STATIC_PATH / "ui" -CUSTOM_DIR = STATIC_PATH / "custom" -IMAGES_DIR = STATIC_PATH / "images" +__all__ = ("app",) -EXAMPLE_DEVICES_PY = EXAMPLES_DIR / "devices.py" -EXAMPLE_QUERIES_PY = EXAMPLES_DIR / "queries.py" -EXAMPLE_QUERY_PY = EXAMPLES_DIR / "query.py" -EXAMPLE_DEVICES_CURL = EXAMPLES_DIR / "devices.sh" -EXAMPLE_QUERIES_CURL = EXAMPLES_DIR / "queries.sh" -EXAMPLE_QUERY_CURL = EXAMPLES_DIR / "query.sh" +STATE = use_state() -ASGI_PARAMS = { - "host": str(params.listen_address), - "port": params.listen_port, - "debug": params.debug, - "workers": cpu_count(2), -} -DOCS_PARAMS = {} -if params.docs.enable: - DOCS_PARAMS.update({"openapi_url": params.docs.openapi_uri}) - if params.docs.mode == "redoc": - DOCS_PARAMS.update({"docs_url": None, "redoc_url": params.docs.uri}) - elif params.docs.mode == "swagger": - DOCS_PARAMS.update({"docs_url": params.docs.uri, "redoc_url": None}) +UI_DIR = STATE.settings.static_path / "ui" +IMAGES_DIR = STATE.settings.static_path / "images" -for directory in (UI_DIR, IMAGES_DIR): - if not directory.exists(): - log.warning("Directory '{d}' does not exist, creating...", d=str(directory)) - directory.mkdir() -# Main App Definition -app = FastAPI( - debug=params.debug, - title=params.site_title, - description=params.site_description, +OPEN_API = OpenAPIConfig( + title=STATE.params.docs.title.format(site_title=STATE.params.site_title), version=__version__, - default_response_class=JSONResponse, - **DOCS_PARAMS, -) - -# Add Event Handlers -for startup in on_startup: - app.add_event_handler("startup", startup) - -for shutdown in on_shutdown: - app.add_event_handler("shutdown", shutdown) - -# HTTP Error Handler -app.add_exception_handler(StarletteHTTPException, http_handler) - -# Backend Application Error Handler -app.add_exception_handler(HyperglassError, app_handler) - -# Request Validation Error Handler -app.add_exception_handler(RequestValidationError, validation_handler) - -# App Validation Error Handler -app.add_exception_handler(ValidationError, validation_handler) - -# Uncaught Error Handler -app.add_exception_handler(Exception, default_handler) - - -def _custom_openapi(): - """Generate custom OpenAPI config.""" - openapi_schema = get_openapi( - title=params.docs.title.format(site_title=params.site_title), - version=__version__, - description=params.docs.description, - routes=app.routes, - ) - openapi_schema["info"]["x-logo"] = { - "url": "/images/light" + params.web.logo.light.suffix - } - - query_samples = [] - queries_samples = [] - devices_samples = [] - - with EXAMPLE_QUERY_CURL.open("r") as e: - example = e.read() - query_samples.append( - {"lang": "cURL", "source": example % str(params.docs.base_url)} - ) - - with EXAMPLE_QUERY_PY.open("r") as e: - example = e.read() - query_samples.append( - {"lang": "Python", "source": example % str(params.docs.base_url)} - ) - - with EXAMPLE_DEVICES_CURL.open("r") as e: - example = e.read() - queries_samples.append( - {"lang": "cURL", "source": example % str(params.docs.base_url)} - ) - with EXAMPLE_DEVICES_PY.open("r") as e: - example = e.read() - queries_samples.append( - {"lang": "Python", "source": example % str(params.docs.base_url)} - ) - - with EXAMPLE_QUERIES_CURL.open("r") as e: - example = e.read() - devices_samples.append( - {"lang": "cURL", "source": example % str(params.docs.base_url)} - ) - - with EXAMPLE_QUERIES_PY.open("r") as e: - example = e.read() - devices_samples.append( - {"lang": "Python", "source": example % str(params.docs.base_url)} - ) - - openapi_schema["paths"]["/api/query/"]["post"]["x-code-samples"] = query_samples - openapi_schema["paths"]["/api/devices"]["get"]["x-code-samples"] = devices_samples - openapi_schema["paths"]["/api/queries"]["get"]["x-code-samples"] = queries_samples - - app.openapi_schema = openapi_schema - return app.openapi_schema - - -CORS_ORIGINS = params.cors_origins.copy() -if params.developer_mode: - CORS_ORIGINS.append(URL_DEV) - -# CORS Configuration -app.add_middleware( - CORSMiddleware, - allow_origins=CORS_ORIGINS, - allow_methods=["GET", "POST", "OPTIONS"], - allow_headers=["*"], -) - -app.add_api_route( - path="/api/info", - endpoint=info, - methods=["GET"], - response_model=InfoResponse, - response_class=JSONResponse, - summary=params.docs.info.summary, - description=params.docs.info.description, - tags=[params.docs.info.title], -) - -app.add_api_route( - path="/api/devices", - endpoint=routers, - methods=["GET"], - response_model=List[RoutersResponse], - response_class=JSONResponse, - summary=params.docs.devices.summary, - description=params.docs.devices.description, - tags=[params.docs.devices.title], -) - -app.add_api_route( - path="/api/communities", - endpoint=communities, - methods=["GET"], - response_model=List[CommunityResponse], - summary=params.docs.communities.summary, - tags=[params.docs.communities.title], + description=STATE.params.docs.description, + path=STATE.params.docs.path, + root_schema_site="elements", ) -app.add_api_route( - path="/api/queries", - endpoint=queries, - methods=["GET"], - response_class=JSONResponse, - response_model=List[SupportedQueryResponse], - summary=params.docs.queries.summary, - description=params.docs.queries.description, - tags=[params.docs.queries.title], -) - -app.add_api_route( - path="/api/query/", - endpoint=query, - methods=["POST"], - summary=params.docs.query.summary, - description=params.docs.query.description, - responses={ - 400: {"model": QueryError, "description": "Request Content Error"}, - 422: {"model": QueryError, "description": "Request Format Error"}, - 500: {"model": QueryError, "description": "Server Error"}, +HANDLERS = [ + device, + devices, + queries, + info, + query, +] + +if not STATE.settings.disable_ui: + HANDLERS = [ + *HANDLERS, + create_static_files_router( + path="/images", directories=[IMAGES_DIR], name="images", include_in_schema=False + ), + create_static_files_router( + path="/", directories=[UI_DIR], name="ui", html_mode=True, include_in_schema=False + ), + ] + + +app = Litestar( + route_handlers=HANDLERS, + exception_handlers={ + HTTPException: http_handler, + HyperglassError: app_handler, + ValidationException: validation_handler, + Exception: default_handler, }, - response_model=QueryResponse, - tags=[params.docs.query.title], - response_class=JSONResponse, + on_startup=[check_redis], + debug=STATE.settings.debug, + cors_config=create_cors_config(state=STATE), + compression_config=COMPRESSION_CONFIG, + openapi_config=OPEN_API if STATE.params.docs.enable else None, ) - -# Enable certificate import route only if a device using -# hyperglass-agent is defined. -if [n for n in devices.all_nos if n in TRANSPORT_REST]: - app.add_api_route( - path="/api/import-agent-certificate/", - endpoint=import_certificate, - methods=["POST"], - include_in_schema=False, - ) - -if params.docs.enable: - app.add_api_route(path=params.docs.uri, endpoint=docs, include_in_schema=False) - app.openapi = _custom_openapi - log.debug("API Docs config: {}", app.openapi()) - -app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images") -app.mount("/custom", StaticFiles(directory=CUSTOM_DIR), name="custom") -app.mount("/", StaticFiles(directory=UI_DIR, html=True), name="ui") - - -def start(**kwargs): - """Start the web server with Uvicorn ASGI.""" - # Third Party - import uvicorn - - try: - uvicorn.run("hyperglass.api:app", **ASGI_PARAMS, **kwargs) - except KeyboardInterrupt: - sys.exit(0) diff --git a/hyperglass/api/error_handlers.py b/hyperglass/api/error_handlers.py index d2ac9bd22..e56f88400 100644 --- a/hyperglass/api/error_handlers.py +++ b/hyperglass/api/error_handlers.py @@ -1,40 +1,78 @@ """API Error Handlers.""" +# Standard Library +import typing as t + # Third Party -from starlette.responses import JSONResponse +from litestar import Request, Response +from litestar.exceptions import ValidationException # Project -from hyperglass.configuration import params +from hyperglass.log import log +from hyperglass.state import use_state + +__all__ = ( + "default_handler", + "http_handler", + "app_handler", + "validation_handler", +) + + +def get_validation_exception_detail(exc: ValidationException) -> Response: + data: dict[str, t.Any] = { + "level": "error", + "status_code": 422, + "keywords": [], + "output": repr(exc), + } + if isinstance(exc.extra, dict): + outputs = [] + kw = [] + for k, v in exc.extra.values(): + outputs = [*outputs, f"{k}: {v!r}"] + kw = [*kw, k] + data["output"] = "\n".join(outputs) + data["keywords"] = kw + if isinstance(exc.extra, list): + data["output"] = "\n".join(str(v) for v in exc.extra) + data["keywords"] = [] -async def default_handler(request, exc): + return Response(data) + + +def default_handler(request: Request, exc: BaseException) -> Response: """Handle uncaught errors.""" - return JSONResponse( - {"output": params.messages.general, "level": "danger", "keywords": []}, + state = use_state() + log.bind(method=request.method, path=request.url.path, detail=str(exc)).critical("Error") + return Response( + {"output": state.params.messages.general, "level": "danger", "keywords": []}, status_code=500, ) -async def http_handler(request, exc): +def http_handler(request: Request, exc: BaseException) -> Response: """Handle web server errors.""" - return JSONResponse( + log.bind(method=request.method, path=request.url.path, detail=exc.detail).critical("HTTP Error") + return Response( {"output": exc.detail, "level": "danger", "keywords": []}, status_code=exc.status_code, ) -async def app_handler(request, exc): +def app_handler(request: Request, exc: BaseException) -> Response: """Handle application errors.""" - return JSONResponse( + log.bind(method=request.method, path=request.url.path, detail=exc.message).critical( + "hyperglass Error" + ) + return Response( {"output": exc.message, "level": exc.level, "keywords": exc.keywords}, status_code=exc.status_code, ) -async def validation_handler(request, exc): +def validation_handler(request: Request, exc: ValidationException) -> Response: """Handle Pydantic validation errors raised by FastAPI.""" - error = exc.errors()[0] - return JSONResponse( - {"output": error["msg"], "level": "error", "keywords": error["loc"]}, - status_code=422, - ) + log.bind(method=request.method, path=request.url.path, detail=exc).critical("Validation Error") + return get_validation_exception_detail(exc) diff --git a/hyperglass/api/events.py b/hyperglass/api/events.py index 418010aa5..3d20c4ab0 100644 --- a/hyperglass/api/events.py +++ b/hyperglass/api/events.py @@ -1,16 +1,18 @@ """API Events.""" -# Project -from hyperglass.cache import AsyncCache -from hyperglass.configuration import REDIS_CONFIG, params +# Standard Library +import typing as t +# Third Party +from litestar import Litestar -async def check_redis() -> bool: - """Ensure Redis is running before starting server.""" - cache = AsyncCache(db=params.cache.database, **REDIS_CONFIG) - await cache.test() - return True +# Project +from hyperglass.state import use_state +__all__ = ("check_redis",) -on_startup = (check_redis,) -on_shutdown = () + +async def check_redis(_: Litestar) -> t.NoReturn: + """Ensure Redis is running before starting server.""" + cache = use_state("cache") + cache.check() diff --git a/hyperglass/api/examples/devices.py b/hyperglass/api/examples/devices.py deleted file mode 100644 index e6ec88da0..000000000 --- a/hyperglass/api/examples/devices.py +++ /dev/null @@ -1,6 +0,0 @@ -# Third Party -import httpx - -request = httpx.get("%s/api/devices") - -print(request.json()) diff --git a/hyperglass/api/examples/devices.sh b/hyperglass/api/examples/devices.sh deleted file mode 100644 index cf94cb23c..000000000 --- a/hyperglass/api/examples/devices.sh +++ /dev/null @@ -1 +0,0 @@ -curl %s/api/devices \ No newline at end of file diff --git a/hyperglass/api/examples/queries.py b/hyperglass/api/examples/queries.py deleted file mode 100644 index b44b0fabc..000000000 --- a/hyperglass/api/examples/queries.py +++ /dev/null @@ -1,6 +0,0 @@ -# Third Party -import httpx - -request = httpx.get("%s/api/queries") - -print(request.json()) diff --git a/hyperglass/api/examples/queries.sh b/hyperglass/api/examples/queries.sh deleted file mode 100644 index b89af31ef..000000000 --- a/hyperglass/api/examples/queries.sh +++ /dev/null @@ -1 +0,0 @@ -curl %s/api/queries \ No newline at end of file diff --git a/hyperglass/api/examples/query.py b/hyperglass/api/examples/query.py deleted file mode 100644 index 3b75ddfbd..000000000 --- a/hyperglass/api/examples/query.py +++ /dev/null @@ -1,13 +0,0 @@ -# Third Party -import httpx - -query = { - "query_location": "router01", - "query_type": "bgp_route", - "query_vrf": "default", - "query_target": "1.1.1.0/24", -} - -request = httpx.post("%s/api/query/", data=query) - -print(request.json().get("output")) diff --git a/hyperglass/api/examples/query.sh b/hyperglass/api/examples/query.sh deleted file mode 100644 index ec41f6c15..000000000 --- a/hyperglass/api/examples/query.sh +++ /dev/null @@ -1,7 +0,0 @@ -curl -X POST %s/api/query/ -d \ - '{ - "query_location": "router01", - "query_type": "bgp_route", - "query_vrf": "default", - "query_target": "1.1.1.0/24" - }' \ No newline at end of file diff --git a/hyperglass/api/fake_output.py b/hyperglass/api/fake_output.py index 1c7ffeda4..8405a828a 100644 --- a/hyperglass/api/fake_output.py +++ b/hyperglass/api/fake_output.py @@ -1,10 +1,12 @@ """Return fake, static data for development purposes.""" # Standard Library -from typing import Dict, Union +import typing as t -PLAIN = r""" -BGP routing table entry for 4.0.0.0/9, version 1017877672 +# Project +from hyperglass.models.data import BGPRouteTable + +BGP_PLAIN = r"""BGP routing table entry for 4.0.0.0/9, version 1017877672 BGP Bestpath: deterministic-med Paths: (10 available, best #9, table default) Advertised to update-groups: @@ -51,7 +53,7 @@ Community: 6939:7016 6939:8840 6939:9001 """ # noqa: W291,E501 -ROUTES = [ +BGP_ROUTES = [ { "prefix": "1.1.1.0/24", "active": True, @@ -156,19 +158,42 @@ }, ] -STRUCTURED = { - "vrf": "default", - "count": len(ROUTES), - "routes": ROUTES, - "winning_weight": "high", -} +PING = r"""PING 1.1.1.1 (1.1.1.1): 56 data bytes +64 bytes from 1.1.1.1: icmp_seq=0 ttl=59 time=4.696 ms +64 bytes from 1.1.1.1: icmp_seq=1 ttl=59 time=4.699 ms +64 bytes from 1.1.1.1: icmp_seq=2 ttl=59 time=4.640 ms +64 bytes from 1.1.1.1: icmp_seq=3 ttl=59 time=4.583 ms +64 bytes from 1.1.1.1: icmp_seq=4 ttl=59 time=4.640 ms +--- 1.1.1.1 ping statistics --- +5 packets transmitted, 5 packets received, 0% packet loss +round-trip min/avg/max/stddev = 4.583/4.652/4.699/0.043 ms +""" -async def fake_output(structured: bool) -> Union[str, Dict]: - """Bypass the standard execution process and return static, fake output.""" - output = PLAIN +TRACEROUTE = r"""traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, 52 byte packets + 1 157.231.183.50 4.412 ms + 2 129.219.10.4 4.612 ms + 3 128.249.9.12 4.503 ms + 4 139.15.19.3 7.458 ms + 5 172.69.68.3 4.814 ms + 6 1.1.1.1 4.564 ms +""" - if structured: - output = STRUCTURED - return output +async def fake_output(query_type: str, structured: bool) -> t.Union[str, BGPRouteTable]: + """Bypass the standard execution process and return static, fake output.""" + + if "ping" in query_type: + return PING + if "traceroute" in query_type: + return TRACEROUTE + if "bgp" in query_type: + if structured: + return BGPRouteTable( + vrf="default", + count=len(BGP_ROUTES), + routes=BGP_ROUTES, + winning_weight="high", + ) + return BGP_PLAIN + return BGP_PLAIN diff --git a/hyperglass/api/middleware.py b/hyperglass/api/middleware.py new file mode 100644 index 000000000..71a57266b --- /dev/null +++ b/hyperglass/api/middleware.py @@ -0,0 +1,34 @@ +"""hyperglass API middleware.""" + +# Standard Library +import typing as t + +# Third Party +from litestar.config.cors import CORSConfig +from litestar.config.compression import CompressionConfig + +if t.TYPE_CHECKING: + # Project + from hyperglass.state import HyperglassState + +__all__ = ("create_cors_config", "COMPRESSION_CONFIG") + +COMPRESSION_CONFIG = CompressionConfig(backend="brotli", brotli_gzip_fallback=True) + +REQUEST_LOG_MESSAGE = "REQ" +RESPONSE_LOG_MESSAGE = "RES" +REQUEST_LOG_FIELDS = ("method", "path", "path_params", "query") +RESPONSE_LOG_FIELDS = ("status_code",) + + +def create_cors_config(state: "HyperglassState") -> CORSConfig: + """Create CORS configuration from parameters.""" + origins = state.params.cors_origins.copy() + if state.settings.dev_mode: + origins = [*origins, state.settings.dev_url, "http://localhost:3000"] + + return CORSConfig( + allow_origins=origins, + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["*"], + ) diff --git a/hyperglass/api/routes.py b/hyperglass/api/routes.py index 9f5b41b69..9e17c9a69 100644 --- a/hyperglass/api/routes.py +++ b/hyperglass/api/routes.py @@ -1,266 +1,169 @@ """API Routes.""" # Standard Library -import os import json import time -from datetime import datetime +import typing as t +from datetime import UTC, datetime # Third Party -from fastapi import HTTPException, BackgroundTasks -from starlette.requests import Request -from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html +from litestar import Request, Response, get, post +from litestar.di import Provide +from litestar.background_tasks import BackgroundTask # Project from hyperglass.log import log -from hyperglass.cache import AsyncCache -from hyperglass.encode import jwt_decode -from hyperglass.external import Webhook, bgptools -from hyperglass.api.tasks import process_headers, import_public_key -from hyperglass.constants import __version__ +from hyperglass.state import HyperglassState from hyperglass.exceptions import HyperglassError -from hyperglass.models.api import Query, EncodedRequest -from hyperglass.configuration import REDIS_CONFIG, params, devices +from hyperglass.models.api import Query +from hyperglass.models.data import OutputDataModel +from hyperglass.util.typing import is_type from hyperglass.execution.main import execute +from hyperglass.models.api.response import QueryResponse +from hyperglass.models.config.params import Params, APIParams +from hyperglass.models.config.devices import Devices, APIDevice # Local +from .state import get_state, get_params, get_devices +from .tasks import send_webhook from .fake_output import fake_output -APP_PATH = os.environ["hyperglass_directory"] +__all__ = ( + "device", + "devices", + "queries", + "info", + "query", +) -async def send_webhook(query_data: Query, request: Request, timestamp: datetime): - """If webhooks are enabled, get request info and send a webhook. +@get("/api/devices/{id:str}", dependencies={"devices": Provide(get_devices)}) +async def device(devices: Devices, id: str) -> APIDevice: + """Retrieve a device by ID.""" + return devices[id].export_api() - Args: - query_data (Query): Valid query - request (Request): Starlette/FastAPI request - Returns: - int: Returns 1 regardless of result - """ - try: - if params.logging.http is not None: - headers = await process_headers(headers=request.headers) +@get("/api/devices", dependencies={"devices": Provide(get_devices)}) +async def devices(devices: Devices) -> t.List[APIDevice]: + """Retrieve all devices.""" + return devices.export_api() - if headers.get("x-real-ip") is not None: - host = headers["x-real-ip"] - elif headers.get("x-forwarded-for") is not None: - host = headers["x-forwarded-for"] - else: - host = request.client.host - network_info = await bgptools.network_info(host) +@get("/api/queries", dependencies={"devices": Provide(get_devices)}) +async def queries(devices: Devices) -> t.List[str]: + """Retrieve all directive names.""" + return devices.directive_names() - async with Webhook(params.logging.http) as hook: - await hook.send( - query={ - **query_data.export_dict(pretty=True), - "headers": headers, - "source": host, - "network": network_info.get(host, {}), - "timestamp": timestamp, - } - ) - except Exception as err: - log.error( - "Error sending webhook to {}: {}", params.logging.http.provider, str(err) - ) +@get("/api/info", dependencies={"params": Provide(get_params)}) +async def info(params: Params) -> APIParams: + """Retrieve looking glass parameters.""" + return params.export_api() -async def query(query_data: Query, request: Request, background_tasks: BackgroundTasks): +@post("/api/query", dependencies={"_state": Provide(get_state)}) +async def query(_state: HyperglassState, request: Request, data: Query) -> QueryResponse: """Ingest request data pass it to the backend application to perform the query.""" - timestamp = datetime.utcnow() - background_tasks.add_task(send_webhook, query_data, request, timestamp) + timestamp = datetime.now(UTC) # Initialize cache - cache = AsyncCache(db=params.cache.database, **REDIS_CONFIG) - log.debug("Initialized cache {}", repr(cache)) + cache = _state.redis - # Use hashed query_data string as key for for k/v cache store so + # Use hashed `data` string as key for for k/v cache store so # each command output value is unique. - cache_key = query_data.digest() + cache_key = f"hyperglass.query.{data.digest()}" - # Define cache entry expiry time - cache_timeout = params.cache.timeout + _log = log.bind(query=data.summary()) - log.debug("Cache Timeout: {}", cache_timeout) - log.info("Starting query execution for query {}", query_data.summary) - - cache_response = await cache.get_dict(cache_key, "output") + _log.info("Starting query execution") + cache_response = cache.get_map(cache_key, "output") json_output = False - - if query_data.device.structured_output and query_data.query_type in ( - "bgp_route", - "bgp_community", - "bgp_aspath", - ): - json_output = True - cached = False runtime = 65535 + if cache_response: - log.debug("Query {} exists in cache", cache_key) + _log.bind(cache_key=cache_key).debug("Cache hit") # If a cached response exists, reset the expiration time. - await cache.expire(cache_key, seconds=cache_timeout) + cache.expire(cache_key, expire_in=_state.params.cache.timeout) cached = True runtime = 0 - timestamp = await cache.get_dict(cache_key, "timestamp") + timestamp = cache.get_map(cache_key, "timestamp") elif not cache_response: - log.debug("No existing cache entry for query {}", cache_key) - log.debug( - "Created new cache key {} entry for query {}", cache_key, query_data.summary - ) + _log.bind(cache_key=cache_key).debug("Cache miss") - timestamp = query_data.timestamp + timestamp = data.timestamp starttime = time.time() - if params.fake_output: + if _state.params.fake_output: # Return fake, static data for development purposes, if enabled. - cache_output = await fake_output(json_output) + output = await fake_output( + query_type=data.query_type, + structured=data.device.structured_output or False, + ) else: # Pass request to execution module - cache_output = await execute(query_data) + output = await execute(data) endtime = time.time() elapsedtime = round(endtime - starttime, 4) - log.debug("Query {} took {} seconds to run.", cache_key, elapsedtime) + _log.debug("Runtime: {!s} seconds", elapsedtime) + + if output is None: + raise HyperglassError(message=_state.params.messages.general, alert="danger") - if cache_output is None: - raise HyperglassError(message=params.messages.general, alert="danger") + json_output = is_type(output, OutputDataModel) - # Create a cache entry if json_output: - raw_output = json.dumps(cache_output) + # Export structured output as JSON string to guarantee value + # is serializable, then convert it back to a dict. + as_json = output.export_json() + raw_output = json.loads(as_json) else: - raw_output = str(cache_output) - await cache.set_dict(cache_key, "output", raw_output) - await cache.set_dict(cache_key, "timestamp", timestamp) - await cache.expire(cache_key, seconds=cache_timeout) + raw_output = str(output) - log.debug("Added cache entry for query: {}", cache_key) + cache.set_map_item(cache_key, "output", raw_output) + cache.set_map_item(cache_key, "timestamp", timestamp) + cache.expire(cache_key, expire_in=_state.params.cache.timeout) + + _log.bind(cache_timeout=_state.params.cache.timeout).debug("Response cached") runtime = int(round(elapsedtime, 0)) # If it does, return the cached entry - cache_response = await cache.get_dict(cache_key, "output") + cache_response = cache.get_map(cache_key, "output") + + json_output = is_type(cache_response, t.Dict) response_format = "text/plain" if json_output: response_format = "application/json" + _log.info("Execution completed") - log.debug("Cache match for {}:\n{}", cache_key, cache_response) - log.success("Completed query execution for query {}", query_data.summary) - - return { + response = { "output": cache_response, "id": cache_key, "cached": cached, "runtime": runtime, "timestamp": timestamp, "format": response_format, - "random": query_data.random(), + "random": data.random(), "level": "success", "keywords": [], } - -async def import_certificate(encoded_request: EncodedRequest): - """Import a certificate from hyperglass-agent.""" - - # Try to match the requested device name with configured devices - log.debug( - "Attempting certificate import for device '{}'", devices[encoded_request.device] + return Response( + response, + background=BackgroundTask( + send_webhook, + params=_state.params, + data=data, + request=request, + timestamp=timestamp, + ), ) - try: - matched_device = devices[encoded_request.device] - except AttributeError: - raise HTTPException( - detail=f"Device {str(encoded_request.device)} not found", status_code=404 - ) - - try: - # Decode JSON Web Token - decoded_request = await jwt_decode( - payload=encoded_request.encoded, - secret=matched_device.credential.password.get_secret_value(), - ) - except HyperglassError as decode_error: - raise HTTPException(detail=str(decode_error), status_code=400) - - try: - # Write certificate to file - import_public_key( - app_path=APP_PATH, - device_name=matched_device._id, - keystring=decoded_request, - ) - except RuntimeError as err: - raise HyperglassError(str(err), level="danger") - - log.info("Added public key for {}", encoded_request.device) - return { - "output": f"Added public key for {encoded_request.device}", - "level": "success", - "keywords": [encoded_request.device], - } - - -async def docs(): - """Serve custom docs.""" - if params.docs.enable: - docs_func_map = {"swagger": get_swagger_ui_html, "redoc": get_redoc_html} - docs_func = docs_func_map[params.docs.mode] - return docs_func( - openapi_url=params.docs.openapi_url, title=params.site_title + " - API Docs" - ) - else: - raise HTTPException(detail="Not found", status_code=404) - - -async def routers(): - """Serve list of configured routers and attributes.""" - return [ - d.dict( - include={ - "name": ..., - "network": ..., - "display_name": ..., - "vrfs": {-1: {"name", "display_name"}}, - } - ) - for d in devices.objects - ] - - -async def communities(): - """Serve list of configured communities if mode is select.""" - if params.queries.bgp_community.mode != "select": - raise HTTPException(detail="BGP community mode is not select", status_code=404) - - return [c.export_dict() for c in params.queries.bgp_community.communities] - - -async def queries(): - """Serve list of enabled query types.""" - return params.queries.list - - -async def info(): - """Serve general information about this instance of hyperglass.""" - return { - "name": params.site_title, - "organization": params.org_name, - "primary_asn": int(params.primary_asn), - "version": f"hyperglass {__version__}", - } - - -endpoints = [query, docs, routers, info] diff --git a/hyperglass/api/state.py b/hyperglass/api/state.py new file mode 100644 index 000000000..f41a96d78 --- /dev/null +++ b/hyperglass/api/state.py @@ -0,0 +1,27 @@ +"""hyperglass state dependencies.""" + +# Standard Library +import typing as t + +# Project +from hyperglass.state import use_state + + +async def get_state(attr: t.Optional[str] = None): + """Get hyperglass state as a FastAPI dependency.""" + return use_state(attr) + + +async def get_params(): + """Get hyperglass params as FastAPI dependency.""" + return use_state("params") + + +async def get_devices(): + """Get hyperglass devices as FastAPI dependency.""" + return use_state("devices") + + +async def get_ui_params(): + """Get hyperglass ui_params as FastAPI dependency.""" + return use_state("ui_params") diff --git a/hyperglass/api/tasks.py b/hyperglass/api/tasks.py index 1761d4f52..0a7d16d46 100644 --- a/hyperglass/api/tasks.py +++ b/hyperglass/api/tasks.py @@ -1,43 +1,26 @@ """Tasks to be executed from web API.""" # Standard Library -from typing import Dict, Union -from pathlib import Path +import typing as t +from datetime import datetime # Third Party from httpx import Headers +from litestar import Request +# Project +from hyperglass.log import log +from hyperglass.external import Webhook, bgptools +from hyperglass.models.api import Query -def import_public_key( - app_path: Union[Path, str], device_name: str, keystring: str -) -> bool: - """Import a public key for hyperglass-agent.""" - if not isinstance(app_path, Path): - app_path = Path(app_path) +if t.TYPE_CHECKING: + # Project + from hyperglass.models.config.params import Params - cert_dir = app_path / "certs" +__all__ = ("send_webhook",) - if not cert_dir.exists(): - cert_dir.mkdir() - if not cert_dir.exists(): - raise RuntimeError(f"Failed to create certs directory at {str(cert_dir)}") - - filename = f"{device_name}.pem" - cert_file = cert_dir / filename - - with cert_file.open("w+") as file: - file.write(str(keystring)) - - with cert_file.open("r") as file: - read_file = file.read().strip() - if not keystring == read_file: - raise RuntimeError("Wrote key, but written file did not match input key") - - return True - - -async def process_headers(headers: Headers) -> Dict: +async def process_headers(headers: Headers) -> t.Dict[str, t.Any]: """Filter out unwanted headers and return as a dictionary.""" headers = dict(headers) header_keys = ( @@ -49,3 +32,39 @@ async def process_headers(headers: Headers) -> Dict: "x-forwarded-for", ) return {k: headers.get(k) for k in header_keys} + + +async def send_webhook( + params: "Params", + data: Query, + request: Request, + timestamp: datetime, +) -> t.NoReturn: + """If webhooks are enabled, get request info and send a webhook.""" + try: + if params.logging.http is not None: + headers = await process_headers(headers=request.headers) + + if headers.get("x-real-ip") is not None: + host = headers["x-real-ip"] + elif headers.get("x-forwarded-for") is not None: + host = headers["x-forwarded-for"] + else: + host = request.client.host + + network_info = await bgptools.network_info(host) + + async with Webhook(params.logging.http) as hook: + await hook.send( + query={ + **data.dict(), + "headers": headers, + "source": host, + "network": network_info.get(host, {}), + "timestamp": timestamp, + } + ) + except Exception as err: + log.bind(destination=params.logging.http.provider, error=str(err)).error( + "Failed to send webhook" + ) diff --git a/hyperglass/cache/__init__.py b/hyperglass/cache/__init__.py deleted file mode 100644 index 04c3efd26..000000000 --- a/hyperglass/cache/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Redis cache handlers.""" - -# Project -from hyperglass.cache.aio import AsyncCache -from hyperglass.cache.sync import SyncCache - -__all__ = ("AsyncCache", "SyncCache") diff --git a/hyperglass/cache/aio.py b/hyperglass/cache/aio.py deleted file mode 100644 index 9484fbbb7..000000000 --- a/hyperglass/cache/aio.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Asyncio Redis cache handler.""" - -# Standard Library -import json -import time -import pickle -import asyncio -from typing import Any, Dict - -# Third Party -from yaaredis import StrictRedis as AsyncRedis -from yaaredis.pubsub import PubSub as AsyncPubSub -from yaaredis.exceptions import RedisError - -# Project -from hyperglass.cache.base import BaseCache -from hyperglass.exceptions import HyperglassError - - -class AsyncCache(BaseCache): - """Asynchronous Redis cache handler.""" - - def __init__(self, *args, **kwargs): - """Initialize Redis connection.""" - super().__init__(*args, **kwargs) - - password = self.password - if password is not None: - password = password.get_secret_value() - - self.instance: AsyncRedis = AsyncRedis( - db=self.db, - host=self.host, - port=self.port, - password=password, - decode_responses=self.decode_responses, - **self.redis_args, - ) - - async def test(self): - """Send an echo to Redis to ensure it can be reached.""" - try: - await self.instance.echo("hyperglass test") - except RedisError as err: - err_msg = str(err) - if not err_msg and hasattr(err, "__context__"): - # Some Redis exceptions are raised without a message - # even if they are raised from another exception that - # does have a message. - err_msg = str(err.__context__) - - if "auth" in err_msg.lower(): - raise HyperglassError( - "Authentication to Redis server {server} failed.".format( - server=repr(self) - ), - level="danger", - ) from None - else: - print(err_msg) - raise HyperglassError( - "Unable to connect to Redis server {server}".format( - server=repr(self) - ), - level="danger", - ) from None - - async def get(self, *args: str) -> Any: - """Get item(s) from cache.""" - if len(args) == 1: - raw = await self.instance.get(args[0]) - else: - raw = await self.instance.mget(args) - return self.parse_types(raw) - - async def get_dict(self, key: str, field: str = "") -> Any: - """Get hash map (dict) item(s).""" - if not field: - raw = await self.instance.hgetall(key) - else: - raw = await self.instance.hget(key, field) - - return self.parse_types(raw) - - async def set(self, key: str, value: str) -> bool: - """Set cache values.""" - return await self.instance.set(key, value) - - async def set_dict(self, key: str, field: str, value: str) -> bool: - """Set hash map (dict) values.""" - success = False - - if isinstance(value, Dict): - value = json.dumps(value) - else: - value = str(value) - - response = await self.instance.hset(key, field, value) - - if response in (0, 1): - success = True - - return success - - async def wait(self, pubsub: AsyncPubSub, timeout: int = 30, **kwargs) -> Any: - """Wait for pub/sub messages & return posted message.""" - now = time.time() - timeout = now + timeout - - while now < timeout: - - message = await pubsub.get_message(ignore_subscribe_messages=True, **kwargs) - - if message is not None and message["type"] == "message": - data = message["data"] - return self.parse_types(data) - - await asyncio.sleep(0.01) - now = time.time() - - return None - - async def pubsub(self) -> AsyncPubSub: - """Provide an yaaredis.pubsub.Pubsub instance.""" - return self.instance.pubsub() - - async def pub(self, key: str, value: str) -> None: - """Publish a value.""" - await asyncio.sleep(1) - await self.instance.publish(key, value) - - async def clear(self) -> None: - """Clear the cache.""" - await self.instance.flushdb() - - async def delete(self, *keys: str) -> None: - """Delete a cache key.""" - await self.instance.delete(*keys) - - async def expire(self, *keys: str, seconds: int) -> None: - """Set timeout of key in seconds.""" - for key in keys: - await self.instance.expire(key, seconds) - - async def get_config(self) -> Dict: - """Get picked config object from cache.""" - - pickled = await self.instance.get("HYPERGLASS_CONFIG") - return pickle.loads(pickled) diff --git a/hyperglass/cache/base.py b/hyperglass/cache/base.py deleted file mode 100644 index 87fa43f6c..000000000 --- a/hyperglass/cache/base.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Base Redis cache handler.""" - -# Standard Library -import re -import json -from typing import Any, Optional - -# Third Party -from pydantic import SecretStr - - -class BaseCache: - """Redis cache handler.""" - - def __init__( - self, - db: int, - host: str = "localhost", - port: int = 6379, - password: Optional[SecretStr] = None, - decode_responses: bool = True, - **kwargs: Any, - ) -> None: - """Initialize Redis connection.""" - self.db: int = db - self.host: str = str(host) - self.port: int = port - self.password: Optional[SecretStr] = password - self.decode_responses: bool = decode_responses - self.redis_args: dict = kwargs - - def __repr__(self) -> str: - """Represent class state.""" - return "HyperglassCache(db={}, host={}, port={}, password={})".format( - self.db, self.host, self.port, self.password - ) - - def parse_types(self, value: str) -> Any: - """Parse a string to standard python types.""" - - def parse_string(str_value: str): - - is_float = (re.compile(r"^(\d+\.\d+)$"), float) - is_int = (re.compile(r"^(\d+)$"), int) - is_bool = (re.compile(r"^(True|true|False|false)$"), bool) - is_none = (re.compile(r"^(None|none|null|nil|\(nil\))$"), lambda v: None) - is_jsonable = (re.compile(r"^[\{\[].*[\}\]]$"), json.loads) - - for pattern, factory in (is_float, is_int, is_bool, is_none, is_jsonable): - if isinstance(str_value, str) and bool(re.match(pattern, str_value)): - str_value = factory(str_value) - break - return str_value - - if isinstance(value, str): - value = parse_string(value) - elif isinstance(value, bytes): - value = parse_string(value.decode("utf-8")) - elif isinstance(value, list): - value = [parse_string(i) for i in value] - elif isinstance(value, tuple): - value = tuple(parse_string(i) for i in value) - elif isinstance(value, dict): - value = {k: self.parse_types(v) for k, v in value.items()} - - return value diff --git a/hyperglass/cache/sync.py b/hyperglass/cache/sync.py deleted file mode 100644 index 03a913b1c..000000000 --- a/hyperglass/cache/sync.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Non-asyncio Redis cache handler.""" - -# Standard Library -import json -import time -import pickle -from typing import Any, Dict - -# Third Party -from redis import Redis as SyncRedis -from redis.client import PubSub as SyncPubsSub -from redis.exceptions import RedisError - -# Project -from hyperglass.cache.base import BaseCache -from hyperglass.exceptions import HyperglassError - - -class SyncCache(BaseCache): - """Synchronous Redis cache handler.""" - - def __init__(self, *args, **kwargs): - """Initialize Redis connection.""" - super().__init__(*args, **kwargs) - - password = self.password - if password is not None: - password = password.get_secret_value() - - self.instance: SyncRedis = SyncRedis( - db=self.db, - host=self.host, - port=self.port, - password=password, - decode_responses=self.decode_responses, - **self.redis_args, - ) - - def test(self): - """Send an echo to Redis to ensure it can be reached.""" - try: - self.instance.echo("hyperglass test") - except RedisError as err: - err_msg = str(err) - if not err_msg and hasattr(err, "__context__"): - # Some Redis exceptions are raised without a message - # even if they are raised from another exception that - # does have a message. - err_msg = str(err.__context__) - - if "auth" in err_msg.lower(): - raise HyperglassError( - "Authentication to Redis server {server} failed.".format( - server=repr(self) - ), - level="danger", - ) from None - else: - raise HyperglassError( - "Unable to connect to Redis cache {server}".format( - server=repr(self) - ), - level="danger", - ) from None - - def get(self, *args: str) -> Any: - """Get item(s) from cache.""" - if len(args) == 1: - raw = self.instance.get(args[0]) - else: - raw = self.instance.mget(args) - return self.parse_types(raw) - - def get_dict(self, key: str, field: str = "") -> Any: - """Get hash map (dict) item(s).""" - if not field: - raw = self.instance.hgetall(key) - else: - raw = self.instance.hget(key, str(field)) - - return self.parse_types(raw) - - def set(self, key: str, value: str) -> bool: - """Set cache values.""" - return self.instance.set(key, str(value)) - - def set_dict(self, key: str, field: str, value: str) -> bool: - """Set hash map (dict) values.""" - success = False - - if isinstance(value, Dict): - value = json.dumps(value) - else: - value = str(value) - - response = self.instance.hset(key, str(field), value) - - if response in (0, 1): - success = True - - return success - - def wait(self, pubsub: SyncPubsSub, timeout: int = 30, **kwargs) -> Any: - """Wait for pub/sub messages & return posted message.""" - now = time.time() - timeout = now + timeout - - while now < timeout: - - message = pubsub.get_message(ignore_subscribe_messages=True, **kwargs) - - if message is not None and message["type"] == "message": - data = message["data"] - return self.parse_types(data) - - time.sleep(0.01) - now = time.time() - - return None - - def pubsub(self) -> SyncPubsSub: - """Provide a redis.client.Pubsub instance.""" - return self.instance.pubsub() - - def pub(self, key: str, value: str) -> None: - """Publish a value.""" - time.sleep(1) - self.instance.publish(key, value) - - def clear(self) -> None: - """Clear the cache.""" - self.instance.flushdb() - - def delete(self, *keys: str) -> None: - """Delete a cache key.""" - self.instance.delete(*keys) - - def expire(self, *keys: str, seconds: int) -> None: - """Set timeout of key in seconds.""" - for key in keys: - self.instance.expire(key, seconds) - - def get_config(self) -> Dict: - """Get picked config object from cache.""" - - pickled = self.instance.get("HYPERGLASS_CONFIG") - return pickle.loads(pickled) diff --git a/hyperglass/cli/__init__.py b/hyperglass/cli/__init__.py index 86ab7f8e9..be2362771 100644 --- a/hyperglass/cli/__init__.py +++ b/hyperglass/cli/__init__.py @@ -1,6 +1,6 @@ """hyperglass cli module.""" -# Project -from hyperglass.cli.commands import hg +# Local +from .main import cli, run -CLI = hg +__all__ = ("cli", "run") diff --git a/hyperglass/cli/commands.py b/hyperglass/cli/commands.py deleted file mode 100644 index dfdbd4cc6..000000000 --- a/hyperglass/cli/commands.py +++ /dev/null @@ -1,205 +0,0 @@ -"""CLI Command definitions.""" - -# Standard Library -import sys -from pathlib import Path - -# Third Party -from click import group, option, help_option - -# Project -from hyperglass.util import cpu_count - -# Local -from .echo import error, label, success, warning, cmd_help -from .util import build_ui -from .static import LABEL, CLI_HELP, E -from .installer import Installer -from .formatting import HelpColorsGroup, HelpColorsCommand, random_colors - -# Define working directory -WORKING_DIR = Path(__file__).parent - -supports_color = "utf" in sys.getfilesystemencoding().lower() - - -def _print_version(ctx, param, value): - # Project - from hyperglass import __version__ - - if not value or ctx.resilient_parsing: - return - label("hyperglass version: {v}", v=__version__) - ctx.exit() - - -@group( - cls=HelpColorsGroup, - help=CLI_HELP, - context_settings={"help_option_names": ["-h", "--help"], "color": supports_color}, - help_headers_color=LABEL, - help_options_custom_colors=random_colors( - "build-ui", "start", "secret", "setup", "system-info", "clear-cache" - ), -) -@option( - "-v", - "--version", - is_flag=True, - callback=_print_version, - expose_value=False, - is_eager=True, - help=cmd_help(E.NUMBERS, "hyperglass version", supports_color), -) -@help_option( - "-h", - "--help", - help=cmd_help(E.FOLDED_HANDS, "Show this help message", supports_color), -) -def hg(): - """Initialize Click Command Group.""" - pass - - -@hg.command( - "build-ui", help=cmd_help(E.BUTTERFLY, "Create a new UI build", supports_color) -) -@option("-t", "--timeout", required=False, default=180, help="Timeout in seconds") -def build_frontend(timeout): - """Create a new UI build.""" - return build_ui(timeout) - - -@hg.command( # noqa: C901 - "start", - help=cmd_help(E.ROCKET, "Start web server", supports_color), - cls=HelpColorsCommand, - help_options_custom_colors=random_colors("-b", "-d", "-w"), -) -@option("-b", "--build", is_flag=True, help="Render theme & build frontend assets") -@option( - "-d", - "--direct", - is_flag=True, - default=False, - help="Start hyperglass directly instead of through process manager", -) -@option( - "-w", - "--workers", - type=int, - required=False, - default=0, - help=f"Number of workers. By default, calculated from CPU cores [{cpu_count(2)}]", -) -def start(build, direct, workers): # noqa: C901 - """Start web server and optionally build frontend assets.""" - # Project - from hyperglass.api import start as uvicorn_start - from hyperglass.main import start - - kwargs = {} - if workers != 0: - kwargs["workers"] = workers - - try: - - if build: - build_complete = build_ui(timeout=180) - - if build_complete and not direct: - start(**kwargs) - elif build_complete and direct: - uvicorn_start(**kwargs) - - if not build and not direct: - start(**kwargs) - - elif not build and direct: - uvicorn_start(**kwargs) - - except (KeyboardInterrupt, SystemExit) as err: - error_message = str(err) - if (len(error_message)) > 1: - warning(str(err)) - error("Stopping hyperglass due to keyboard interrupt.") - - -@hg.command( - "secret", - help=cmd_help(E.LOCK, "Generate agent secret", supports_color), - cls=HelpColorsCommand, - help_options_custom_colors=random_colors("-l"), -) -@option( - "-l", "--length", "length", default=32, help="Number of characters [default: 32]" -) -def generate_secret(length): - """Generate secret for hyperglass-agent. - - Arguments: - length {int} -- Length of secret - """ - # Standard Library - import secrets - - gen_secret = secrets.token_urlsafe(length) - label("Secret: {s}", s=gen_secret) - - -@hg.command( - "setup", - help=cmd_help(E.TOOLBOX, "Run the setup wizard", supports_color), - cls=HelpColorsCommand, - help_options_custom_colors=random_colors("-d"), -) -@option( - "-d", - "--use-defaults", - "unattended", - default=False, - is_flag=True, - help="Use hyperglass defaults (requires no input)", -) -def setup(unattended): - """Define application directory, move example files, generate systemd service.""" - - installer = Installer(unattended=unattended) - installer.install() - - success( - """Completed hyperglass installation. -After adding your hyperglass.yaml file, you should run the `hyperglass build-ui` command.""" # noqa: E501 - ) - - -@hg.command( - "system-info", - help=cmd_help( - E.THERMOMETER, " Get system information for a bug report", supports_color - ), - cls=HelpColorsCommand, -) -def get_system_info(): - """Get CPU, Memory, Disk, Python, & hyperglass version.""" - # Project - from hyperglass.cli.util import system_info - - system_info() - - -@hg.command( - "clear-cache", - help=cmd_help(E.SOAP, "Clear the Redis cache", supports_color), - cls=HelpColorsCommand, -) -def clear_cache(): - """Clear the Redis Cache.""" - # Project - from hyperglass.util import sync_clear_redis_cache - - try: - sync_clear_redis_cache() - success("Cleared Redis Cache") - except RuntimeError as err: - error(str(err)) diff --git a/hyperglass/cli/echo.py b/hyperglass/cli/echo.py index 63d075545..4f7ced0c8 100644 --- a/hyperglass/cli/echo.py +++ b/hyperglass/cli/echo.py @@ -1,142 +1,43 @@ """Helper functions for CLI message printing.""" -# Standard Library -import re -# Third Party -from click import echo, style +# Standard Library +import typing as t # Project -from hyperglass.cli.static import CMD_HELP, Message -from hyperglass.cli.exceptions import CliError - - -def cmd_help(emoji="", help_text="", supports_color=False): - """Print formatted command help.""" - if supports_color: - help_str = emoji + style(help_text, **CMD_HELP) - else: - help_str = help_text - return help_str - - -def _base_formatter(_text, _state, _callback, *args, **kwargs): - """Format text block, replace template strings with keyword arguments. - - Arguments: - state {dict} -- Text format attributes - label {dict} -- Keyword format attributes - text {[type]} -- Text to format - callback {function} -- Callback function - - Returns: - {str|ClickException} -- Formatted output - """ - fmt = Message(_state) - - if _callback is None: - _callback = style - - nargs = () - for i in args: - if not isinstance(i, str): - nargs += (str(i),) - else: - nargs += (i,) - - for k, v in kwargs.items(): - if not isinstance(v, str): - v = str(v) - kwargs[k] = style(v, **fmt.kw) - - text_all = re.split(r"(\{\w+\})", _text) - text_all = [style(i, **fmt.msg) for i in text_all] - text_all = [i.format(*nargs, **kwargs) for i in text_all] - - if fmt.emoji: - text_all.insert(0, fmt.emoji) - - text_fmt = "".join(text_all) - - return _callback(text_fmt) - - -def info(text, *args, **kwargs): - """Generate formatted informational text. - - Arguments: - text {str} -- Text to format - callback {callable} -- Callback function (default: {echo}) - - Returns: - {str} -- Informational output - """ - return _base_formatter(_state="info", _text=text, _callback=echo, *args, **kwargs) - - -def error(text, *args, **kwargs): - """Generate formatted exception. - - Arguments: - text {str} -- Text to format - callback {callable} -- Callback function (default: {echo}) - - Raises: - ClickException: Raised after formatting - """ - raise _base_formatter(text, "error", CliError, *args, **kwargs) - - -def success(text, *args, **kwargs): - """Generate formatted success text. - - Arguments: - text {str} -- Text to format - callback {callable} -- Callback function (default: {echo}) - - Returns: - {str} -- Success output - """ - return _base_formatter( - _state="success", _text=text, _callback=echo, *args, **kwargs - ) - +from hyperglass.log import HyperglassConsole -def warning(text, *args, **kwargs): - """Generate formatted warning text. - Arguments: - text {str} -- Text to format - callback {callable} -- Callback function (default: {echo}) +class Echo: + """Container for console-printing functions.""" - Returns: - {str} -- Warning output - """ - return _base_formatter( - _state="warning", _text=text, _callback=echo, *args, **kwargs - ) + _console = HyperglassConsole + def _fmt(self, message: t.Any, *args: t.Any, **kwargs: t.Any) -> t.Any: + if isinstance(message, str): + args = (f"[bold]{arg}[/bold]" for arg in args) + kwargs = {k: f"[bold]{v}[/bold]" for k, v in kwargs.items()} + return message.format(*args, **kwargs) + return message -def label(text, *args, **kwargs): - """Generate formatted info text with accented labels. + def error(self, message: str, *args, **kwargs): + """Print an error message.""" + return self._console.print(self._fmt(message, *args, **kwargs), style="error") - Arguments: - text {str} -- Text to format - callback {callable} -- Callback function (default: {echo}) + def info(self, message: str, *args, **kwargs): + """Print an informational message.""" + return self._console.print(self._fmt(message, *args, **kwargs), style="info") - Returns: - {str} -- Label output - """ - return _base_formatter(_state="label", _text=text, _callback=echo, *args, **kwargs) + def warning(self, message: str, *args, **kwargs): + """Print a warning message.""" + return self._console.print(self._fmt(message, *args, **kwargs), style="info") + def success(self, message: str, *args, **kwargs): + """Print a success message.""" + return self._console.print(self._fmt(message, *args, **kwargs), style="success") -def status(text, *args, **kwargs): - """Generate formatted status text. + def plain(self, message: str, *args, **kwargs): + """Print an unformatted message.""" + return self._console.print(self._fmt(message, *args, **kwargs)) - Arguments: - text {str} -- Text to format - callback {callable} -- Callback function (default: {echo}) - Returns: - {str} -- Status output - """ - return _base_formatter(_state="status", _text=text, _callback=echo, *args, **kwargs) +echo = Echo() diff --git a/hyperglass/cli/exceptions.py b/hyperglass/cli/exceptions.py deleted file mode 100644 index 3b4480677..000000000 --- a/hyperglass/cli/exceptions.py +++ /dev/null @@ -1,15 +0,0 @@ -"""hyperglass CLI custom exceptions.""" - -# Third Party -from click import ClickException, echo -from click._compat import get_text_stderr - - -class CliError(ClickException): - """Custom exception to exclude the 'Error:' prefix from echos.""" - - def show(self, file=None): - """Exclude 'Error:' prefix from raised exceptions.""" - if file is None: - file = get_text_stderr() - echo(self.format_message()) diff --git a/hyperglass/cli/formatting.py b/hyperglass/cli/formatting.py deleted file mode 100644 index 22b58bdde..000000000 --- a/hyperglass/cli/formatting.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Help formatting. - -https://github.com/click-contrib/click-help-colors -MIT License - -Copyright (c) 2016 Roman Tonkonozhko - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -# Standard Library -import random - -# Third Party -import click - - -def random_colors(*commands): - """From tuple of commands, generate random but unique colors.""" - colors = ["blue", "green", "red", "yellow", "magenta", "cyan", "white"] - num_colors = len(colors) - num_commands = len(commands) - - if num_commands >= num_colors: - colors += colors - - unique_colors = random.sample(colors, num_commands) - commands_fmt = {} - for i, cmd in enumerate(commands): - commands_fmt.update({cmd: {"fg": unique_colors[i], "bold": True}}) - commands_fmt.update({"--help": {"fg": "white"}}) - return commands_fmt - - -class HelpColorsFormatter(click.HelpFormatter): - """Click help formatting plugin. See file docstring for license. - - Modified from original copy to support click.style() instead of - direct ANSII string formatting. - """ - - def __init__( - self, - headers_color=None, - options_color=None, - options_custom_colors=None, - *args, - **kwargs - ): - """Initialize help formatter. - - Keyword Arguments: - headers_color {dict} -- click.style() paramters for header - options_color {dict} -- click.style() paramters for options - options_custom_colors {dict} -- click.style() paramters for options by name - """ - self.headers_color = headers_color or {} - self.options_color = options_color or {} - self.options_custom_colors = options_custom_colors or {} - - super().__init__(indent_increment=3, *args, **kwargs) - - def _pick_color(self, option_name): - """Filter options and pass relevant click.style() options for command.""" - opt = option_name.split()[0].strip(",") - color = {} - if self.options_custom_colors and opt in self.options_custom_colors.keys(): - color = self.options_custom_colors[opt] - else: - color = self.options_color - return color - - def write_usage(self, prog, args="", prefix="Usage: "): - """Write Usage: section.""" - prefix_fmt = click.style(prefix, **self.headers_color) - super().write_usage(prog, args, prefix=prefix_fmt) - - def write_heading(self, heading): - """Write Heading section.""" - heading_fmt = click.style(heading, **self.headers_color) - super().write_heading(heading_fmt) - - def write_dl(self, rows, **kwargs): - """Write Options section.""" - colorized_rows = [ - (click.style(row[0], **self._pick_color(row[0])), row[1]) for row in rows - ] - super().write_dl(colorized_rows, **kwargs) - - -class HelpColorsMixin: - """Click help formatting plugin. See file docstring for license. - - Modified from original copy to support click.style() instead of - direct ANSII string formatting. - """ - - def __init__( - self, - help_headers_color=None, - help_options_color=None, - help_options_custom_colors=None, - *args, - **kwargs - ): - """Initialize help mixin.""" - self.help_headers_color = help_headers_color or {} - self.help_options_color = help_options_color or {} - self.help_options_custom_colors = help_options_custom_colors or {} - super().__init__(*args, **kwargs) - - def get_help(self, ctx): - """Format help.""" - formatter = HelpColorsFormatter( - width=ctx.terminal_width, - max_width=ctx.max_content_width, - headers_color=self.help_headers_color, - options_color=self.help_options_color, - options_custom_colors=self.help_options_custom_colors, - ) - self.format_help(ctx, formatter) - return formatter.getvalue().rstrip("\n") - - -class HelpColorsGroup(HelpColorsMixin, click.Group): - """Click help formatting plugin. See file docstring for license. - - Modified from original copy to support click.style() instead of - direct ANSII string formatting. - """ - - def __init__(self, *args, **kwargs): - """Initialize group formatter.""" - super().__init__(*args, **kwargs) - - def command(self, *args, **kwargs): - """Set command values.""" - kwargs.setdefault("cls", HelpColorsCommand) - kwargs.setdefault("help_headers_color", self.help_headers_color) - kwargs.setdefault("help_options_color", self.help_options_color) - kwargs.setdefault("help_options_custom_colors", self.help_options_custom_colors) - return super().command(*args, **kwargs) - - def group(self, *args, **kwargs): - """Set group values.""" - kwargs.setdefault("cls", HelpColorsGroup) - kwargs.setdefault("help_headers_color", self.help_headers_color) - kwargs.setdefault("help_options_color", self.help_options_color) - kwargs.setdefault("help_options_custom_colors", self.help_options_custom_colors) - return super().group(*args, **kwargs) - - -class HelpColorsCommand(HelpColorsMixin, click.Command): - """Click help formatting plugin. See file docstring for license. - - Modified from original copy to support click.style() instead of - direct ANSII string formatting. - """ - - def __init__(self, *args, **kwargs): - """Initialize command formatter.""" - super().__init__(*args, **kwargs) diff --git a/hyperglass/cli/installer.py b/hyperglass/cli/installer.py index 9ef2f702a..eeb760dd6 100644 --- a/hyperglass/cli/installer.py +++ b/hyperglass/cli/installer.py @@ -2,89 +2,144 @@ # Standard Library import os +import time import shutil +import typing as t +import getpass +from types import TracebackType from filecmp import dircmp from pathlib import Path # Third Party -import inquirer +import typer +from rich.progress import Progress + +# Project +from hyperglass.util import compare_lists +from hyperglass.settings import Settings +from hyperglass.constants import __version__ # Local -from .echo import error, success, warning -from .util import create_dir +from .echo import echo -USER_PATH = Path.home() / "hyperglass" -ROOT_PATH = Path("/etc/hyperglass/") ASSET_DIR = Path(__file__).parent.parent / "images" IGNORED_FILES = [".DS_Store"] -INSTALL_PATHS = [ - inquirer.List( - "install_path", - message="Choose a directory for hyperglass", - choices=[USER_PATH, ROOT_PATH], - ) -] - - -def prompt_for_path() -> str: - """Recursively prompt the user for an app path until one is provided.""" - - answer = inquirer.prompt(INSTALL_PATHS) - - if answer is None: - warning("A directory for hyperglass is required") - answer = prompt_for_path() - - return answer["install_path"] - class Installer: """Install hyperglass.""" - def __init__(self, unattended: bool): - """Initialize installer.""" + app_path: Path + progress: Progress + user: str + assets: int - self.unattended = unattended + def __init__(self): + """Start hyperglass installer.""" + self.app_path = Settings.app_path + self.progress: Progress = Progress(console=echo._console) + self.user = getpass.getuser() + self.assets = len([p for p in ASSET_DIR.iterdir() if p.name not in IGNORED_FILES]) def install(self) -> None: - """Complete the installation.""" - - self.app_path = self._get_app_path() - self._scaffold() - self._migrate_static_assets() - - def _get_app_path(self) -> Path: - """Find the app path from env variables or a prompt.""" - - if self.unattended: - return USER_PATH - - if os.environ.get("HYPERGLASS_PATH"): - return Path(os.environ.get("HYPERGLASS_PATH")) + """Initialize tasks and start installer.""" + permissions_task = self.progress.add_task("[bright purple]Checking System", total=2) + scaffold_task = self.progress.add_task( + "[bright blue]Creating Directory Structures", total=3 + ) + asset_task = self.progress.add_task( + "[bright cyan]Migrating Static Assets", total=self.assets + ) + ui_task = self.progress.add_task("[bright teal]Initialzing UI", total=1, start=False) + + self.progress.start() + + self.check_permissions(task_id=permissions_task) + self.scaffold(task_id=scaffold_task) + self.migrate_static_assets(task_id=asset_task) + self.init_ui(task_id=ui_task) + + def __enter__(self) -> t.Callable[[], None]: + """Initialize tasks.""" + self.progress.print(f"Starting hyperglass {__version__} setup") + return self.install + + def __exit__( + self, + exc_type: t.Optional[t.Type[BaseException]] = None, + exc_value: t.Optional[BaseException] = None, + exc_traceback: t.Optional[TracebackType] = None, + ): + """Print errors on exit.""" + self.progress.stop() + if exc_type is not None: + echo._console.print_exception(show_locals=True) + raise typer.Exit(1) + raise typer.Exit(0) + + def check_permissions(self, task_id: int) -> None: + """Ensure the executing user has permissions to the app path.""" + read = os.access(self.app_path, os.R_OK) + if not read: + self.progress.print( + f"User {self.user!r} does not have read access to {self.app_path!s}", style="error" + ) + raise typer.Exit(1) + + self.progress.advance(task_id) + time.sleep(0.4) + + write = os.access(self.app_path, os.W_OK) + if not write: + self.progress.print( + f"User {self.user!r} does not have write access to {self.app_path!s}", style="error" + ) + raise typer.Exit(1) + self.progress.advance(task_id) + + def scaffold(self, task_id: int) -> None: + """Create the file structure necessary for hyperglass to run.""" - return Path(prompt_for_path()) + if not self.app_path.exists(): + self.progress.print("Created {!s}".format(self.app_path), style="info") + self.app_path.mkdir(parents=True) - def _scaffold(self) -> None: - """Create the file structure necessary for hyperglass to run.""" + self.progress.print(f"hyperglass path is {self.app_path!s}", style="subtle") + self.progress.advance(task_id) ui_dir = self.app_path / "static" / "ui" - images_dir = self.app_path / "static" / "images" - favicon_dir = images_dir / "favicons" - custom_dir = self.app_path / "static" / "custom" + favicon_dir = self.app_path / "static" / "images" / "favicons" - create_dir(self.app_path) + for path in (ui_dir, favicon_dir): + if not path.exists(): + self.progress.print("Created {!s}".format(path), style="info") + path.mkdir(parents=True) - for path in (ui_dir, images_dir, favicon_dir, custom_dir): - create_dir(path, parents=True) + self.progress.advance(task_id) + time.sleep(0.4) - def _migrate_static_assets(self) -> bool: + def migrate_static_assets(self, task_id: int) -> None: """Synchronize the project assets with the installation assets.""" target_dir = self.app_path / "static" / "images" + def copy_func(src: str, dst: str): + time.sleep(self.assets / 10) + + exists = Path(dst).exists() + if not exists: + copied = shutil.copy2(src, dst) + self.progress.print(f"Copied {copied!s}", style="info") + self.progress.advance(task_id) + return dst + if not target_dir.exists(): - shutil.copytree(ASSET_DIR, target_dir) + shutil.copytree( + ASSET_DIR, + target_dir, + ignore=shutil.ignore_patterns(*IGNORED_FILES), + copy_function=copy_func, + ) # Compare the contents of the project's asset directory (considered # the source of truth) with the installation directory. If they do @@ -92,21 +147,41 @@ def _migrate_static_assets(self) -> bool: # re-copy it. compare_initial = dircmp(ASSET_DIR, target_dir, ignore=IGNORED_FILES) - if not compare_initial.left_list == compare_initial.right_list: + if not compare_lists( + compare_initial.left_list, + compare_initial.right_list, + ignore=["hyperglass-opengraph.jpg"], + ): shutil.rmtree(target_dir) - shutil.copytree(ASSET_DIR, target_dir) + shutil.copytree( + ASSET_DIR, + target_dir, + copy_function=copy_func, + ignore=shutil.ignore_patterns(*IGNORED_FILES), + ) # Re-compare the source and destination directory contents to # ensure they match. compare_post = dircmp(ASSET_DIR, target_dir, ignore=IGNORED_FILES) - if not compare_post.left_list == compare_post.right_list: - error( - "Files in {a} do not match files in {b}", - a=str(ASSET_DIR), - b=str(target_dir), - ) - return False - - success("Migrated assets from {a} to {b}", a=str(ASSET_DIR), b=str(target_dir)) - return True + if not compare_lists( + compare_post.left_list, compare_post.right_list, ignore=["hyperglass-opengraph.jpg"] + ): + echo.error("Files in {!s} do not match files in {!s}", ASSET_DIR, target_dir) + raise typer.Exit(1) + else: + self.progress.update(task_id, completed=self.assets, refresh=True) + + def init_ui(self, task_id: int) -> None: + """Initialize UI.""" + # Project + from hyperglass.log import log + + # Local + from .util import build_ui + + with self.progress.console.capture(): + log.disable("hyperglass") + build_ui(timeout=180) + log.enable("hyperglass") + self.progress.advance(task_id) diff --git a/hyperglass/cli/main.py b/hyperglass/cli/main.py new file mode 100644 index 000000000..7cc06f601 --- /dev/null +++ b/hyperglass/cli/main.py @@ -0,0 +1,348 @@ +"""hyperglass Command Line Interface.""" + +# Standard Library +import re +import sys +import typing as t + +# Third Party +import typer + +# Local +from .echo import echo + + +def _version(value: bool) -> None: + # Project + from hyperglass import __version__ + + if value: + echo.info(__version__) + raise typer.Exit() + + +cli = typer.Typer(name="hyperglass", help="hyperglass Command Line Interface", no_args_is_help=True) + + +def run(): + """Run the hyperglass CLI.""" + return typer.run(cli()) + + +@cli.callback(name="version") +def _version( + version: t.Optional[bool] = typer.Option( + None, "--version", help="hyperglass version", callback=_version + ), +) -> None: + """hyperglass""" + pass + + +@cli.command(name="start") +def _start(build: bool = False, workers: t.Optional[int] = None) -> None: + """Start hyperglass""" + # Project + from hyperglass.main import run + + # Local + from .util import build_ui + + kwargs = {} + if workers != 0: + kwargs["workers"] = workers + + try: + if build: + build_complete = build_ui(timeout=180) + if build_complete: + run(workers) + else: + run(workers) + + except (KeyboardInterrupt, SystemExit) as err: + error_message = str(err) + if (len(error_message)) > 1: + echo.warning(str(err)) + echo.error("Stopping hyperglass due to keyboard interrupt.") + raise typer.Exit(0) + + +@cli.command(name="build-ui") +def _build_ui(timeout: int = typer.Option(180, help="Timeout in seconds")) -> None: + """Create a new UI build.""" + # Local + from .util import build_ui as _build_ui + + with echo._console.status( + f"Starting new UI build with a {timeout} second timeout...", spinner="aesthetic" + ): + _build_ui(timeout=120) + + +@cli.command(name="system-info") +def _system_info(): + """Get system information for a bug report""" + # Third Party + from rich import box + from rich.panel import Panel + from rich.table import Table + + # Project + from hyperglass.util.system_info import get_system_info + + # Local + from .static import MD_BOX + + data = get_system_info() + + rows = tuple( + (f"**{title}**", f"`{value!s}`" if mod == "code" else str(value)) + for title, (value, mod) in data.items() + ) + + table = Table("Metric", "Value", box=MD_BOX) + for title, metric in rows: + table.add_row(title, metric) + echo._console.print( + Panel( + "Please copy & paste this table in your bug report", + style="bold yellow", + expand=False, + border_style="yellow", + box=box.HEAVY, + ) + ) + echo.plain(table) + + +@cli.command(name="clear-cache") +def _clear_cache(): + """Clear the Redis cache""" + # Project + from hyperglass.state import use_state + + state = use_state() + + try: + state.clear() + echo.success("Cleared Redis Cache") + + except Exception as err: + if not sys.stdout.isatty(): + echo._console.print_exception(show_locals=True) + raise typer.Exit(1) + + echo.error("Error clearing cache: {!s}", err) + raise typer.Exit(1) + + +@cli.command(name="devices") +def _devices( + search: t.Optional[str] = typer.Argument(None, help="Device ID or Name Search Pattern"), +): + """Show all configured devices""" + # Third Party + from rich.columns import Columns + from rich._inspect import Inspect + + # Project + from hyperglass.state import use_state + + devices = use_state("devices") + if search is not None: + pattern = re.compile(search, re.IGNORECASE) + for device in devices: + if pattern.match(device.id) or pattern.match(device.name): + echo._console.print( + Inspect( + device, + title=device.name, + docs=False, + methods=False, + dunder=False, + sort=True, + all=False, + value=True, + help=False, + ) + ) + raise typer.Exit(0) + + panels = [ + Inspect( + device, + title=device.name, + docs=False, + methods=False, + dunder=False, + sort=True, + all=False, + value=True, + help=False, + ) + for device in devices + ] + echo._console.print(Columns(panels)) + + +@cli.command(name="directives") +def _directives( + search: t.Optional[str] = typer.Argument(None, help="Directive ID or Name Search Pattern"), +): + """Show all configured devices""" + # Third Party + from rich.columns import Columns + from rich._inspect import Inspect + + # Project + from hyperglass.state import use_state + + directives = use_state("directives") + if search is not None: + pattern = re.compile(search, re.IGNORECASE) + for directive in directives: + if pattern.match(directive.id) or pattern.match(directive.name): + echo._console.print( + Inspect( + directive, + title=directive.name, + docs=False, + methods=False, + dunder=False, + sort=True, + all=False, + value=True, + help=False, + ) + ) + raise typer.Exit(0) + + panels = [ + Inspect( + directive, + title=directive.name, + docs=False, + methods=False, + dunder=False, + sort=True, + all=False, + value=True, + help=False, + ) + for directive in directives + ] + echo._console.print(Columns(panels)) + + +@cli.command(name="plugins") +def _plugins( + search: t.Optional[str] = typer.Argument(None, help="Plugin ID or Name Search Pattern"), + _input: bool = typer.Option( + False, "--input", show_default=False, is_flag=True, help="Show Input Plugins" + ), + output: bool = typer.Option( + False, "--output", show_default=False, is_flag=True, help="Show Output Plugins" + ), +): + """Show all configured devices""" + # Third Party + from rich.columns import Columns + + # Project + from hyperglass.state import use_state + + to_fetch = ("input", "output") + if _input is True: + to_fetch = ("input",) + + elif output is True: + to_fetch = ("output",) + + state = use_state() + all_plugins = [plugin for _type in to_fetch for plugin in state.plugins(_type)] + + if search is not None: + pattern = re.compile(search, re.IGNORECASE) + matching = [plugin for plugin in all_plugins if pattern.match(plugin.name)] + if len(matching) == 0: + echo.error(f"No plugins matching {search!r}") + raise typer.Exit(1) + + echo._console.print(Columns(matching)) + raise typer.Exit(0) + + echo._console.print(Columns(all_plugins)) + + +@cli.command(name="params") +def _params( + path: t.Optional[str] = typer.Argument( + None, help="Parameter Object Path, for example 'messages.no_input'" + ), +): + """Show configuration parameters""" + # Standard Library + from operator import attrgetter + + # Third Party + from rich._inspect import Inspect + + # Project + from hyperglass.state import use_state + + params = use_state("params") + if path is not None: + try: + value = attrgetter(path)(params) + + echo._console.print( + Inspect( + value, + title=f"params.{path}", + docs=False, + methods=False, + dunder=False, + sort=True, + all=False, + value=True, + help=False, + ) + ) + raise typer.Exit(0) + except AttributeError: + echo.error(f"{'params.' + path!r} does not exist") + raise typer.Exit(1) + + panel = Inspect( + params, + title="hyperglass Configuration Parameters", + docs=False, + methods=False, + dunder=False, + sort=True, + all=False, + value=True, + help=False, + ) + echo._console.print(panel) + + +@cli.command(name="setup") +def _setup(): + """Initialize hyperglass setup.""" + # Local + from .installer import Installer + + with Installer() as start: + start() + + +@cli.command(name="settings") +def _settings(): + """Show hyperglass system settings (environment variables)""" + + # Project + from hyperglass.settings import Settings + + echo.plain(Settings) diff --git a/hyperglass/cli/static.py b/hyperglass/cli/static.py index 624c511ae..d6b9c8225 100644 --- a/hyperglass/cli/static.py +++ b/hyperglass/cli/static.py @@ -1,6 +1,21 @@ """Static string definitions.""" + # Third Party -import click +from rich.box import Box + +MD_BOX = Box( + """\ + +| || +|-|| +| || +| | +| | +| || + +""", + ascii=True, +) class Char: @@ -27,101 +42,6 @@ def __add__(self, other): return str(self.char) + str(other) -class Emoji: - """Helper class for unicode emoji.""" - - BUTTERFLY = "\U0001F98B " - CHECK = "\U00002705 " - INFO = "\U00002755 " - ERROR = "\U0000274C " - WARNING = "\U000026A0\U0000FE0F " - TOOLBOX = "\U0001F9F0 " - NUMBERS = "\U0001F522 " - FOLDED_HANDS = "\U0001F64F " - ROCKET = "\U0001F680 " - SPARKLES = "\U00002728 " - PAPERCLIP = "\U0001F4CE " - KEY = "\U0001F511 " - LOCK = "\U0001F512 " - CLAMP = "\U0001F5DC " - BOOKS = "\U0001F4DA " - THERMOMETER = "\U0001F321 " - SOAP = "\U0001F9FC " - - WS = Char(" ") NL = Char("\n") CL = Char(":") -E = Emoji() - -CLI_HELP = ( - click.style("hyperglass", fg="magenta", bold=True) - + WS[1] - + click.style("Command Line Interface", fg="white") -) - -# Click Style Helpers -SUCCESS = {"fg": "green", "bold": True} -WARNING = {"fg": "yellow"} -ERROR = {"fg": "red", "bold": True} -LABEL = {"fg": "white"} -INFO = {"fg": "blue", "bold": True} -STATUS = {"fg": "black"} -VALUE = {"fg": "magenta", "bold": True} -CMD_HELP = {"fg": "white"} - - -class Message: - """Helper class for single-character strings.""" - - colors = { - "warning": "yellow", - "success": "green", - "error": "red", - "info": "blue", - "status": "black", - "label": "white", - } - label_colors = { - "warning": "yellow", - "success": "green", - "error": "red", - "info": "blue", - "status": "black", - "label": "magenta", - } - emojis = { - "warning": E.WARNING, - "success": E.CHECK, - "error": E.ERROR, - "info": E.INFO, - "status": "", - "label": "", - } - - def __init__(self, state): - """Set instance character.""" - self.state = state - self.color = self.colors[self.state] - self.label_color = self.label_colors[self.state] - - @property - def msg(self): - """Click style attributes for message text.""" - return {"fg": self.color} - - @property - def kw(self): - """Click style attributes for keywords.""" - return {"fg": self.label_color, "bold": True, "underline": True} - - @property - def emoji(self): - """Match emoji from state.""" - return self.emojis[self.state] - - def __repr__(self): - """Stringify the instance character for representation.""" - return "Message(msg={m}, kw={k}, emoji={e})".format( - m=self.msg, k=self.kw, e=self.emoji - ) diff --git a/hyperglass/cli/util.py b/hyperglass/cli/util.py index 1a6759ae7..4dbb6cf8d 100644 --- a/hyperglass/cli/util.py +++ b/hyperglass/cli/util.py @@ -1,181 +1,51 @@ """CLI utility functions.""" # Standard Library -import os -from pathlib import Path +import sys +import asyncio # Third Party -from click import echo, style +import typer -# Project -from hyperglass.cli.echo import info, error, status, success -from hyperglass.cli.static import CL, NL, WS, E - -PROJECT_ROOT = Path(__file__).parent.parent - - -def async_command(func) -> None: - """Decororator for to make async functions runable from synchronous code.""" - # Standard Library - import asyncio - from functools import update_wrapper - - func = asyncio.coroutine(func) - - def wrapper(*args, **kwargs): - loop = asyncio.get_event_loop() - return loop.run_until_complete(func(*args, **kwargs)) - - return update_wrapper(wrapper, func) - - -def start_web_server(start, params): - """Start web server.""" - msg_start = "Starting hyperglass web server on" - msg_uri = "http://" - msg_host = str(params["host"]) - msg_port = str(params["port"]) - msg_len = len("".join([msg_start, WS[1], msg_uri, msg_host, CL[1], msg_port])) - try: - echo( - NL[1] - + WS[msg_len + 8] - + E.ROCKET - + NL[1] - + E.CHECK - + style(msg_start, fg="green", bold=True) - + WS[1] - + style(msg_uri, fg="white") - + style(msg_host, fg="blue", bold=True) - + style(CL[1], fg="white") - + style(msg_port, fg="magenta", bold=True) - + WS[1] - + E.ROCKET - + NL[1] - + WS[1] - + NL[1] - ) - start() - - except Exception as e: - error("Failed to start web server: {e}", e=e) +# Local +from .echo import echo def build_ui(timeout: int) -> None: """Create a new UI build.""" - try: - # Project - from hyperglass.configuration import CONFIG_PATH, params, frontend_params - from hyperglass.util.frontend import build_frontend - from hyperglass.compat._asyncio import aiorun - except ImportError as e: - error("Error importing UI builder: {e}", e=e) + # Project + from hyperglass.state import use_state + from hyperglass.frontend import build_frontend + from hyperglass.configuration import init_user_config - status("Starting new UI build with a {t} second timeout...", t=timeout) + # Populate configuration to Redis prior to accessing it. + init_user_config() - if params.developer_mode: + state = use_state() + + dev_mode = "production" + if state.settings.dev_mode: dev_mode = "development" - else: - dev_mode = "production" try: - build_success = aiorun( + build_success = asyncio.run( build_frontend( - dev_mode=params.developer_mode, - dev_url=f"http://localhost:{str(params.listen_port)}/", - prod_url="/api/", - params=frontend_params, + app_path=state.settings.app_path, + dev_mode=state.settings.dev_mode, + dev_url=f"http://localhost:{state.settings.port!s}/", force=True, - app_path=CONFIG_PATH, + params=state.ui_params, + prod_url="/api/", + timeout=timeout, ) ) if build_success: - success("Completed UI build in {m} mode", m=dev_mode) + echo.success("Completed UI build in {} mode", dev_mode) except Exception as e: - error("Error building UI: {e}", e=e) - - return True - - -def create_dir(path, **kwargs) -> bool: - """Validate and attempt to create a directory, if it does not exist.""" - - # If input path is not a path object, try to make it one - if not isinstance(path, Path): - try: - path = Path(path) - except TypeError: - error("{p} is not a valid path", p=path) - - # If path does not exist, try to create it - if not path.exists(): - try: - path.mkdir(**kwargs) - except PermissionError: - error( - "{u} does not have permission to create {p}. Try running with sudo?", - u=os.getlogin(), - p=path, - ) - - # Verify the path was actually created - if path.exists(): - success("Created {p}", p=path) - - # If the path already exists, inform the user - elif path.exists(): - info("{p} already exists", p=path) - - return True - - -def write_to_file(file, data) -> bool: - """Write string data to a file.""" - try: - with file.open("w+") as f: - f.write(data.strip()) - except PermissionError: - error( - "{u} does not have permission to write to {f}. Try running with sudo?", - u=os.getlogin(), - f=file, - ) - if not file.exists(): - error("Error writing file {f}", f=file) - elif file.exists(): - success("Wrote systemd file {f}", f=file) - return True - - -def system_info() -> None: - """Create a markdown table of various system information.""" - # Project - from hyperglass.util.system_info import get_system_info - - data = get_system_info() - - def _code(val): - return f"`{str(val)}`" - - def _bold(val): - return f"**{str(val)}**" - - md_table_lines = ("| Metric | Value |", "| :----- | :---- |") - - for title, metric in data.items(): - value, mod = metric - - title = _bold(title) - - if mod == "code": - value = _code(value) - - md_table_lines += (f"| {title} | {value} |",) - - md_table = "\n".join(md_table_lines) - - info("Please copy & paste this table in your bug report:\n") - echo(md_table + "\n") + if not sys.stdout.isatty(): + echo._console.print_exception(show_locals=True) + raise typer.Exit(1) - return None + echo.error("Error building UI: {!s}", e) + raise typer.Exit(1) diff --git a/hyperglass/compat/__init__.py b/hyperglass/compat/__init__.py index 0352479d7..e498f61ed 100644 --- a/hyperglass/compat/__init__.py +++ b/hyperglass/compat/__init__.py @@ -1 +1,10 @@ -"""Functions for maintaining compatability with older Python versions or libraries.""" +"""Functions for maintaining compatibility with older Python versions or libraries.""" + +# Local +from ._sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError, open_tunnel + +__all__ = ( + "BaseSSHTunnelForwarderError", + "open_tunnel", + "SSHTunnelForwarder", +) diff --git a/hyperglass/compat/_asyncio.py b/hyperglass/compat/_asyncio.py deleted file mode 100644 index 8cfeed99d..000000000 --- a/hyperglass/compat/_asyncio.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Functions for maintaining asyncio compatability with other versions of Python.""" - -# Standard Library -import sys -import asyncio -import weakref - -try: - # Standard Library - from asyncio import get_running_loop -except ImportError: - # Standard Library - from asyncio.events import _get_running_loop as get_running_loop - -RUNNING_PYTHON_VERSION = sys.version_info - -# _patch_loop, _patched_run, and _cancel_all_tasks are taken directly -# from github.com/nickdavis: -# https://gist.github.com/nickdavies/4a37c6cd9dcc7041fddd2d2a81cee383 - -# These functions are a backport of the functionality added in -# Python 3.7 to support asyncio.run(), which is used in several areas -# of hyperglass. Because the LTS version of Ubuntu at this time (18.04) -# still ships with Python 3.6, compatibility with Python 3.6 is the -# goal. - - -def _patch_loop(loop): - tasks = weakref.WeakSet() - - task_factory = [None] - - def _set_task_factory(factory): - task_factory[0] = factory - - def _get_task_factory(): - return task_factory[0] - - def _safe_task_factory(loop, coro): - if task_factory[0] is None: - task = asyncio.Task(coro, loop=loop) - if task._source_traceback: - del task._source_traceback[-1] - else: - task = task_factory[0](loop, coro) - tasks.add(task) - return task - - loop.set_task_factory(_safe_task_factory) - loop.set_task_factory = _set_task_factory - loop.get_task_factory = _get_task_factory - - return tasks - - -def _cancel_all_tasks(loop, tasks): - to_cancel = [task for task in tasks if not task.done()] - - if not to_cancel: - return - - for task in to_cancel: - task.cancel() - - loop.run_until_complete( - asyncio.gather(*to_cancel, loop=loop, return_exceptions=True) - ) - - for task in to_cancel: - if task.cancelled(): - continue - if task.exception() is not None: - loop.call_exception_handler( - { - "message": "unhandled exception during asyncio.run() shutdown", - "exception": task.exception(), - "task": task, - } - ) - - -def _patched_run(main, *, debug=False): - try: - loop = get_running_loop() - except RuntimeError: - loop = None - - if loop is not None: - raise RuntimeError("asyncio.run() cannot be called from a running event loop") - - if not asyncio.iscoroutine(main): - raise ValueError("a coroutine was expected, got {!r}".format(main)) - - loop = asyncio.new_event_loop() - tasks = _patch_loop(loop) - - try: - asyncio.set_event_loop(loop) - loop.set_debug(debug) - return loop.run_until_complete(main) - finally: - try: - _cancel_all_tasks(loop, tasks) - loop.run_until_complete(loop.shutdown_asyncgens()) - finally: - asyncio.set_event_loop(None) - loop.close() - - -# If local system's python version is at least 3.6, use the backported -# asyncio runner. -if RUNNING_PYTHON_VERSION >= (3, 6): - aiorun = _patched_run - -# If the local system's python version is at least 3.7, use the standard -# library's asyncio.run() -elif RUNNING_PYTHON_VERSION >= (3, 7): - aiorun = asyncio.run diff --git a/hyperglass/compat/_sshtunnel.py b/hyperglass/compat/_sshtunnel.py index 59ff3a6dd..ca6547f70 100644 --- a/hyperglass/compat/_sshtunnel.py +++ b/hyperglass/compat/_sshtunnel.py @@ -35,7 +35,6 @@ import queue import socket import getpass -import logging import argparse import warnings import threading @@ -48,12 +47,6 @@ # Project from hyperglass.log import log -from hyperglass.configuration import params - -if params.debug: - logging.getLogger("paramiko").setLevel(logging.DEBUG) - -log.bind(logger_name="paramiko") TUNNEL_TIMEOUT = 1.0 #: Timeout (seconds) for tunnel connection _DAEMON = False #: Use daemon threads in connections @@ -117,12 +110,8 @@ def check_address(address): elif isinstance(address, str): if os.name != "posix": raise ValueError("Platform does not support UNIX domain sockets") - if not ( - os.path.exists(address) or os.access(os.path.dirname(address), os.W_OK) - ): - raise ValueError( - "ADDRESS not a valid socket domain socket ({0})".format(address) - ) + if not (os.path.exists(address) or os.access(os.path.dirname(address), os.W_OK)): + raise ValueError("ADDRESS not a valid socket domain socket ({0})".format(address)) else: raise ValueError( "ADDRESS is not a tuple, string, or character buffer " @@ -179,7 +168,7 @@ def get_connection_id(): def _remove_none_values(dictionary): - """ Remove dictionary keys whose value is None.""" + """Remove dictionary keys whose value is None.""" return list(map(dictionary.pop, [i for i in dictionary if dictionary[i] is None])) @@ -191,7 +180,7 @@ def _remove_none_values(dictionary): class BaseSSHTunnelForwarderError(Exception): - """ Exception raised by :class:`SSHTunnelForwarder` errors """ + """Exception raised by :class:`SSHTunnelForwarder` errors""" def __init__(self, *args, **kwargs): self.value = kwargs.pop("value", args[0] if args else "") @@ -201,7 +190,7 @@ def __str__(self): class HandlerSSHTunnelForwarderError(BaseSSHTunnelForwarderError): - """ Exception for Tunnel forwarder errors """ + """Exception for Tunnel forwarder errors""" pass @@ -214,7 +203,7 @@ class HandlerSSHTunnelForwarderError(BaseSSHTunnelForwarderError): class _ForwardHandler(socketserver.BaseRequestHandler): - """ Base handler for tunnel connections """ + """Base handler for tunnel connections""" remote_address = None ssh_transport = None @@ -228,26 +217,16 @@ def _redirect(self, chan): data = self.request.recv(1024) if not data: break - self.logger.trace( - ">>> OUT {0} send to {1}: {2} >>>".format( - self.info, self.remote_address, hexlify(data) - ), - ) chan.sendall(data) if chan in rqst: # else if not chan.recv_ready(): break data = chan.recv(1024) - self.logger.trace( - "<<< IN {0} recv: {1} <<<".format(self.info, hexlify(data)), - ) self.request.sendall(data) def handle(self): uid = get_connection_id() - self.info = "#{0} <-- {1}".format( - uid, self.client_address or self.server.local_address - ) + self.info = "#{0} <-- {1}".format(uid, self.client_address or self.server.local_address) src_address = self.request.getpeername() if not isinstance(src_address, tuple): src_address = ("dummy", 12345) @@ -261,13 +240,9 @@ def handle(self): except paramiko.SSHException: chan = None if chan is None: - msg = "{0} to {1} was rejected by the SSH server".format( - self.info, self.remote_address - ) - self.logger.trace(msg) + msg = "{0} to {1} was rejected by the SSH server".format(self.info, self.remote_address) raise HandlerSSHTunnelForwarderError(msg) - self.logger.trace("{0} connected".format(self.info)) try: self._redirect(chan) except socket.error: @@ -275,13 +250,12 @@ def handle(self): # exception. It was seen that a 3way FIN is processed later on, so # no need to make an ordered close of the connection here or raise # the exception beyond this point... - self.logger.trace("{0} sending RST".format(self.info)) - except Exception as e: - self.logger.trace("{0} error: {1}".format(self.info, repr(e))) + pass + except Exception: + pass finally: chan.close() self.request.close() - self.logger.trace("{0} connection closed.".format(self.info)) class _ForwardServer(socketserver.TCPServer): # Not Threading @@ -298,10 +272,7 @@ def __init__(self, *args, **kwargs): def handle_error(self, request, client_address): (exc_class, exc, tb) = sys.exc_info() - self.logger.error( - "Could not establish connection from {0} to remote " "side of the tunnel", - request.getsockname(), - ) + self.logger.bind(source=request.getsockname()).error("Could not establish connection to remote side of the tunnel") self.tunnel_ok.put(False) @property @@ -373,9 +344,7 @@ def remote_port(self): return self.RequestHandlerClass.remote_address[1] -class _ThreadingUnixStreamForwardServer( - socketserver.ThreadingMixIn, _UnixStreamForwardServer -): +class _ThreadingUnixStreamForwardServer(socketserver.ThreadingMixIn, _UnixStreamForwardServer): """ Allow concurrent connections to each tunnel """ @@ -693,11 +662,7 @@ def _make_ssh_forward_server_class(self, remote_address_): return _ThreadingForwardServer if self._threaded else _ForwardServer def _make_unix_ssh_forward_server_class(self, remote_address_): - return ( - _ThreadingUnixStreamForwardServer - if self._threaded - else _UnixStreamForwardServer - ) + return _ThreadingUnixStreamForwardServer if self._threaded else _UnixStreamForwardServer def _make_ssh_forward_server(self, remote_address, local_bind_address): """ @@ -711,7 +676,9 @@ def _make_ssh_forward_server(self, remote_address, local_bind_address): forward_maker_class = self._make_ssh_forward_server_class _Server = forward_maker_class(remote_address) ssh_forward_server = _Server( - local_bind_address, _Handler, logger=self.logger, + local_bind_address, + _Handler, + logger=self.logger, ) if ssh_forward_server: @@ -762,7 +729,7 @@ def __init__( gateway_timeout=None, *args, **kwargs, # for backwards compatibility - ): + ) -> None: self.logger = logger or log self.ssh_host_key = ssh_host_key self.set_keepalive = set_keepalive @@ -802,9 +769,7 @@ def __init__( ) # local binds self._local_binds = self._get_binds(local_bind_address, local_bind_addresses) - self._local_binds = self._consolidate_binds( - self._local_binds, self._remote_binds - ) + self._local_binds = self._consolidate_binds(self._local_binds, self._remote_binds) ( self.ssh_host, @@ -835,16 +800,13 @@ def __init__( check_host(self.ssh_host) check_port(self.ssh_port) - - self.logger.info( - "Connecting to gateway: {h}:{p} as user '{u}', timeout {t}", - h=self.ssh_host, - p=self.ssh_port, - u=self.ssh_username, - t=self.gateway_timeout, - ) - - self.logger.debug("Concurrent connections allowed: {0}", self._threaded) + self.logger.bind( + host=self.ssh_host, + port=self.ssh_port, + username=self.ssh_username, + timeout=self.gateway_timeout, + ).info("Connecting to gateway") + self.logger.bind(count=self._threaded).debug("Concurrent connections allowed") @staticmethod def _read_ssh_config( @@ -882,16 +844,12 @@ def _read_ssh_config( ssh_port = ssh_port or hostname_info.get("port") proxycommand = hostname_info.get("proxycommand") - ssh_proxy = ssh_proxy or ( - paramiko.ProxyCommand(proxycommand) if proxycommand else None - ) + ssh_proxy = ssh_proxy or (paramiko.ProxyCommand(proxycommand) if proxycommand else None) if compression is None: compression = hostname_info.get("compression", "") compression = True if compression.upper() == "YES" else False except IOError: - logger.warning( - "Could not read SSH configuration file: {f}", f=ssh_config_file - ) + logger.warning("Could not read SSH configuration file: {f}", f=ssh_config_file) except (AttributeError, TypeError): # ssh_config_file is None logger.info("Skipping loading of ssh configuration file") finally: @@ -979,8 +937,7 @@ def _consolidate_binds(local_binds, remote_binds): count = len(remote_binds) - len(local_binds) if count < 0: raise ValueError( - "Too many local bind addresses " - "(local_bind_addresses > remote_bind_addresses)" + "Too many local bind addresses " "(local_bind_addresses > remote_bind_addresses)" ) local_binds.extend([("0.0.0.0", 0) for x in range(count)]) return local_binds @@ -1058,9 +1015,7 @@ def _create_tunnels(self): try: self._connect_to_gateway() except socket.gaierror: # raised by paramiko.Transport - msg = "Could not resolve IP address for {0}, aborting!".format( - self.ssh_host - ) + msg = "Could not resolve IP address for {0}, aborting!".format(self.ssh_host) self.logger.error(msg) return except (paramiko.SSHException, socket.error) as e: @@ -1109,9 +1064,7 @@ def _process_deprecated(attrib, deprecated_attrib, kwargs): """Processes optional deprecate arguments.""" if deprecated_attrib not in DEPRECATIONS: - raise ValueError( - "{0} not included in deprecations list".format(deprecated_attrib) - ) + raise ValueError("{0} not included in deprecations list".format(deprecated_attrib)) if deprecated_attrib in kwargs: warnings.warn( "'{0}' is DEPRECATED use '{1}' instead".format( @@ -1156,9 +1109,7 @@ def read_private_key_file(pkey_file, pkey_password=None, key_type=None, logger=l ) ): try: - ssh_pkey = pkey_class.from_private_key_file( - pkey_file, password=pkey_password - ) + ssh_pkey = pkey_class.from_private_key_file(pkey_file, password=pkey_password) logger.debug( "Private key file ({k0}, {k1}) successfully loaded", @@ -1181,13 +1132,12 @@ def read_private_key_file(pkey_file, pkey_password=None, key_type=None, logger=l return ssh_pkey - def _check_tunnel(self, _srv): + def _check_tunnel(self, _srv) -> None: """Check if tunnel is already established.""" if self.skip_tunnel_checkup: self.tunnel_is_up[_srv.local_address] = True return - - self.logger.info("Checking tunnel to: {a}", a=_srv.remote_address) + self.logger.debug("Checking tunnel", address=_srv.remote_address) if isinstance(_srv.local_address, str): # UNIX stream s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -1202,29 +1152,27 @@ def _check_tunnel(self, _srv): else _srv.local_address ) s.connect(connect_to) - self.tunnel_is_up[_srv.local_address] = _srv.tunnel_ok.get( - timeout=TUNNEL_TIMEOUT * 1.1 - ) - self.logger.debug("Tunnel to {0} is DOWN".format(_srv.remote_address)) + self.tunnel_is_up[_srv.local_address] = _srv.tunnel_ok.get(timeout=TUNNEL_TIMEOUT * 1.1) + self.logger.bind(status="DOWN", address=_srv.remote_address).debug("Tunnel Status") except socket.error: - self.logger.debug("Tunnel to {0} is DOWN".format(_srv.remote_address)) + self.logger.bind(status="DOWN", address=_srv.remote_address).debug("Tunnel Status") self.tunnel_is_up[_srv.local_address] = False except queue.Empty: - self.logger.debug("Tunnel to {0} is UP".format(_srv.remote_address)) + self.logger.bind(status="UP", address=_srv.remote_address).debug("Tunnel Status") self.tunnel_is_up[_srv.local_address] = True finally: s.close() - def check_tunnels(self): + def check_tunnels(self) -> None: """Check that if all tunnels are established and populates. - + :attr:`.tunnel_is_up` """ for _srv in self._server_list: self._check_tunnel(_srv) - def start(self): + def start(self) -> None: """Start the SSH tunnels.""" if self.is_alive: self.logger.warning("Already started!") @@ -1251,7 +1199,7 @@ def start(self): "An error occurred while opening tunnels.", ) - def stop(self): + def stop(self) -> None: """Shut the tunnel down. .. note:: This **had** to be handled with care before ``0.1.0``: @@ -1270,24 +1218,23 @@ def stop(self): """ self.logger.info("Closing all open connections...") opened_address_text = ( - ", ".join((address_to_str(k.local_address) for k in self._server_list)) - or "None" + ", ".join((address_to_str(k.local_address) for k in self._server_list)) or "None" ) self.logger.debug("Listening tunnels: " + opened_address_text) self._stop_transport() self._server_list = [] # reset server list self.tunnel_is_up = {} # reset tunnel status - def close(self): + def close(self) -> None: """Stop the an active tunnel, alias to :meth:`.stop`.""" self.stop() - def restart(self): + def restart(self) -> None: """Restart connection to the gateway and tunnels.""" self.stop() self.start() - def _connect_to_gateway(self): + def _connect_to_gateway(self) -> None: """Open connection to SSH gateway. - First try with all keys loaded from an SSH agent (if allowed) @@ -1311,9 +1258,7 @@ def _connect_to_gateway(self): if self.ssh_password: # avoid conflict using both pass and pkey self.logger.debug( - "Trying to log in with password: {0}".format( - "*" * len(self.ssh_password) - ) + "Trying to log in with password: {0}".format("*" * len(self.ssh_password)) ) try: self._transport = self._get_transport() @@ -1330,7 +1275,7 @@ def _connect_to_gateway(self): self.logger.error("Could not open connection to gateway") - def _serve_forever_wrapper(self, _srv, poll_interval=0.1): + def _serve_forever_wrapper(self, _srv, poll_interval=0.1) -> None: """Wrapper for the server created for a SSH forward.""" self.logger.info( "Opening tunnel: {0} <> {1}".format( @@ -1345,7 +1290,7 @@ def _serve_forever_wrapper(self, _srv, poll_interval=0.1): ) ) - def _stop_transport(self): + def _stop_transport(self) -> None: """Close the underlying transport when nothing more is needed.""" try: @@ -1364,9 +1309,7 @@ def _stop_transport(self): os.unlink(_srv.local_address) except Exception as e: self.logger.error( - "Unable to unlink socket {0}: {1}".format( - self.local_address, repr(e) - ) + "Unable to unlink socket {0}: {1}".format(self.local_address, repr(e)) ) self.is_alive = False if self.is_active: @@ -1413,9 +1356,7 @@ def local_bind_ports(self): self._check_is_started() return [ - _server.local_port - for _server in self._server_list - if _server.local_port is not None + _server.local_port for _server in self._server_list if _server.local_port is not None ] @property @@ -1423,9 +1364,7 @@ def local_bind_hosts(self): """Return a list containing the IP addresses listening for the tunnels.""" self._check_is_started() return [ - _server.local_host - for _server in self._server_list - if _server.local_host is not None + _server.local_host for _server in self._server_list if _server.local_host is not None ] @property @@ -1444,13 +1383,13 @@ def tunnel_bindings(self): ) @property - def is_active(self): - """ Return True if the underlying SSH transport is up """ + def is_active(self) -> bool: + """Return True if the underlying SSH transport is up""" if "_transport" in self.__dict__ and self._transport.is_active(): return True return False - def _check_is_started(self): + def _check_is_started(self) -> None: if not self.is_active: # underlying transport not alive msg = "Server is not started. Please .start() first!" raise BaseSSHTunnelForwarderError(msg) @@ -1458,13 +1397,10 @@ def _check_is_started(self): msg = "Tunnels are not started. Please .start() first!" raise HandlerSSHTunnelForwarderError(msg) - def __str__(self): + def __str__(self) -> str: credentials = { "password": self.ssh_password, - "pkeys": [ - (key.get_name(), hexlify(key.get_fingerprint())) - for key in self.ssh_pkeys - ] + "pkeys": [(key.get_name(), hexlify(key.get_fingerprint())) for key in self.ssh_pkeys] if any(self.ssh_pkeys) else None, } @@ -1496,9 +1432,7 @@ def __str__(self): credentials, self.ssh_host_key if self.ssh_host_key else "not checked", "" if self.is_alive else "not ", - "disabled" - if not self.set_keepalive - else "every {0} sec".format(self.set_keepalive), + "disabled" if not self.set_keepalive else "every {0} sec".format(self.set_keepalive), "disabled" if self.skip_tunnel_checkup else "enabled", "" if self._threaded else "not ", "" if self.compression else "not ", @@ -1507,21 +1441,21 @@ def __str__(self): self._remote_binds, ) - def __repr__(self): + def __repr__(self) -> str: return self.__str__() - def __enter__(self): + def __enter__(self) -> "SSHTunnelForwarder": try: self.start() return self except KeyboardInterrupt: self.__exit__() - def __exit__(self, *args): + def __exit__(self, *args) -> None: self._stop_transport() -def open_tunnel(*args, **kwargs): +def open_tunnel(*args, **kwargs) -> "SSHTunnelForwarder": """Open an SSH Tunnel, wrapper for :class:`SSHTunnelForwarder`. Arguments: @@ -1612,8 +1546,6 @@ def _bindlist(input_str): _port = "22" # default port if not given return _ip, int(_port) except ValueError: - raise argparse.ArgumentTypeError( - "Address tuple must be of type IP_ADDRESS:PORT" - ) + raise argparse.ArgumentTypeError("Address tuple must be of type IP_ADDRESS:PORT") except AssertionError: raise argparse.ArgumentTypeError("Both IP:PORT can't be missing!") diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index 073a8a1b6..4d425159a 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -1,14 +1,45 @@ """hyperglass Configuration.""" +# Standard Library +import typing as t + +# Project +from hyperglass.state import use_state +from hyperglass.defaults.directives import init_builtin_directives + # Local -from .main import ( - URL_DEV, - URL_PROD, - CONFIG_PATH, - STATIC_PATH, - REDIS_CONFIG, - params, - devices, - commands, - frontend_params, -) +from .validate import init_files, init_params, init_devices, init_ui_params, init_directives + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.directive import Directives + from hyperglass.models.config.params import Params + from hyperglass.models.config.devices import Devices + +__all__ = ("init_user_config",) + + +def init_user_config( + params: t.Optional["Params"] = None, + directives: t.Optional["Directives"] = None, + devices: t.Optional["Devices"] = None, +) -> None: + """Initialize all user configurations and add them to global state.""" + state = use_state() + init_files() + + _params = params or init_params() + builtins = init_builtin_directives() + _custom = directives or init_directives() + _directives = builtins + _custom + with state.cache.pipeline() as pipeline: + # Write params and directives to the cache first to avoid a race condition where ui_params + # or devices try to access params or directives before they're available. + pipeline.set("params", _params) + pipeline.set("directives", _directives) + + _devices = devices or init_devices() + ui_params = init_ui_params(params=_params, devices=_devices) + with state.cache.pipeline() as pipeline: + pipeline.set("devices", _devices) + pipeline.set("ui_params", ui_params) diff --git a/hyperglass/configuration/load.py b/hyperglass/configuration/load.py new file mode 100644 index 000000000..0ea6dab0e --- /dev/null +++ b/hyperglass/configuration/load.py @@ -0,0 +1,125 @@ +"""Collect configurations from files.""" + +# Standard Library +import typing as t +from pathlib import Path + +# Project +from hyperglass.log import log +from hyperglass.util import run_coroutine_in_new_thread +from hyperglass.settings import Settings +from hyperglass.constants import CONFIG_EXTENSIONS +from hyperglass.exceptions.private import ConfigError, ConfigMissing, ConfigLoaderMissing + +LoadedConfig = t.Union[t.Dict[str, t.Any], t.List[t.Any], t.Tuple[t.Any, ...]] + + +def find_path(file_name: str, *, required: bool) -> t.Union[Path, None]: + """Find the first matching configuration file.""" + for extension in CONFIG_EXTENSIONS: + path = Settings.app_path / f"{file_name}.{extension}" + if path.exists(): + return path + + if required: + raise ConfigMissing(file_name, app_path=Settings.app_path) + return None + + +def load_dsl(path: Path, *, empty_allowed: bool) -> LoadedConfig: + """Verify and load data from DSL (non-python) config files.""" + loader = None + if path.suffix in (".yaml", ".yml"): + try: + # Third Party + import yaml + + loader = yaml.safe_load + + except ImportError as err: + raise ConfigLoaderMissing(path) from err + elif path.suffix == ".toml": + try: + # Third Party + import toml + + loader = toml.load + + except ImportError as err: + raise ConfigLoaderMissing(path) from err + + elif path.suffix == ".json": + # Standard Library + import json + + loader = json.load + + if loader is None: + raise ConfigLoaderMissing(path) + + with path.open("r") as f: + data = loader(f) + if data is None and empty_allowed is False: + raise ConfigError( + "'{!s}' exists, but it is empty and is required to start hyperglass.".format(path), + ) + log.bind(path=path).debug("Loaded configuration") + return data or {} + + +def load_python(path: Path, *, empty_allowed: bool) -> LoadedConfig: + """Import configuration from a python configuration file.""" + # Standard Library + import inspect + from importlib.util import module_from_spec, spec_from_file_location + + # Load the file as a module. + name, _ = path.name.split(".") + spec = spec_from_file_location(name, location=path) + module = module_from_spec(spec) + spec.loader.exec_module(module) + # Get all exports that are named 'main' (any case). + exports = tuple(getattr(module, e, None) for e in dir(module) if e.lower() == "main") + if len(exports) < 1: + # Raise an error if there are no exports named main. + raise ConfigError( + f"'{path!s} exists', but it is missing a variable or function named 'main'" + ) + # Pick the first export named main. + main, *_ = exports + data = None + if isinstance(main, t.Callable): + if inspect.iscoroutinefunction(main): + # Resolve an async funcion. + data = run_coroutine_in_new_thread(main) + else: + # Resolve a standard function. + data = main() + elif isinstance(main, (t.Dict, t.List, t.Tuple)): + data = main + + if data is None and empty_allowed is False: + raise ConfigError(f"'{path!s} exists', but variable or function 'main' is an invalid type") + + log.bind(path=path).debug("Loaded configuration") + return data or {} + + +def load_config(name: str, *, required: bool) -> LoadedConfig: + """Load a configuration file.""" + path = find_path(name, required=required) + + if path is None and required is False: + return {} + + if path.suffix == ".py": + return load_python(path, empty_allowed=not required) + + if path.suffix.replace(".", "") in CONFIG_EXTENSIONS: + return load_dsl(path, empty_allowed=not required) + + raise ConfigError( + "{p} has an unsupported file extension. Must be one of {e}", + p=path, + e=", ".join(CONFIG_EXTENSIONS), + ) diff --git a/hyperglass/configuration/main.py b/hyperglass/configuration/main.py deleted file mode 100644 index c26493be2..000000000 --- a/hyperglass/configuration/main.py +++ /dev/null @@ -1,376 +0,0 @@ -"""Import configuration files and returns default values if undefined.""" - -# Standard Library -import os -import json -from typing import Dict, List -from pathlib import Path - -# Third Party -import yaml - -# Project -from hyperglass.log import ( - log, - set_log_level, - enable_file_logging, - enable_syslog_logging, -) -from hyperglass.util import set_app_path, set_cache_env, current_log_level -from hyperglass.defaults import CREDIT, DEFAULT_DETAILS -from hyperglass.constants import ( - SUPPORTED_QUERY_TYPES, - PARSED_RESPONSE_FIELDS, - __version__, -) -from hyperglass.exceptions import ConfigError, ConfigMissing -from hyperglass.util.files import check_path -from hyperglass.models.commands import Commands -from hyperglass.models.config.params import Params -from hyperglass.models.config.devices import Devices - -# Local -from .markdown import get_markdown -from .validation import validate_config, validate_nos_commands - -set_app_path(required=True) - -CONFIG_PATH = Path(os.environ["hyperglass_directory"]) -log.info("Configuration directory: {d}", d=str(CONFIG_PATH)) - -# Project Directories -WORKING_DIR = Path(__file__).resolve().parent -CONFIG_FILES = ( - ("hyperglass.yaml", False), - ("devices.yaml", True), - ("commands.yaml", False), -) - - -def _check_config_files(directory: Path): - """Verify config files exist and are readable.""" - - files = () - - for file in CONFIG_FILES: - file_name, required = file - file_path = directory / file_name - - checked = check_path(file_path) - - if checked is None and required: - raise ConfigMissing(missing_item=str(file_path)) - - if checked is None and not required: - log.warning( - "'{f}' was not found, but is not required to run hyperglass. " - + "Defaults will be used.", - f=str(file_path), - ) - files += (checked,) - - return files - - -STATIC_PATH = CONFIG_PATH / "static" - -CONFIG_MAIN, CONFIG_DEVICES, CONFIG_COMMANDS = _check_config_files(CONFIG_PATH) - - -def _config_required(config_path: Path) -> Dict: - try: - with config_path.open("r") as cf: - config = yaml.safe_load(cf) - - except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: - raise ConfigError(str(yaml_error)) - - if config is None: - log.critical("{} appears to be empty", str(config_path)) - raise ConfigMissing(missing_item=config_path.name) - - return config - - -def _config_optional(config_path: Path) -> Dict: - - config = {} - - if config_path is None: - return config - - else: - try: - with config_path.open("r") as cf: - config = yaml.safe_load(cf) or {} - - except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: - raise ConfigError(error_msg=str(yaml_error)) - - return config - - -user_config = _config_optional(CONFIG_MAIN) - -# Read raw debug value from config to enable debugging quickly. -set_log_level(logger=log, debug=user_config.get("debug", True)) - -# Map imported user configuration to expected schema. -log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config) -params = validate_config(config=user_config, importer=Params) - -# Re-evaluate debug state after config is validated -log_level = current_log_level(log) - -if params.debug and log_level != "debug": - set_log_level(logger=log, debug=True) -elif not params.debug and log_level == "debug": - set_log_level(logger=log, debug=False) - -# Map imported user commands to expected schema. -_user_commands = _config_optional(CONFIG_COMMANDS) -log.debug("Unvalidated commands from {}: {}", CONFIG_COMMANDS, _user_commands) -commands = validate_config(config=_user_commands, importer=Commands.import_params) - -# Map imported user devices to expected schema. -_user_devices = _config_required(CONFIG_DEVICES) -log.debug("Unvalidated devices from {}: {}", CONFIG_DEVICES, _user_devices) -devices = validate_config(config=_user_devices.get("routers", []), importer=Devices) - -# Validate commands are both supported and properly mapped. -validate_nos_commands(devices.all_nos, commands) - -# Set cache configurations to environment variables, so they can be -# used without importing this module (Gunicorn, etc). -set_cache_env(db=params.cache.database, host=params.cache.host, port=params.cache.port) - -# Set up file logging once configuration parameters are initialized. -enable_file_logging( - logger=log, - log_directory=params.logging.directory, - log_format=params.logging.format, - log_max_size=params.logging.max_size, -) - -# Set up syslog logging if enabled. -if params.logging.syslog is not None and params.logging.syslog.enable: - enable_syslog_logging( - logger=log, - syslog_host=params.logging.syslog.host, - syslog_port=params.logging.syslog.port, - ) - -if params.logging.http is not None and params.logging.http.enable: - log.debug("HTTP logging is enabled") - -# Perform post-config initialization string formatting or other -# functions that require access to other config levels. E.g., -# something in 'params.web.text' needs to be formatted with a value -# from params. -try: - params.web.text.subtitle = params.web.text.subtitle.format( - **params.dict(exclude={"web", "queries", "messages"}) - ) - - # If keywords are unmodified (default), add the org name & - # site_title. - if Params().site_keywords == params.site_keywords: - params.site_keywords = sorted( - {*params.site_keywords, params.org_name, params.site_title} - ) - -except KeyError: - pass - - -def _build_frontend_devices(): - """Build filtered JSON structure of devices for frontend. - - Schema: - { - "device.name": { - "display_name": "device.display_name", - "vrfs": [ - "Global", - "vrf.display_name" - ] - } - } - - Raises: - ConfigError: Raised if parsing/building error occurs. - - Returns: - {dict} -- Frontend devices - """ - frontend_dict = {} - for device in devices.objects: - if device.name in frontend_dict: - frontend_dict[device.name].update( - { - "network": device.network.display_name, - "display_name": device.display_name, - "vrfs": [ - { - "id": vrf.name, - "display_name": vrf.display_name, - "default": vrf.default, - "ipv4": True if vrf.ipv4 else False, # noqa: IF100 - "ipv6": True if vrf.ipv6 else False, # noqa: IF100 - } - for vrf in device.vrfs - ], - } - ) - elif device.name not in frontend_dict: - frontend_dict[device.name] = { - "network": device.network.display_name, - "display_name": device.display_name, - "vrfs": [ - { - "id": vrf.name, - "display_name": vrf.display_name, - "default": vrf.default, - "ipv4": True if vrf.ipv4 else False, # noqa: IF100 - "ipv6": True if vrf.ipv6 else False, # noqa: IF100 - } - for vrf in device.vrfs - ], - } - if not frontend_dict: - raise ConfigError(error_msg="Unable to build network to device mapping") - return frontend_dict - - -def _build_networks() -> List[Dict]: - """Build filtered JSON Structure of networks & devices for Jinja templates.""" - networks = [] - _networks = list(set({device.network.display_name for device in devices.objects})) - - for _network in _networks: - network_def = {"display_name": _network, "locations": []} - for device in devices.objects: - if device.network.display_name == _network: - network_def["locations"].append( - { - "_id": device._id, - "name": device.name, - "network": device.network.display_name, - "vrfs": [ - { - "_id": vrf._id, - "display_name": vrf.display_name, - "default": vrf.default, - "ipv4": True if vrf.ipv4 else False, # noqa: IF100 - "ipv6": True if vrf.ipv6 else False, # noqa: IF100 - } - for vrf in device.vrfs - ], - } - ) - networks.append(network_def) - - if not networks: - raise ConfigError(error_msg="Unable to build network to device mapping") - return networks - - -content_params = json.loads( - params.json(include={"primary_asn", "org_name", "site_title", "site_description"}) -) - - -def _build_vrf_help() -> Dict: - """Build a dict of vrfs as keys, help content as values.""" - all_help = {} - for vrf in devices.vrf_objects: - - vrf_help = {} - for command in SUPPORTED_QUERY_TYPES: - cmd = getattr(vrf.info, command) - if cmd.enable: - help_params = {**content_params, **cmd.params.dict()} - - if help_params["title"] is None: - command_params = getattr(params.queries, command) - help_params[ - "title" - ] = f"{vrf.display_name}: {command_params.display_name}" - - md = get_markdown( - config_path=cmd, - default=DEFAULT_DETAILS[command], - params=help_params, - ) - - vrf_help.update( - { - command: { - "content": md, - "enable": cmd.enable, - "params": help_params, - } - } - ) - - all_help.update({vrf._id: vrf_help}) - - return all_help - - -content_greeting = get_markdown( - config_path=params.web.greeting, - default="", - params={"title": params.web.greeting.title}, -) - -content_vrf = _build_vrf_help() - -content_credit = CREDIT.format(version=__version__) - -networks = _build_networks() -frontend_devices = _build_frontend_devices() -_include_fields = { - "cache": {"show_text", "timeout"}, - "debug": ..., - "developer_mode": ..., - "primary_asn": ..., - "request_timeout": ..., - "org_name": ..., - "google_analytics": ..., - "site_title": ..., - "site_description": ..., - "site_keywords": ..., - "web": ..., - "messages": ..., -} -_frontend_params = params.dict(include=_include_fields) - - -_frontend_params["web"]["logo"]["light_format"] = params.web.logo.light.suffix -_frontend_params["web"]["logo"]["dark_format"] = params.web.logo.dark.suffix - -_frontend_params.update( - { - "hyperglass_version": __version__, - "queries": {**params.queries.map, "list": params.queries.list}, - "networks": networks, - "parsed_data_fields": PARSED_RESPONSE_FIELDS, - "content": { - "credit": content_credit, - "vrf": content_vrf, - "greeting": content_greeting, - }, - } -) -frontend_params = _frontend_params - -URL_DEV = f"http://localhost:{str(params.listen_port)}/" -URL_PROD = "/api/" - -REDIS_CONFIG = { - "host": str(params.cache.host), - "port": params.cache.port, - "decode_responses": True, - "password": params.cache.password, -} diff --git a/hyperglass/configuration/markdown.py b/hyperglass/configuration/markdown.py index 873e6719a..3df2762bc 100644 --- a/hyperglass/configuration/markdown.py +++ b/hyperglass/configuration/markdown.py @@ -1,61 +1,26 @@ """Markdown processing utility functions.""" -# Project -from hyperglass.log import log +# Standard Library +import typing as t +from pathlib import Path +if t.TYPE_CHECKING: + # Project + from hyperglass.models import HyperglassModel -def _get_file(path_obj): - """Read a file. - Arguments: - path_obj {Path} -- Path to file. +def get_markdown(config: "HyperglassModel", default: str, params: t.Dict[str, t.Any]) -> str: + """Get markdown file if specified, or use default.""" - Returns: - {str} -- File contents - """ - with path_obj.open("r") as raw_file: - return raw_file.read() - - -def format_markdown(content, params): - """Format content with config parameters. - - Arguments: - content {str} -- Unformatted content - - Returns: - {str} -- Formatted content - """ - try: - fmt = content.format(**params) - except KeyError: - fmt = content - return fmt - - -def get_markdown(config_path, default, params): - """Get markdown file if specified, or use default. - - Format the content with config parameters. - - Arguments: - config_path {object} -- content config - default {str} -- default content - - Returns: - {str} -- Formatted content - """ - log.trace(f"Getting Markdown content for '{params['title']}'") - - if config_path.enable and config_path.file is not None: - md = _get_file(config_path.file) + if config.enable and config.file is not None: + # with config_path.file + if hasattr(config, "file") and isinstance(config.file, Path): + with config.file.open("r") as config_file: + md = config_file.read() else: md = default - log.trace(f"Unformatted Content for '{params['title']}':\n{md}") - - md_fmt = format_markdown(md, params) - - log.trace(f"Formatted Content for '{params['title']}':\n{md_fmt}") - - return md_fmt + try: + return md.format(**params) + except KeyError: + return md diff --git a/hyperglass/configuration/tests/__init__.py b/hyperglass/configuration/tests/__init__.py new file mode 100644 index 000000000..233c95a85 --- /dev/null +++ b/hyperglass/configuration/tests/__init__.py @@ -0,0 +1 @@ +"""hyperglass configuration tests.""" diff --git a/hyperglass/configuration/tests/test_load.py b/hyperglass/configuration/tests/test_load.py new file mode 100644 index 000000000..dba92e99e --- /dev/null +++ b/hyperglass/configuration/tests/test_load.py @@ -0,0 +1,58 @@ +"""Test configuration file collection.""" + +# Standard Library +import tempfile +from pathlib import Path + +# Project +from hyperglass.settings import Settings + +# Local +from ..load import load_config + +TOML = """ +test = "from toml" +""" + +YAML = """ +test: from yaml +""" + +JSON = """ +{"test": "from json"} +""" + +PY_VARIABLE = """ +MAIN = {'test': 'from python variable'} +""" + +PY_FUNCTION = """ +def main(): + return {'test': 'from python function'} +""" + +PY_COROUTINE = """ +async def main(): + return {'test': 'from python coroutine'} +""" + +CASES = ( + ("test.toml", "from toml", TOML), + ("test.yaml", "from yaml", YAML), + ("test_py_variable.py", "from python variable", PY_VARIABLE), + ("test_py_function.py", "from python function", PY_FUNCTION), + ("test_py_coroutine.py", "from python coroutine", PY_COROUTINE), +) + + +def test_collect(monkeypatch): + with tempfile.TemporaryDirectory() as directory_name: + directory = Path(directory_name) + monkeypatch.setattr(Settings, "app_path", directory) + for name, value, data in CASES: + path = directory / Path(name) + with path.open("w") as p: + p.write(data) + loaded = load_config(path.stem, required=True) + assert loaded.get("test") is not None + assert loaded["test"] == value diff --git a/hyperglass/configuration/validate.py b/hyperglass/configuration/validate.py new file mode 100644 index 000000000..3accb7e16 --- /dev/null +++ b/hyperglass/configuration/validate.py @@ -0,0 +1,136 @@ +"""Import configuration files and run validation.""" + +# Third Party +from pydantic import ValidationError + +# Project +from hyperglass.log import log +from hyperglass.settings import Settings +from hyperglass.models.ui import UIParameters +from hyperglass.models.directive import Directive, Directives +from hyperglass.exceptions.private import ConfigError, ConfigInvalid +from hyperglass.models.config.params import Params +from hyperglass.models.config.devices import Devices + +# Local +from .load import load_config +from .markdown import get_markdown + +__all__ = ( + "init_devices", + "init_directives", + "init_files", + "init_params", + "init_ui_params", +) + + +def init_files() -> None: + """Check if required directories exist and if not, create them.""" + for directory in ("plugins", "static/images"): + path = Settings.app_path / directory + if not path.exists(): + path.mkdir(parents=True) + log.debug("Created directory", path=path) + + +def init_params() -> "Params": + """Validate & initialize configuration parameters.""" + user_config = load_config("config", required=False) + # Map imported user configuration to expected schema. + params = Params(**user_config) + + # # Set up file logging once configuration parameters are initialized. + # enable_file_logging( + # log_directory=params.logging.directory, + # log_format=params.logging.format, + # log_max_size=params.logging.max_size, + # debug=Settings.debug, + # ) + + # Set up syslog logging if enabled. + # if params.logging.syslog is not None and params.logging.syslog.enable: + # enable_syslog_logging( + # syslog_host=params.logging.syslog.host, + # syslog_port=params.logging.syslog.port, + # ) + + if params.logging.http is not None and params.logging.http.enable: + log.debug("HTTP logging is enabled") + + # Perform post-config initialization string formatting or other + # functions that require access to other config levels. E.g., + # something in 'params.web.text' needs to be formatted with a value + # from params. + try: + params.web.text.subtitle = params.web.text.subtitle.format( + **params.model_dump(exclude={"web", "queries", "messages"}) + ) + except KeyError: + pass + + return params + + +def init_directives() -> "Directives": + """Validate & initialize directives.""" + # Map imported user directives to expected schema. + directives = load_config("directives", required=False) + try: + directives = ( + Directive(id=name, **directive) + for name, directive in load_config("directives", required=False).items() + ) + + except ValidationError as err: + raise ConfigInvalid(errors=err.errors()) from err + + return Directives(*directives) + + +def init_devices() -> "Devices": + """Validate & initialize devices.""" + devices_config = load_config("devices", required=True) + items = [] + + # Support first matching main key name. + for key in ("main", "devices", "routers"): + if key in devices_config: + items = devices_config[key] + break + + if len(items) < 1: + raise ConfigError("No devices are defined in devices file") + + devices = Devices(*items) + log.debug("Initialized devices", devices=devices) + + return devices + + +def init_ui_params(*, params: "Params", devices: "Devices") -> "UIParameters": + """Validate & initialize UI parameters.""" + + # Project + from hyperglass.defaults import CREDIT + from hyperglass.constants import PARSED_RESPONSE_FIELDS, __version__ + + content_greeting = get_markdown( + config=params.web.greeting, + default="", + params={"title": params.web.greeting.title}, + ) + content_credit = CREDIT.format(version=__version__) + + _ui_params = params.frontend() + _ui_params["web"]["logo"]["light_format"] = params.web.logo.light.suffix + _ui_params["web"]["logo"]["dark_format"] = params.web.logo.dark.suffix + + return UIParameters( + **_ui_params, + version=__version__, + devices=devices.frontend(), + developer_mode=Settings.dev_mode, + parsed_data_fields=PARSED_RESPONSE_FIELDS, + content={"credit": content_credit, "greeting": content_greeting}, + ) diff --git a/hyperglass/configuration/validation.py b/hyperglass/configuration/validation.py deleted file mode 100644 index 900cb933a..000000000 --- a/hyperglass/configuration/validation.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Post-Validation Validation. - -Some validations need to occur across multiple config files. -""" -# Standard Library -from typing import Dict, List, Union, Callable - -# Third Party -from pydantic import ValidationError - -# Project -from hyperglass.log import log -from hyperglass.models import HyperglassModel -from hyperglass.constants import TRANSPORT_REST, SUPPORTED_STRUCTURED_OUTPUT -from hyperglass.exceptions import ConfigError, ConfigInvalid -from hyperglass.models.commands import Commands - - -def validate_nos_commands(all_nos: List[str], commands: Commands) -> bool: - """Ensure defined devices have associated commands.""" - custom_commands = commands.dict().keys() - - for nos in all_nos: - valid = False - if nos in (*SUPPORTED_STRUCTURED_OUTPUT, *TRANSPORT_REST, *custom_commands): - valid = True - - if not valid: - raise ConfigError( - '"{nos}" is used on a device, ' - + 'but no command profile for "{nos}" is defined.', - nos=nos, - ) - - return True - - -def validate_config(config: Union[Dict, List], importer: Callable) -> HyperglassModel: - """Validate a config dict against a model.""" - validated = None - try: - if isinstance(config, Dict): - validated = importer(**config) - elif isinstance(config, List): - validated = importer(config) - except ValidationError as err: - log.error(str(err)) - raise ConfigInvalid(err.errors()) from None - - return validated diff --git a/hyperglass/console.py b/hyperglass/console.py index a1a7aed82..bdc2d07ad 100755 --- a/hyperglass/console.py +++ b/hyperglass/console.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """hyperglass CLI management tool.""" -# Project -from hyperglass.cli import CLI +# Local +from .cli import run if __name__ == "__main__": - CLI() + run() diff --git a/hyperglass/constants.py b/hyperglass/constants.py index 85584b973..6da841440 100644 --- a/hyperglass/constants.py +++ b/hyperglass/constants.py @@ -4,22 +4,24 @@ from datetime import datetime __name__ = "hyperglass" -__version__ = "1.0.4" +__version__ = "2.0.4" __author__ = "Matt Love" __copyright__ = f"Copyright {datetime.now().year} Matthew Love" __license__ = "BSD 3-Clause Clear License" METADATA = (__name__, __version__, __author__, __copyright__, __license__) -MIN_PYTHON_VERSION = (3, 6) +MIN_PYTHON_VERSION = (3, 11) -MIN_NODE_VERSION = 14 +MIN_NODE_VERSION = 18 TARGET_FORMAT_SPACE = ("huawei", "huawei_vrpv8") TARGET_JUNIPER_ASPATH = ("juniper", "juniper_junos") -SUPPORTED_STRUCTURED_OUTPUT = ("juniper", "arista_eos") +SUPPORTED_STRUCTURED_OUTPUT = ("frr", "juniper", "arista_eos") + +CONFIG_EXTENSIONS = ("py", "yaml", "yml", "json", "toml") STATUS_CODE_MAP = {"warning": 400, "error": 400, "danger": 500} @@ -74,16 +76,14 @@ } DRIVER_MAP = { - # TODO: Troubleshoot Arista with Scrapli, broken after upgrading to 2021.1.30. - # "arista_eos": "scrapli", # noqa: E800 - "bird": "scrapli", - "cisco_ios": "scrapli", - "cisco_xe": "scrapli", - "cisco_xr": "scrapli", - "cisco_nxos": "scrapli", - "juniper": "scrapli", - "tnsr": "scrapli", - "frr": "scrapli", - "frr_legacy": "hyperglass_agent", - "bird_legacy": "hyperglass_agent", + "bird": "netmiko", + "frr": "netmiko", + "openbgpd": "netmiko", + "http": "hyperglass_http_client", } + +LINUX_PLATFORMS = ( + "frr", + "bird", + "openbgpd", +) diff --git a/hyperglass/defaults/__init__.py b/hyperglass/defaults/__init__.py new file mode 100644 index 000000000..ce1f492ad --- /dev/null +++ b/hyperglass/defaults/__init__.py @@ -0,0 +1,11 @@ +"""Default or built-in hyperglass data.""" + +# Local +from ._strings import CREDIT, DEFAULT_HELP, DEFAULT_TERMS, DEFAULT_DETAILS + +__all__ = ( + "CREDIT", + "DEFAULT_TERMS", + "DEFAULT_DETAILS", + "DEFAULT_HELP", +) diff --git a/hyperglass/defaults.py b/hyperglass/defaults/_strings.py similarity index 99% rename from hyperglass/defaults.py rename to hyperglass/defaults/_strings.py index b4a299ab7..e1ae1456c 100644 --- a/hyperglass/defaults.py +++ b/hyperglass/defaults/_strings.py @@ -2,7 +2,7 @@ CREDIT = """ Powered by [**hyperglass**](https://hyperglass.dev) version {version}. \ -Source code licensed [_BSD 3-Clause Clear_](https://hyperglass.dev/docs/license/). +Source code licensed [_BSD 3-Clause Clear_](https://hyperglass.dev/license/). """ DEFAULT_TERMS = """ diff --git a/hyperglass/defaults/directives/__init__.py b/hyperglass/defaults/directives/__init__.py new file mode 100644 index 000000000..8b29355a7 --- /dev/null +++ b/hyperglass/defaults/directives/__init__.py @@ -0,0 +1,26 @@ +"""Built-in hyperglass directives.""" + +# Standard Library +import pkgutil +import importlib +from pathlib import Path + +# Project +from hyperglass.log import log +from hyperglass.models.directive import Directives + + +def init_builtin_directives() -> "Directives": + """Find all directives and register them with global state manager.""" + directives_dir = Path(__file__).parent + directives = () + for _, name, __ in pkgutil.iter_modules([directives_dir]): + module = importlib.import_module(f"hyperglass.defaults.directives.{name}") + + if not all((hasattr(module, "__all__"), len(getattr(module, "__all__", ())) > 0)): + # Warn if there is no __all__ export or if it is empty. + log.warning("Module '{!s}' is missing an '__all__' export", module) + + exports = (getattr(module, p) for p in module.__all__ if hasattr(module, p)) + directives += (*exports,) + return Directives(*directives) diff --git a/hyperglass/defaults/directives/arista_eos.py b/hyperglass/defaults/directives/arista_eos.py new file mode 100644 index 000000000..5d1bad7ab --- /dev/null +++ b/hyperglass/defaults/directives/arista_eos.py @@ -0,0 +1,174 @@ +"""Default Arista Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "AristaBGPRoute", + "AristaBGPASPath", + "AristaBGPCommunity", + "AristaPing", + "AristaTraceroute", + "AristaBGPRouteTable", + "AristaBGPASPathTable", + "AristaBGPCommunityTable", +) + +NAME = "Arista EOS" +PLATFORMS = ["arista_eos"] + +AristaBGPRoute = BuiltinDirective( + id="__hyperglass_arista_eos_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="show ip bgp {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="show ipv6 bgp {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + table_output="__hyperglass_arista_eos_bgp_route_table__", + platforms=PLATFORMS, +) + +AristaBGPASPath = BuiltinDirective( + id="__hyperglass_arista_eos_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show ip bgp regexp {target}", + "show ipv6 bgp regexp {target}", + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + table_output="__hyperglass_arista_eos_bgp_aspath_table__", + platforms=PLATFORMS, +) + +AristaBGPCommunity = BuiltinDirective( + id="__hyperglass_arista_eos_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show ip bgp community {target}", + "show ipv6 bgp community {target}", + ], + ) + ], + field=Text(description="BGP Community String"), + table_output="__hyperglass_arista_eos_bgp_community_table__", + platforms=PLATFORMS, +) + + +AristaPing = BuiltinDirective( + id="__hyperglass_arista_eos_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping ip {target} source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping ipv6 {target} source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +AristaTraceroute = BuiltinDirective( + id="__hyperglass_arista_eos_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute ip {target} source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute ipv6 {target} source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +# Table Output Directives + +AristaBGPRouteTable = BuiltinDirective( + id="__hyperglass_arista_eos_bgp_route_table__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="show ip bgp {target} | json", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="show ipv6 bgp {target} | json", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +AristaBGPASPathTable = BuiltinDirective( + id="__hyperglass_arista_eos_bgp_aspath_table__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show ip bgp regexp {target} | json", + "show ipv6 bgp regexp {target} | json", + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +AristaBGPCommunityTable = BuiltinDirective( + id="__hyperglass_arista_eos_bgp_community_table__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show ip bgp community {target} | json", + "show ipv6 bgp community {target} | json", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/bird.py b/hyperglass/defaults/directives/bird.py new file mode 100644 index 000000000..14c971275 --- /dev/null +++ b/hyperglass/defaults/directives/bird.py @@ -0,0 +1,110 @@ +"""Default BIRD Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "BIRD_BGPASPath", + "BIRD_BGPCommunity", + "BIRD_BGPRoute", + "BIRD_Ping", + "BIRD_Traceroute", +) + +NAME = "BIRD" +PLATFORMS = ["bird"] + +BIRD_BGPRoute = BuiltinDirective( + id="__hyperglass_bird_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command='birdc "show route all where {target} ~ net"', + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command='birdc "show route all where {target} ~ net"', + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +BIRD_BGPASPath = BuiltinDirective( + id="__hyperglass_bird_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'birdc "show route all where bgp_path ~ {target}"', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +BIRD_BGPCommunity = BuiltinDirective( + id="__hyperglass_bird_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'birdc "show route all where {target} ~ bgp_community"', + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +BIRD_Ping = BuiltinDirective( + id="__hyperglass_bird_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping -4 -c 5 -I {source4} {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping -6 -c 5 -I {source6} {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +BIRD_Traceroute = BuiltinDirective( + id="__hyperglass_bird_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute -4 -w 1 -q 1 -s {source4} {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute -6 -w 1 -q 1 -s {source6} {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/cisco_ios.py b/hyperglass/defaults/directives/cisco_ios.py new file mode 100644 index 000000000..f4327c925 --- /dev/null +++ b/hyperglass/defaults/directives/cisco_ios.py @@ -0,0 +1,112 @@ +"""Default Cisco IOS Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "CiscoIOS_BGPASPath", + "CiscoIOS_BGPCommunity", + "CiscoIOS_BGPRoute", + "CiscoIOS_Ping", + "CiscoIOS_Traceroute", +) + +NAME = "Cisco IOS" +PLATFORMS = ["cisco_ios"] + +CiscoIOS_BGPRoute = BuiltinDirective( + id="__hyperglass_cisco_ios_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="show bgp ipv4 unicast {target} | exclude pathid:|Epoch", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="show bgp ipv6 unicast {target} | exclude pathid:|Epoch", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +CiscoIOS_BGPASPath = BuiltinDirective( + id="__hyperglass_cisco_ios_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'show bgp ipv4 unicast quote-regexp "{target}"', + 'show bgp ipv6 unicast quote-regexp "{target}"', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +CiscoIOS_BGPCommunity = BuiltinDirective( + id="__hyperglass_cisco_ios_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show bgp ipv4 unicast community {target}", + "show bgp ipv6 unicast community {target}", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +CiscoIOS_Ping = BuiltinDirective( + id="__hyperglass_cisco_ios_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping {target} repeat 5 source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping ipv6 {target} repeat 5 source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +CiscoIOS_Traceroute = BuiltinDirective( + id="__hyperglass_cisco_ios_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute {target} timeout 1 probe 2 source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/cisco_nxos.py b/hyperglass/defaults/directives/cisco_nxos.py new file mode 100644 index 000000000..1c7a8ea64 --- /dev/null +++ b/hyperglass/defaults/directives/cisco_nxos.py @@ -0,0 +1,112 @@ +"""Default Cisco NX-OS Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "CiscoNXOS_BGPASPath", + "CiscoNXOS_BGPCommunity", + "CiscoNXOS_BGPRoute", + "CiscoNXOS_Ping", + "CiscoNXOS_Traceroute", +) + +NAME = "Cisco NX-OS" +PLATFORMS = ["cisco_nxos"] + +CiscoNXOS_BGPRoute = BuiltinDirective( + id="__hyperglass_cisco_nxos_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="show bgp ipv4 unicast {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="show bgp ipv6 unicast {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +CiscoNXOS_BGPASPath = BuiltinDirective( + id="__hyperglass_cisco_nxos_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'show bgp ipv4 unicast regexp "{target}"', + 'show bgp ipv6 unicast regexp "{target}"', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +CiscoNXOS_BGPCommunity = BuiltinDirective( + id="__hyperglass_cisco_nxos_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show bgp ipv4 unicast community {target}", + "show bgp ipv6 unicast community {target}", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +CiscoNXOS_Ping = BuiltinDirective( + id="__hyperglass_cisco_nxos_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping {target} source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping6 {target} source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +CiscoNXOS_Traceroute = BuiltinDirective( + id="__hyperglass_cisco_nxos_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute {target} source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute6 {target} source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/cisco_xr.py b/hyperglass/defaults/directives/cisco_xr.py new file mode 100644 index 000000000..84501806e --- /dev/null +++ b/hyperglass/defaults/directives/cisco_xr.py @@ -0,0 +1,112 @@ +"""Default Cisco IOS-XR Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "CiscoXR_BGPASPath", + "CiscoXR_BGPCommunity", + "CiscoXR_BGPRoute", + "CiscoXR_Ping", + "CiscoXR_Traceroute", +) + +NAME = "Cisco IOS-XR" +PLATFORMS = ["cisco_xr"] + +CiscoXR_BGPRoute = BuiltinDirective( + id="__hyperglass_cisco_xr_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="show bgp ipv4 unicast {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="show bgp ipv6 unicast {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +CiscoXR_BGPASPath = BuiltinDirective( + id="__hyperglass_cisco_xr_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show bgp ipv4 unicast regexp {target}", + "show bgp ipv6 unicast regexp {target}", + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +CiscoXR_BGPCommunity = BuiltinDirective( + id="__hyperglass_cisco_xr_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show bgp ipv4 unicast community {target}", + "show bgp ipv6 unicast community {target}", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +CiscoXR_Ping = BuiltinDirective( + id="__hyperglass_cisco_xr_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping ipv4 {target} count 5 source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping ipv6 {target} count 5 source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +CiscoXR_Traceroute = BuiltinDirective( + id="__hyperglass_cisco_xr_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute ipv4 {target} timeout 1 probe 2 source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute ipv6 {target} timeout 1 probe 2 source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/frr.py b/hyperglass/defaults/directives/frr.py new file mode 100644 index 000000000..eb6baafd9 --- /dev/null +++ b/hyperglass/defaults/directives/frr.py @@ -0,0 +1,135 @@ +"""Default FRRouting Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "FRRouting_BGPASPath", + "FRRouting_BGPCommunity", + "FRRouting_BGPRoute", + "FRRouting_Ping", + "FRRouting_Traceroute", + "FRRouting_BGPRouteTable", +) + +NAME = "FRRouting" +PLATFORMS = ["frr"] + +FRRouting_BGPRoute = BuiltinDirective( + id="__hyperglass_frr_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command='vtysh -c "show bgp ipv4 unicast {target}"', + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command='vtysh -c "show bgp ipv6 unicast {target}"', + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + table_output="__hyperglass_frr_bgp_route_table__", + platforms=PLATFORMS, +) + +FRRouting_BGPASPath = BuiltinDirective( + id="__hyperglass_frr_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'vtysh -c "show bgp ipv4 unicast regexp {target}"', + 'vtysh -c "show bgp ipv6 unicast regexp {target}"', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +FRRouting_BGPCommunity = BuiltinDirective( + id="__hyperglass_frr_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'vtysh -c "show bgp ipv4 unicast community {target}"', + 'vtysh -c "show bgp ipv6 unicast community {target}"', + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +FRRouting_Ping = BuiltinDirective( + id="__hyperglass_frr_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping -4 -c 5 -I {source4} {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping -6 -c 5 -I {source6} {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +FRRouting_Traceroute = BuiltinDirective( + id="__hyperglass_frr_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute -4 -w 1 -q 1 -s {source4} {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute -6 -w 1 -q 1 -s {source6} {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +# Table Output Directives + +FRRouting_BGPRouteTable = BuiltinDirective( + id="__hyperglass_frr_bgp_route_table__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command='vtysh -c "show bgp ipv4 unicast {target} json"', + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command='vtysh -c "show bgp ipv6 unicast {target} json"', + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/huawei.py b/hyperglass/defaults/directives/huawei.py new file mode 100644 index 000000000..f8d3ef0cf --- /dev/null +++ b/hyperglass/defaults/directives/huawei.py @@ -0,0 +1,113 @@ +"""Default Huawei Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "Huawei_BGPASPath", + "Huawei_BGPCommunity", + "Huawei_BGPRoute", + "Huawei_Ping", + "Huawei_Traceroute", +) + +NAME = "Huawei VRP" +PLATFORMS = ["huawei", "huawei_vrpv8"] + +Huawei_BGPRoute = BuiltinDirective( + id="__hyperglass_huawei_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="display bgp routing-table {target} | no-more", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="display bgp ipv6 routing-table {target} | no-more", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + plugins=["bgp_route_huawei"], + platforms=PLATFORMS, +) + +Huawei_BGPASPath = BuiltinDirective( + id="__hyperglass_huawei_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "display bgp routing-table regular-expression {target}", + "display bgp ipv6 routing-table regular-expression {target}", + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +Huawei_BGPCommunity = BuiltinDirective( + id="__hyperglass_huawei_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "display bgp routing-table community {target}", + "display bgp ipv6 routing-table community {target}", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +Huawei_Ping = BuiltinDirective( + id="__hyperglass_huawei_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping -c 5 -a {source4} {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping ipv6 -c 5 -a {source6} {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +Huawei_Traceroute = BuiltinDirective( + id="__hyperglass_huawei_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="tracert -q 2 -f 1 -a {source4} {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="tracert -q 2 -f 1 -a {source6} {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/juniper.py b/hyperglass/defaults/directives/juniper.py new file mode 100644 index 000000000..dda31e102 --- /dev/null +++ b/hyperglass/defaults/directives/juniper.py @@ -0,0 +1,174 @@ +"""Default Juniper Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "JuniperBGPRoute", + "JuniperBGPASPath", + "JuniperBGPCommunity", + "JuniperPing", + "JuniperTraceroute", + "JuniperBGPRouteTable", + "JuniperBGPASPathTable", + "JuniperBGPCommunityTable", +) + +NAME = "Juniper Junos" +PLATFORMS = ["juniper", "juniper_junos"] + +JuniperBGPRoute = BuiltinDirective( + id="__hyperglass_juniper_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="show route protocol bgp table inet.0 {target} detail", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="show route protocol bgp table inet6.0 {target} detail", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + table_output="__hyperglass_juniper_bgp_route_table__", + platforms=PLATFORMS, +) + +JuniperBGPASPath = BuiltinDirective( + id="__hyperglass_juniper_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'show route protocol bgp table inet.0 aspath-regex "{target}"', + 'show route protocol bgp table inet6.0 aspath-regex "{target}"', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + table_output="__hyperglass_juniper_bgp_aspath_table__", + platforms=PLATFORMS, +) + +JuniperBGPCommunity = BuiltinDirective( + id="__hyperglass_juniper_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'show route protocol bgp table inet.0 community "{target}" detail', + 'show route protocol bgp table inet6.0 community "{target}" detail', + ], + ) + ], + field=Text(description="BGP Community String"), + table_output="__hyperglass_juniper_bgp_community_table__", + platforms=PLATFORMS, +) + + +JuniperPing = BuiltinDirective( + id="__hyperglass_juniper_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping inet {target} count 5 source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping inet6 {target} count 5 source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +JuniperTraceroute = BuiltinDirective( + id="__hyperglass_juniper_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute inet {target} wait 1 source {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute inet6 {target} wait 2 source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +# Table Output Directives + +JuniperBGPRouteTable = BuiltinDirective( + id="__hyperglass_juniper_bgp_route_table__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="show route protocol bgp table inet.0 {target} best detail | display xml", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="show route protocol bgp table inet6.0 {target} best detail | display xml", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +JuniperBGPASPathTable = BuiltinDirective( + id="__hyperglass_juniper_bgp_aspath_table__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'show route protocol bgp table inet.0 aspath-regex "{target}" detail | display xml', + 'show route protocol bgp table inet6.0 aspath-regex "{target}" detail | display xml', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +JuniperBGPCommunityTable = BuiltinDirective( + id="__hyperglass_juniper_bgp_community_table__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show route protocol bgp table inet.0 community {target} detail | display xml", + "show route protocol bgp table inet6.0 community {target} detail | display xml", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/mikrotik.py b/hyperglass/defaults/directives/mikrotik.py new file mode 100644 index 000000000..4ea96fdff --- /dev/null +++ b/hyperglass/defaults/directives/mikrotik.py @@ -0,0 +1,112 @@ +"""Default Mikrotik Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "Mikrotik_BGPASPath", + "Mikrotik_BGPCommunity", + "Mikrotik_BGPRoute", + "Mikrotik_Ping", + "Mikrotik_Traceroute", +) + +NAME = "Mikrotik" +PLATFORMS = ["mikrotik_routeros", "mikrotik_switchos"] + +Mikrotik_BGPRoute = BuiltinDirective( + id="__hyperglass_mikrotik_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ip route print where {target} in dst-address", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ipv6 route print where {target} in dst-address", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +Mikrotik_BGPASPath = BuiltinDirective( + id="__hyperglass_mikrotik_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "ip route print where bgp-as-path={target}", + "ipv6 route print where bgp-as-path={target}", + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +Mikrotik_BGPCommunity = BuiltinDirective( + id="__hyperglass_mikrotik_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "ip route print where bgp-communities={target}", + "ipv6 route print where bgp-communities={target}", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +Mikrotik_Ping = BuiltinDirective( + id="__hyperglass_mikrotik_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping src-address={source4} count=5 {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping src-address={source6} count=5 {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +Mikrotik_Traceroute = BuiltinDirective( + id="__hyperglass_mikrotik_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="tool traceroute src-address={source4} timeout=1 duration=5 count=1 {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="tool traceroute src-address={source6} timeout=1 duration=5 count=1 {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/nokia_sros.py b/hyperglass/defaults/directives/nokia_sros.py new file mode 100644 index 000000000..1e8ea5e31 --- /dev/null +++ b/hyperglass/defaults/directives/nokia_sros.py @@ -0,0 +1,110 @@ +"""Default Nokia SR-OS Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "NokiaSROS_BGPASPath", + "NokiaSROS_BGPCommunity", + "NokiaSROS_BGPRoute", + "NokiaSROS_Ping", + "NokiaSROS_Traceroute", +) + +NAME = "Nokia SR OS" +PLATFORMS = ["nokia_sros"] + +NokiaSROS_BGPRoute = BuiltinDirective( + id="__hyperglass_nokia_sros_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="/show router bgp routes {target} ipv4 hunt", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="/show router bgp routes {target} ipv6 hunt", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +NokiaSROS_BGPASPath = BuiltinDirective( + id="__hyperglass_nokia_sros_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "/show router bgp routes aspath-regex {target}", + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +NokiaSROS_BGPCommunity = BuiltinDirective( + id="__hyperglass_nokia_sros_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "/show router bgp routes community {target}", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +NokiaSROS_Ping = BuiltinDirective( + id="__hyperglass_nokia_sros_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="/ping {target} source-address {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="/ping {target} source-address {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +NokiaSROS_Traceroute = BuiltinDirective( + id="__hyperglass_nokia_sros_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="/traceroute {target} source-address {source4} wait 2 seconds", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="/traceroute {target} source-address {source6} wait 2 seconds", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/openbgpd.py b/hyperglass/defaults/directives/openbgpd.py new file mode 100644 index 000000000..71e7077de --- /dev/null +++ b/hyperglass/defaults/directives/openbgpd.py @@ -0,0 +1,112 @@ +"""Default FRRouting Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "OpenBGPD_BGPASPath", + "OpenBGPD_BGPCommunity", + "OpenBGPD_BGPRoute", + "OpenBGPD_Ping", + "OpenBGPD_Traceroute", +) + +NAME = "OpenBGPD" +PLATFORMS = ["openbgpd"] + +OpenBGPD_BGPRoute = BuiltinDirective( + id="__hyperglass_openbgpd_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="bgpctl show rib inet {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="bgpctl show rib inet6 {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +OpenBGPD_BGPASPath = BuiltinDirective( + id="__hyperglass_openbgpd_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "bgpctl show rib inet as {target}", + "bgpctl show rib inet6 as {target}", + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +OpenBGPD_BGPCommunity = BuiltinDirective( + id="__hyperglass_openbgpd_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "bgpctl show rib inet community {target}", + "bgpctl show rib inet6 community {target}", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +OpenBGPD_Ping = BuiltinDirective( + id="__hyperglass_openbgpd_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping -4 -c 5 -I {source4} {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping -6 -c 5 -I {source6} {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +OpenBGPD_Traceroute = BuiltinDirective( + id="__hyperglass_openbgpd_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute -4 -w 1 -q 1 -s {source4} {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute -6 -w 1 -q 1 -s {source6} {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/tnsr.py b/hyperglass/defaults/directives/tnsr.py new file mode 100644 index 000000000..cea46814a --- /dev/null +++ b/hyperglass/defaults/directives/tnsr.py @@ -0,0 +1,112 @@ +"""Default TNSR Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "TNSR_BGPASPath", + "TNSR_BGPCommunity", + "TNSR_BGPRoute", + "TNSR_Ping", + "TNSR_Traceroute", +) + +NAME = "TNSR" +PLATFORMS = ["tnsr"] + +TNSR_BGPRoute = BuiltinDirective( + id="__hyperglass_tnsr_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command='dataplane shell sudo vtysh -c "show bgp ipv4 unicast {target}"', + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command='dataplane shell sudo vtysh -c "show bgp ipv6 unicast {target}"', + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +TNSR_BGPASPath = BuiltinDirective( + id="__hyperglass_tnsr_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'dataplane shell sudo vtysh -c "show bgp ipv4 unicast regexp {target}"', + 'dataplane shell sudo vtysh -c "show bgp ipv6 unicast regexp {target}"', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +TNSR_BGPCommunity = BuiltinDirective( + id="__hyperglass_tnsr_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'dataplane shell sudo vtysh -c "show bgp ipv4 unicast community {target}"', + 'dataplane shell sudo vtysh -c "show bgp ipv6 unicast community {target}"', + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +TNSR_Ping = BuiltinDirective( + id="__hyperglass_tnsr_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping {target} ipv4 source {source4} count 5 timeout 1", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping {target} ipv6 source {source6} count 5 timeout 1", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +TNSR_Traceroute = BuiltinDirective( + id="__hyperglass_tnsr_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute {target} ipv4 source {source4} timeout 1 waittime 1", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute {target} ipv6 source {source6} timeout 1 waittime 1", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/defaults/directives/vyos.py b/hyperglass/defaults/directives/vyos.py new file mode 100644 index 000000000..6aec00ace --- /dev/null +++ b/hyperglass/defaults/directives/vyos.py @@ -0,0 +1,112 @@ +"""Default VyOS Directives.""" + +# Project +from hyperglass.models.directive import ( + Text, + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + BuiltinDirective, +) + +__all__ = ( + "VyOS_BGPASPath", + "VyOS_BGPCommunity", + "VyOS_BGPRoute", + "VyOS_Ping", + "VyOS_Traceroute", +) + +NAME = "VyOS" +PLATFORMS = ["vyos"] + +VyOS_BGPRoute = BuiltinDirective( + id="__hyperglass_vyos_bgp_route__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="show bgp ipv4 {target}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="show bgp ipv6 {target}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +VyOS_BGPASPath = BuiltinDirective( + id="__hyperglass_vyos_bgp_aspath__", + name="BGP AS Path", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + 'show bgp ipv4 regexp "{target}"', + 'show bgp ipv6 regexp "{target}"', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=PLATFORMS, +) + +VyOS_BGPCommunity = BuiltinDirective( + id="__hyperglass_vyos_bgp_community__", + name="BGP Community", + rules=[ + RuleWithPattern( + condition="*", + action="permit", + commands=[ + "show bgp ipv4 community {target}", + "show bgp ipv6 community {target}", + ], + ) + ], + field=Text(description="BGP Community String"), + platforms=PLATFORMS, +) + +VyOS_Ping = BuiltinDirective( + id="__hyperglass_vyos_ping__", + name="Ping", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="ping {target} count 5 interface {source4}", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="ping {target} count 5 interface {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) + +VyOS_Traceroute = BuiltinDirective( + id="__hyperglass_vyos_traceroute__", + name="Traceroute", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command="traceroute {target} source-address {source4} icmp", + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command="traceroute {target} source-address {source6} icmp", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/encode.py b/hyperglass/encode.py deleted file mode 100644 index 3b476acf9..000000000 --- a/hyperglass/encode.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Handle JSON Web Token Encoding & Decoding.""" - -# Standard Library -import datetime - -# Third Party -import jwt - -# Project -from hyperglass.exceptions import RestError - - -async def jwt_decode(payload: str, secret: str) -> str: - """Decode & validate an encoded JSON Web Token (JWT).""" - try: - decoded = jwt.decode(payload, secret, algorithm="HS256") - decoded = decoded["payload"] - return decoded - except (KeyError, jwt.PyJWTError) as exp: - raise RestError(str(exp)) from None - - -async def jwt_encode(payload: str, secret: str, duration: int) -> str: - """Encode a query to a JSON Web Token (JWT).""" - token = { - "payload": payload, - "nbf": datetime.datetime.utcnow(), - "iat": datetime.datetime.utcnow(), - "exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=duration), - } - encoded = jwt.encode(token, secret, algorithm="HS256").decode("utf-8") - return encoded diff --git a/hyperglass/examples/devices.yaml b/hyperglass/examples/devices.yaml deleted file mode 100644 index 777500423..000000000 --- a/hyperglass/examples/devices.yaml +++ /dev/null @@ -1,89 +0,0 @@ ---- -routers: - - name: sfo_router01 - address: 10.0.0.1 - network: - name: primary - display_name: Main Network - credential: - username: user1 - password: secret1 - display_name: San Francisco, CA - port: 22 - nos: cisco_ios - vrfs: - - name: default - display_name: Global - ipv4: - source_address: 192.0.2.1 - access_list: - - network: 10.0.0.0/8 - action: deny - - network: 192.168.0.0/16 - action: deny - - network: 172.16.0.0/12 - action: deny - - network: 0.0.0.0/0 - action: permit - ge: 8 - le: 24 - ipv6: - source_address: 2001:db8::1 - access_list: - - network: ::/0 - action: permit - ge: 32 - le: 64 - - name: customer_a - display_name: Customer A - ipv4: - source_address: 192.168.1.1 - access_list: - - network: 192.0.2.0/24 - action: deny - - network: 10.0.0.0/8 - action: permit - ipv6: null - proxy: null - - name: atl_router01 - address: 10.0.0.2 - network: - name: secondary - display_name: That Other Network - credential: - username: user2 - password: secret2 - display_name: Atlanta, GA - port: 22 - nos: juniper - vrfs: - - name: default - display_name: Global - ipv4: - source_address: 192.0.2.2 - access_list: - - network: 10.0.0.0/8 - action: deny - - network: 192.168.0.0/16 - action: deny - - network: 172.16.0.0/12 - action: deny - - network: 0.0.0.0/0 - action: permit - ge: 8 - le: 24 - ipv6: - source_address: 2001:db8::2 - access_list: - - network: ::/0 - action: permit - ge: 32 - le: 64 - proxy: - name: server01 - address: 10.11.6.204 - port: 22 - credential: - username: user1 - password: secret1 - nos: linux_ssh diff --git a/hyperglass/examples/hyperglass.docker.yaml b/hyperglass/examples/hyperglass.docker.yaml deleted file mode 100644 index 3a9d152d8..000000000 --- a/hyperglass/examples/hyperglass.docker.yaml +++ /dev/null @@ -1,11 +0,0 @@ -cache: - host: localhost - port: 6379 - timeout: 15 -listen_address: 0.0.0.0 -web: - links: - - title: "X4B Looking Glass" - url: "https://lg.x4b.net" - - title: "X4B" - url: "https://x4b.net" \ No newline at end of file diff --git a/hyperglass/examples/hyperglass.yaml b/hyperglass/examples/hyperglass.yaml deleted file mode 100644 index e69de29bb..000000000 diff --git a/hyperglass/exceptions.py b/hyperglass/exceptions.py deleted file mode 100644 index 8aee847fa..000000000 --- a/hyperglass/exceptions.py +++ /dev/null @@ -1,190 +0,0 @@ -"""Custom exceptions for hyperglass.""" - -# Standard Library -import json as _json -from typing import Dict, List, Union, Optional - -# Project -from hyperglass.log import log -from hyperglass.constants import STATUS_CODE_MAP - - -def validation_error_message(*errors: Dict) -> str: - """Parse errors return from pydantic.ValidationError.errors().""" - - errs = ("\n",) - - for err in errors: - loc = " → ".join(str(loc) for loc in err["loc"]) - errs += (f'Field: {loc}\n Error: {err["msg"]}\n',) - - return "\n".join(errs) - - -class HyperglassError(Exception): - """hyperglass base exception.""" - - def __init__( - self, - message: str = "", - level: str = "warning", - keywords: Optional[List[str]] = None, - ) -> None: - """Initialize the hyperglass base exception class.""" - self._message = message - self._level = level - self._keywords = keywords or [] - if self._level == "warning": - log.error(repr(self)) - elif self._level == "danger": - log.critical(repr(self)) - else: - log.info(repr(self)) - - def __str__(self) -> str: - """Return the instance's error message.""" - return self._message - - def __repr__(self) -> str: - """Return the instance's severity & error message in a string.""" - return f"[{self.level.upper()}] {self._message}" - - def dict(self) -> Dict: - """Return the instance's attributes as a dictionary.""" - return { - "message": self._message, - "level": self._level, - "keywords": self._keywords, - } - - def json(self) -> str: - """Return the instance's attributes as a JSON object.""" - return _json.dumps(self.__dict__()) - - @property - def message(self) -> str: - """Return the instance's `message` attribute.""" - return self._message - - @property - def level(self) -> str: - """Return the instance's `level` attribute.""" - return self._level - - @property - def keywords(self) -> List[str]: - """Return the instance's `keywords` attribute.""" - return self._keywords - - @property - def status_code(self) -> int: - """Return HTTP status code based on level level.""" - return STATUS_CODE_MAP.get(self._level, 500) - - -class _UnformattedHyperglassError(HyperglassError): - """Base exception class for freeform error messages.""" - - _level = "warning" - - def __init__( - self, unformatted_msg: str = "", level: Optional[str] = None, **kwargs - ) -> None: - """Format error message with keyword arguments.""" - self._message = unformatted_msg.format(**kwargs) - self._level = level or self._level - self._keywords = list(kwargs.values()) - super().__init__( - message=self._message, level=self._level, keywords=self._keywords - ) - - -class _PredefinedHyperglassError(HyperglassError): - _message = "undefined" - _level = "warning" - - def __init__(self, level: Optional[str] = None, **kwargs) -> None: - self._fmt_msg = self._message.format(**kwargs) - self._level = level or self._level - self._keywords = list(kwargs.values()) - super().__init__( - message=self._fmt_msg, level=self._level, keywords=self._keywords - ) - - -class ConfigInvalid(HyperglassError): - """Raised when a config item fails type or option validation.""" - - def __init__(self, errors: List[str]) -> None: - """Parse Pydantic ValidationError.""" - - super().__init__(message=validation_error_message(*errors)) - - -class ConfigError(_UnformattedHyperglassError): - """Raised for generic user-config issues.""" - - -class ConfigMissing(_PredefinedHyperglassError): - """Raised when a required config file or item is missing or undefined.""" - - _message = ( - "{missing_item} is missing or undefined and is required to start " - "hyperglass. Please consult the installation documentation." - ) - - -class ScrapeError(_UnformattedHyperglassError): - """Raised when a scrape/netmiko error occurs.""" - - _level = "danger" - - -class AuthError(_UnformattedHyperglassError): - """Raised when authentication to a device fails.""" - - _level = "danger" - - -class RestError(_UnformattedHyperglassError): - """Raised upon a rest API client error.""" - - _level = "danger" - - -class DeviceTimeout(_UnformattedHyperglassError): - """Raised when the connection to a device times out.""" - - _level = "danger" - - -class InputInvalid(_UnformattedHyperglassError): - """Raised when input validation fails.""" - - -class InputNotAllowed(_UnformattedHyperglassError): - """Raised when input validation fails due to a configured check.""" - - -class ResponseEmpty(_UnformattedHyperglassError): - """Raised when hyperglass can connect to the device but the response is empty.""" - - -class UnsupportedDevice(_UnformattedHyperglassError): - """Raised when an input NOS is not in the supported NOS list.""" - - -class ParsingError(_UnformattedHyperglassError): - """Raised when there is a problem parsing a structured response.""" - - def __init__( - self, unformatted_msg: Union[List[Dict], str], level: str = "danger", **kwargs, - ) -> None: - """Format error message with keyword arguments.""" - if isinstance(unformatted_msg, list): - self._message = validation_error_message(*unformatted_msg) - else: - self._message = unformatted_msg.format(**kwargs) - self._level = level or self._level - self._keywords = list(kwargs.values()) - super().__init__(self._message, level=self._level, keywords=self._keywords) diff --git a/hyperglass/exceptions/__init__.py b/hyperglass/exceptions/__init__.py new file mode 100644 index 000000000..0f3fb9ef6 --- /dev/null +++ b/hyperglass/exceptions/__init__.py @@ -0,0 +1,10 @@ +"""Custom exceptions for hyperglass.""" + +# Local +from ._common import HyperglassError, PublicHyperglassError, PrivateHyperglassError + +__all__ = ( + "HyperglassError", + "PublicHyperglassError", + "PrivateHyperglassError", +) diff --git a/hyperglass/exceptions/_common.py b/hyperglass/exceptions/_common.py new file mode 100644 index 000000000..1f04f941f --- /dev/null +++ b/hyperglass/exceptions/_common.py @@ -0,0 +1,191 @@ +"""Custom exceptions for hyperglass.""" + +# Standard Library +import json as _json +from typing import Any, Set, Dict, List, Union, Literal, Optional + +# Third Party +from pydantic import ValidationError + +# Project +from hyperglass.log import log +from hyperglass.util import get_fmt_keys, repr_from_attrs +from hyperglass.constants import STATUS_CODE_MAP + +ErrorLevel = Literal["danger", "warning"] + + +class HyperglassError(Exception): + """hyperglass base exception.""" + + def __init__( + self, + message: str = "", + level: ErrorLevel = "warning", + keywords: Optional[List[str]] = None, + ) -> None: + """Initialize the hyperglass base exception class.""" + self._message = message + self._level = level + self._keywords = keywords or [] + if self._level == "warning": + log.error(str(self)) + elif self._level == "danger": + log.critical(str(self)) + else: + log.info(str(self)) + + def __str__(self) -> str: + """Return the instance's error message.""" + return self._message + + def __repr__(self) -> str: + """Return the instance's severity & error message in a string.""" + return repr_from_attrs(self, ("_message", "level", "keywords"), strip="_") + + def dict(self) -> Dict[str, Union[str, List[str]]]: + """Return the instance's attributes as a dictionary.""" + return { + "message": self._message, + "level": self._level, + "keywords": self.keywords, + } + + def json(self) -> str: + """Return the instance's attributes as a JSON object.""" + return _json.dumps(self.__dict__()) + + @staticmethod + def _safe_format(template: str, **kwargs: Dict[str, str]) -> str: + """Safely format a string template from keyword arguments.""" + + keys = get_fmt_keys(template) + for key in keys: + if key not in kwargs: + kwargs.pop(key) + else: + kwargs[key] = str(kwargs[key]) + return template.format(**kwargs) + + def _parse_pydantic_errors(*errors: Dict[str, Any]) -> str: + errs = ("\n",) + + for err in errors: + loc = " → ".join(str(loc) for loc in err["loc"]) + errs += (f"Field: {loc}\n Error: {err['msg']}\n",) + + return "\n".join(errs) + + def _process_keywords(self) -> None: + out: Set[str] = set() + for val in self._keywords: + if isinstance(val, str): + out.add(val) + elif isinstance(val, list): + for v in val: + out.add(v) + else: + out.add(str(val)) + self._keywords = list(out) + + @property + def message(self) -> str: + """Return the instance's `message` attribute.""" + return self._message + + @property + def level(self) -> str: + """Return the instance's `level` attribute.""" + return self._level + + @property + def keywords(self) -> List[str]: + """Return the instance's `keywords` attribute.""" + self._process_keywords() + return self._keywords + + @property + def status_code(self) -> int: + """Return HTTP status code based on level level.""" + return STATUS_CODE_MAP.get(self._level, 500) + + +class PublicHyperglassError(HyperglassError): + """Base exception class for user-facing errors. + + Error text should be defined in + `hyperglass.configuration.params.messages` and associated with the + exception class at start time. + """ + + _level = "warning" + _message_template = "Something went wrong." + _original_template_name: str = "" + + def __init_subclass__( + cls, *, template: Optional[str] = None, level: Optional[ErrorLevel] = None + ) -> None: + """Override error attributes from subclass.""" + + if template is not None: + cls._message_template = template + cls._original_template_name = template + if level is not None: + cls._level = level + + def __init__(self, **kwargs: str) -> None: + """Format error message with keyword arguments.""" + # Project + from hyperglass.state import use_state + + if "error" in kwargs: + error = kwargs.pop("error") + error = self._safe_format(str(error), **kwargs) + kwargs["error"] = error + + template = self._message_template + + (messages := use_state("params").messages) + if messages.has(self._original_template_name): + template = messages[self._original_template_name] + if "error" in kwargs and "({error})" not in template: + template += " ({error})" + self._message = self._safe_format(template, **kwargs) + self._keywords = list(kwargs.values()) + super().__init__(message=self._message, level=self._level, keywords=self._keywords) + + +class PrivateHyperglassError(HyperglassError): + """Base exception class for internal system errors. + + Error text is dynamic based on the exception being caught. + """ + + _level = "warning" + + def _parse_validation_error(self, err: ValidationError) -> str: + errors = err.errors() + parsed = { + k: ", ".join(str(loc) for t in errors for loc in t["loc"] if t["type"] == k) + for k in {e["type"] for e in errors} + } + return ", ".join(parsed.values()) + + def __init_subclass__(cls, *, level: Optional[ErrorLevel] = None) -> None: + """Override error attributes from subclass.""" + if level is not None: + cls._level = level + + def __init__(self, message: str, **kwargs: Any) -> None: + """Format error message with keyword arguments.""" + if "error" in kwargs: + error = kwargs.pop("error") + error = self._safe_format(str(error), **kwargs) + kwargs["error"] = error + + if isinstance(message, ValidationError): + message = self._parse_validation_error(message) + + self._message = self._safe_format(message, **kwargs) + self._keywords = list(kwargs.values()) + super().__init__(message=self._message, level=self._level, keywords=self._keywords) diff --git a/hyperglass/exceptions/private.py b/hyperglass/exceptions/private.py new file mode 100644 index 000000000..80b5c8dd0 --- /dev/null +++ b/hyperglass/exceptions/private.py @@ -0,0 +1,115 @@ +"""Internal/private exceptions.""" + +# Standard Library +from typing import Any, Dict, List +from pathlib import Path + +# Project +from hyperglass.constants import CONFIG_EXTENSIONS + +# Local +from ._common import ErrorLevel, PrivateHyperglassError + + +class ExternalError(PrivateHyperglassError): + """Raised when an error during a connection to an external service occurs.""" + + def __init__(self, message: str, level: ErrorLevel, **kwargs: Dict[str, Any]) -> None: + """Set level according to level argument.""" + self._level = level + super().__init__(message, **kwargs) + + +class UnsupportedDevice(PrivateHyperglassError): + """Raised when an input platform is not in the supported platform list.""" + + def __init__(self, platform: str) -> None: + """Show the unsupported device type and a list of supported drivers.""" + # Third Party + from netmiko.ssh_dispatcher import CLASS_MAPPER # type: ignore + + # Project + from hyperglass.constants import DRIVER_MAP + + sorted_drivers = sorted([*DRIVER_MAP.keys(), *CLASS_MAPPER.keys()]) + driver_list = "\n - ".join(("", *sorted_drivers)) + super().__init__(message=f"'{platform}' is not supported. Must be one of:{driver_list}") + + +class InputValidationError(PrivateHyperglassError): + """Raised when a validation check fails. + + This needs to be separate from `hyperglass.exceptions.public` for + circular import reasons. + """ + + kwargs: Dict[str, Any] + + def __init__(self, **kwargs: Dict[str, Any]) -> None: + """Set kwargs instance attribute so it can be consumed later. + + `hyperglass.exceptions.public.InputInvalid` will be raised from + these kwargs. + """ + self.kwargs = kwargs + super().__init__(message="", **kwargs) + + +class ConfigInvalid(PrivateHyperglassError): + """Raised when a config item fails type or option validation.""" + + def __init__(self, errors: List[Dict[str, Any]]) -> None: + """Parse Pydantic ValidationError.""" + + super().__init__(message=self._parse_pydantic_errors(*errors)) + + +class ConfigMissing(PrivateHyperglassError): + """Raised when a required config file or item is missing or undefined.""" + + def __init__(self, file_name: str, *, app_path: Path) -> None: + """Customize error message.""" + message = " ".join( + ( + file_name.capitalize(), + "file is missing in", + f"'{app_path!s}', and is required to start hyperglass.", + "Supported file names are:", + ", ".join(f"'{file_name}.{e}'" for e in CONFIG_EXTENSIONS), + ". Please consult the installation documentation.", + ) + ) + super().__init__(message) + + +class ConfigLoaderMissing(PrivateHyperglassError): + """Raised when a configuration file is using a file extension that requires a missing loader.""" + + def __init__(self, path: Path, /) -> None: + """Customize error message.""" + message = "'{path}' requires a {loader} loader, but it is not installed" + super().__init__(message=message, path=path, loader=path.suffix.strip(".")) + + +class ConfigError(PrivateHyperglassError): + """Raised for generic user-config issues.""" + + +class UnsupportedError(PrivateHyperglassError): + """Raised when an unsupported action or request occurs.""" + + +class ParsingError(PrivateHyperglassError): + """Raised when there is a problem parsing a structured response.""" + + +class DependencyError(PrivateHyperglassError): + """Raised when a dependency is missing, not running, or on the wrong version.""" + + +class PluginError(PrivateHyperglassError): + """Raised when a plugin error occurs.""" + + +class StateError(PrivateHyperglassError): + """Raised when an error occurs while fetching state from Redis.""" diff --git a/hyperglass/exceptions/public.py b/hyperglass/exceptions/public.py new file mode 100644 index 000000000..912e63ba5 --- /dev/null +++ b/hyperglass/exceptions/public.py @@ -0,0 +1,148 @@ +"""User-facing/Public exceptions.""" + +# Standard Library +from typing import TYPE_CHECKING, Any, Dict, Optional + +# Local +from ._common import PublicHyperglassError + +if TYPE_CHECKING: + # Project + from hyperglass.models.api.query import Query + from hyperglass.models.config.devices import Device + + +class ScrapeError( + PublicHyperglassError, + template="connection_error", + level="danger", +): + """Raised when an SSH driver error occurs.""" + + def __init__(self, *, error: BaseException, device: "Device"): + """Initialize parent error.""" + super().__init__(error=str(error), device=device.name, proxy=device.proxy) + + +class AuthError(PublicHyperglassError, template="authentication_error", level="danger"): + """Raised when authentication to a device fails.""" + + def __init__(self, *, error: BaseException, device: "Device"): + """Initialize parent error.""" + super().__init__(error=str(error), device=device.name, proxy=device.proxy) + + +class RestError(PublicHyperglassError, template="connection_error", level="danger"): + """Raised upon a rest API client error.""" + + def __init__(self, *, error: BaseException, device: "Device"): + """Initialize parent error.""" + super().__init__(error=str(error), device=device.name) + + +class DeviceTimeout(PublicHyperglassError, template="request_timeout", level="danger"): + """Raised when the connection to a device times out.""" + + def __init__(self, *, error: BaseException, device: "Device"): + """Initialize parent error.""" + super().__init__(error=str(error), device=device.name, proxy=device.proxy) + + +class InvalidQuery(PublicHyperglassError, template="request_timeout"): + """Raised when input validation fails.""" + + def __init__( + self, *, error: Optional[str] = None, query: "Query", **kwargs: Dict[str, Any] + ) -> None: + """Initialize parent error.""" + + kwargs = { + "query_type": query.query_type, + "target": query.query_target, + "error": str(error), + **kwargs, + } + + super().__init__(**kwargs) + + +class NotFound(PublicHyperglassError, template="not_found"): + """Raised when an object is not found.""" + + def __init__(self, type: str, name: str, **kwargs: Dict[str, str]) -> None: + """Initialize parent error.""" + super().__init__(type=type, name=name, **kwargs) + + +class QueryLocationNotFound(NotFound): + """Raised when a query location is not found.""" + + def __init__(self, location: Any, **kwargs: Dict[str, Any]) -> None: + """Initialize a NotFound error for a query location.""" + # Project + from hyperglass.state import use_state + + (text := use_state("params").web.text) + + super().__init__(type=text.query_location, name=str(location), **kwargs) + + +class QueryTypeNotFound(NotFound): + """Raised when a query type is not found.""" + + def __init__(self, query_type: Any, **kwargs: Dict[str, Any]) -> None: + """Initialize a NotFound error for a query type.""" + # Project + from hyperglass.state import use_state + + (text := use_state("params").web.text) + super().__init__(type=text.query_type, name=str(query_type), **kwargs) + + +class InputInvalid(PublicHyperglassError, template="invalid_input"): + """Raised when input validation fails.""" + + def __init__( + self, *, error: Optional[Any] = None, target: str, **kwargs: Dict[str, Any] + ) -> None: + """Initialize parent error.""" + + kwargs = {"target": target, "error": str(error), **kwargs} + + super().__init__(**kwargs) + + +class InputNotAllowed(PublicHyperglassError, template="target_not_allowed"): + """Raised when input validation fails due to a configured check.""" + + def __init__( + self, *, error: Optional[str] = None, query: "Query", **kwargs: Dict[str, Any] + ) -> None: + """Initialize parent error.""" + + kwargs = { + "query_type": query.query_type, + "target": query.query_target, + "error": str(error), + **kwargs, + } + + super().__init__(**kwargs) + + +class ResponseEmpty(PublicHyperglassError, template="no_output"): + """Raised when hyperglass can connect to the device but the response is empty.""" + + def __init__( + self, *, error: Optional[str] = None, query: "Query", **kwargs: Dict[str, Any] + ) -> None: + """Initialize parent error.""" + + kwargs = { + "query_type": query.query_type, + "target": query.query_target, + "error": str(error), + **kwargs, + } + + super().__init__(**kwargs) diff --git a/hyperglass/execution/drivers/__init__.py b/hyperglass/execution/drivers/__init__.py index 73a6ada23..1ebbab45e 100644 --- a/hyperglass/execution/drivers/__init__.py +++ b/hyperglass/execution/drivers/__init__.py @@ -1,7 +1,12 @@ """Individual transport driver classes & subclasses.""" # Local -from .agent import AgentConnection from ._common import Connection +from .http_client import HttpClient from .ssh_netmiko import NetmikoConnection -from .ssh_scrapli import ScrapliConnection + +__all__ = ( + "Connection", + "HttpClient", + "NetmikoConnection", +) diff --git a/hyperglass/execution/drivers/_common.py b/hyperglass/execution/drivers/_common.py index 14b425ad1..bec677888 100644 --- a/hyperglass/execution/drivers/_common.py +++ b/hyperglass/execution/drivers/_common.py @@ -1,81 +1,48 @@ """Base Connection Class.""" # Standard Library -from typing import Dict, Union, Sequence +import typing as t +from abc import ABC, abstractmethod # Project -from hyperglass.log import log -from hyperglass.models.api import Query -from hyperglass.parsing.nos import scrape_parsers, structured_parsers -from hyperglass.parsing.common import parsers -from hyperglass.models.config.devices import Device +from hyperglass.types import Series +from hyperglass.plugins import OutputPluginManager # Local from ._construct import Construct +if t.TYPE_CHECKING: + # Project + from hyperglass.compat import SSHTunnelForwarder + from hyperglass.models.api import Query + from hyperglass.models.data import OutputDataModel + from hyperglass.models.config.devices import Device -class Connection: + +class Connection(ABC): """Base transport driver class.""" - def __init__(self, device: Device, query_data: Query) -> None: + def __init__(self, device: "Device", query_data: "Query") -> None: """Initialize connection to device.""" self.device = device self.query_data = query_data self.query_type = self.query_data.query_type self.query_target = self.query_data.query_target - self._query = Construct(device=self.device, query_data=self.query_data) + self._query = Construct(device=self.device, query=self.query_data) self.query = self._query.queries() + self.plugin_manager = OutputPluginManager() - async def parsed_response( # noqa: C901 ("too complex") - self, output: Sequence[str] - ) -> Union[str, Sequence[Dict]]: - """Send output through common parsers.""" - - log.debug("Pre-parsed responses:\n{}", output) - parsed = () - response = None + @abstractmethod + def setup_proxy(self: "Connection") -> "SSHTunnelForwarder": + """Return a preconfigured sshtunnel.SSHTunnelForwarder instance.""" + pass - structured_nos = structured_parsers.keys() - structured_query_types = structured_parsers.get(self.device.nos, {}).keys() - - scrape_nos = scrape_parsers.keys() - scrape_query_types = scrape_parsers.get(self.device.nos, {}).keys() - - if not self.device.structured_output: - _parsed = () - for func in parsers: - for response in output: - _output = func(commands=self.query, output=response) - _parsed += (_output,) - if self.device.nos in scrape_nos and self.query_type in scrape_query_types: - func = scrape_parsers[self.device.nos][self.query_type] - for response in _parsed: - _output = func(response) - parsed += (_output,) - else: - parsed += _parsed + async def response(self, output: Series[str]) -> t.Union["OutputDataModel", str]: + """Send output through common parsers.""" - response = "\n\n".join(parsed) - elif ( - self.device.structured_output - and self.device.nos in structured_nos - and self.query_type not in structured_query_types - ): - for func in parsers: - for response in output: - _output = func(commands=self.query, output=response) - parsed += (_output,) - response = "\n\n".join(parsed) - elif ( - self.device.structured_output - and self.device.nos in structured_nos - and self.query_type in structured_query_types - ): - func = structured_parsers[self.device.nos][self.query_type] - response = func(output) + response = self.plugin_manager.execute(output=output, query=self.query_data) if response is None: - response = "\n\n".join(output) + response = () - log.debug("Post-parsed responses:\n{}", response) return response diff --git a/hyperglass/execution/drivers/_construct.py b/hyperglass/execution/drivers/_construct.py index ad3f41cc3..c1d1e2b5b 100644 --- a/hyperglass/execution/drivers/_construct.py +++ b/hyperglass/execution/drivers/_construct.py @@ -8,117 +8,136 @@ # Standard Library import re import json as _json -from operator import attrgetter +import typing as t +import ipaddress # Project from hyperglass.log import log +from hyperglass.util import get_fmt_keys from hyperglass.constants import TRANSPORT_REST, TARGET_FORMAT_SPACE -from hyperglass.configuration import commands +from hyperglass.exceptions.public import InputInvalid +from hyperglass.exceptions.private import ConfigError + +if t.TYPE_CHECKING: + # Third Party + from loguru import Logger + + # Project + from hyperglass.models.api.query import Query + from hyperglass.models.directive import Directive + from hyperglass.models.config.devices import Device + +FormatterCallback = t.Callable[[str], t.Union[t.List[str], str]] class Construct: """Construct SSH commands/REST API parameters from validated query data.""" - def __init__(self, device, query_data): + directive: "Directive" + device: "Device" + query: "Query" + transport: str + target: str + _log: "Logger" + + def __init__(self, device: "Device", query: "Query"): """Initialize command construction.""" - log.debug( - "Constructing {} query for '{}'", - query_data.query_type, - str(query_data.query_target), - ) + self._log = log.bind(type=query.query_type, target=query.query_target) + self._log.debug("Constructing query") + self.query = query self.device = device - self.query_data = query_data - self.target = self.query_data.query_target + self.target = self.query.query_target + self.directive = query.directive # Set transport method based on NOS type self.transport = "scrape" - if self.device.nos in TRANSPORT_REST: + if self.device.platform in TRANSPORT_REST: self.transport = "rest" # Remove slashes from target for required platforms - if self.device.nos in TARGET_FORMAT_SPACE: - self.target = re.sub(r"\/", r" ", str(self.query_data.query_target)) - - # Set AFIs for based on query type - if self.query_data.query_type in ("bgp_route", "ping", "traceroute"): - # For IP queries, AFIs are enabled (not null/None) VRF -> AFI definitions - # where the IP version matches the IP version of the target. - self.afis = [ - v - for v in ( - self.query_data.query_vrf.ipv4, - self.query_data.query_vrf.ipv6, - ) - if v is not None and self.query_data.query_target.version == v.version - ] - elif self.query_data.query_type in ("bgp_aspath", "bgp_community"): - # For AS Path/Community queries, AFIs are just enabled VRF -> AFI - # definitions, no IP version checking is performed (since there is no IP). - self.afis = [ - v - for v in ( - self.query_data.query_vrf.ipv4, - self.query_data.query_vrf.ipv6, - ) - if v is not None - ] + if self.device.platform in TARGET_FORMAT_SPACE: + self.target = re.sub(r"\/", r" ", str(self.query.query_target)) + + with Formatter(self.query) as formatter: + self.target = formatter(self.prepare_target()) + + def prepare_target(self) -> t.Union[t.List[str], str]: + """Format the query target based on directive parameters.""" + if isinstance(self.query.query_target, t.List): + # Directive can accept multiple values in a single command. + if self.directive.multiple: + return self.directive.multiple_separator.join(self.query.query_target) + # Target is an array of one, return single item. + if len(self.query.query_target) == 1: + return self.query.query_target[0] + # Directive commands should be run once for each item in the target. - with Formatter(self.device.nos, self.query_data.query_type) as formatter: - self.target = formatter(self.query_data.query_target) + return self.query.query_target def json(self, afi): """Return JSON version of validated query for REST devices.""" - log.debug("Building JSON query for {q}", q=repr(self.query_data)) + self._log.debug("Building JSON query") return _json.dumps( { - "query_type": self.query_data.query_type, - "vrf": self.query_data.query_vrf.name, + "query_type": self.query.query_type, + "vrf": self.query.query_vrf.name, "afi": afi.protocol, "source": str(afi.source_address), "target": str(self.target), } ) - def scrape(self, afi): + def format(self, command: str) -> str: """Return formatted command for 'Scrape' endpoints (SSH).""" - if self.device.structured_output: - cmd_paths = ( - self.device.nos, - "structured", - afi.protocol, - self.query_data.query_type, - ) - else: - cmd_paths = (self.device.commands, afi.protocol, self.query_data.query_type) - - command = attrgetter(".".join(cmd_paths))(commands) - return command.format( - target=self.target, - source=str(afi.source_address), - vrf=self.query_data.query_vrf.name, - ) + keys = get_fmt_keys(command) + attrs = {k: v for k, v in self.device.attrs.items() if k in keys} + for key in [k for k in keys if k != "target" and k != "mask"]: + if key not in attrs: + raise ConfigError( + ("Command '{c}' has attribute '{k}', which is missing from device '{d}'"), + level="danger", + c=self.directive.name, + k=key, + d=self.device.name, + ) + + mask = ipaddress.ip_address("255.255.255.255") + try: + network = ipaddress.ip_network(self.target) + if network.version == 4 and network.network_address != network.broadcast_address: + # Network is an IPv4 network with more than one host. + mask = network.netmask + except ValueError: + pass + + return command.format(target=self.target, mask=mask, **attrs) def queries(self): """Return queries for each enabled AFI.""" query = [] - for afi in self.afis: - if self.transport == "rest": - query.append(self.json(afi=afi)) - else: - query.append(self.scrape(afi=afi)) + rules = [r for r in self.directive.rules if r._passed is True] + if len(rules) < 1: + raise InputInvalid( + error="No validation rules matched target '{target}'", + target=self.query.query_target, + ) - log.debug("Constructed query: {}", query) + for rule in [r for r in self.directive.rules if r._passed is True]: + for command in rule.commands: + query.append(self.format(command)) + self._log.bind(constructed_query=query).debug("Constructed query") return query class Formatter: """Modify query target based on the device's NOS requirements and the query type.""" - def __init__(self, nos: str, query_type: str) -> None: + def __init__(self, query: "Query") -> None: """Initialize target formatting.""" - self.nos = nos - self.query_type = query_type + self.query = query + self.platform = query.device.platform + self.query_type = query.query_type def __enter__(self): """Get the relevant formatter.""" @@ -131,20 +150,27 @@ def __exit__(self, exc_type, exc_value, exc_traceback): pass def _get_formatter(self): - if self.nos in ("juniper", "juniper_junos"): + if self.platform in ("juniper", "juniper_junos"): if self.query_type == "bgp_aspath": - return self._juniper_bgp_aspath - if self.nos in ("bird", "bird_ssh"): + return self._with_formatter(self._juniper_bgp_aspath) + if self.platform in ("bird", "bird_ssh"): if self.query_type == "bgp_aspath": - return self._bird_bgp_aspath - elif self.query_type == "bgp_community": - return self._bird_bgp_community - return self._default + return self._with_formatter(self._bird_bgp_aspath) + if self.query_type == "bgp_community": + return self._with_formatter(self._bird_bgp_community) + return self._with_formatter(self._default) def _default(self, target: str) -> str: """Don't format targets by default.""" return target + def _with_formatter(self, formatter: t.Callable[[str], str]) -> FormatterCallback: + result: FormatterCallback + if isinstance(self.query.query_target, t.List): + result = lambda s: [formatter(i) for i in s] + result = lambda s: formatter(s) + return result + def _juniper_bgp_aspath(self, target: str) -> str: """Convert from Cisco AS_PATH format to Juniper format.""" query = str(target) @@ -163,7 +189,7 @@ def _juniper_bgp_aspath(self, target: str) -> str: if was_modified: modified = " ".join(asns) - log.debug("Modified target '{}' to '{}'", target, modified) + log.bind(original=target, modified=modified).debug("Modified target") return modified return query @@ -191,11 +217,11 @@ def _bird_bgp_aspath(self, target: str) -> str: result = " ".join(asns) if was_modified: - log.debug("Modified target '{}' to '{}'", target, result) + log.bind(original=target, modified=result).debug("Modified target") return result def _bird_bgp_community(self, target: str) -> str: """Convert from standard community format to BIRD format.""" parts = target.split(":") - return f'({",".join(parts)})' + return f"({','.join(parts)})" diff --git a/hyperglass/execution/drivers/agent.py b/hyperglass/execution/drivers/agent.py deleted file mode 100644 index 3800e9b86..000000000 --- a/hyperglass/execution/drivers/agent.py +++ /dev/null @@ -1,139 +0,0 @@ -"""Execute validated & constructed query on device. - -Accepts input from front end application, validates the input and -returns errors if input is invalid. Passes validated parameters to -construct.py, which is used to build & run the Netmiko connections or -hyperglass-frr API calls, returns the output back to the front end. -""" - -# Standard Library -from ssl import CertificateError -from typing import Iterable - -# Third Party -import httpx - -# Project -from hyperglass.log import log -from hyperglass.util import parse_exception -from hyperglass.encode import jwt_decode, jwt_encode -from hyperglass.exceptions import RestError, ResponseEmpty -from hyperglass.configuration import params - -# Local -from ._common import Connection - - -class AgentConnection(Connection): - """Connect to target device via hyperglass-agent.""" - - async def collect(self) -> Iterable: # noqa: C901 - """Connect to a device running hyperglass-agent via HTTP.""" - log.debug("Query parameters: {}", self.query) - - client_params = { - "headers": {"Content-Type": "application/json"}, - "timeout": params.request_timeout, - } - if self.device.ssl is not None and self.device.ssl.enable: - with self.device.ssl.cert.open("r") as file: - cert = file.read() - if not cert: - raise RestError( - "SSL Certificate for device {d} has not been imported", - level="danger", - d=self.device.name, - ) - http_protocol = "https" - client_params.update({"verify": str(self.device.ssl.cert)}) - log.debug( - ( - f"Using {str(self.device.ssl.cert)} to validate connection " - f"to {self.device.name}" - ) - ) - else: - http_protocol = "http" - endpoint = "{protocol}://{address}:{port}/query/".format( - protocol=http_protocol, address=self.device._target, port=self.device.port - ) - - log.debug("URL endpoint: {}", endpoint) - - try: - async with httpx.AsyncClient(**client_params) as http_client: - responses = () - - for query in self.query: - encoded_query = await jwt_encode( - payload=query, - secret=self.device.credential.password.get_secret_value(), - duration=params.request_timeout, - ) - log.debug("Encoded JWT: {}", encoded_query) - - raw_response = await http_client.post( - endpoint, json={"encoded": encoded_query} - ) - log.debug("HTTP status code: {}", raw_response.status_code) - - raw = raw_response.text - log.debug("Raw Response:\n{}", raw) - - if raw_response.status_code == 200: - decoded = await jwt_decode( - payload=raw_response.json()["encoded"], - secret=self.device.credential.password.get_secret_value(), - ) - log.debug("Decoded Response:\n{}", decoded) - responses += (decoded,) - - elif raw_response.status_code == 204: - raise ResponseEmpty( - params.messages.no_output, device_name=self.device.name, - ) - - else: - log.error(raw_response.text) - - except httpx.exceptions.HTTPError as rest_error: - msg = parse_exception(rest_error) - log.error("Error connecting to device {}: {}", self.device.name, msg) - raise RestError( - params.messages.connection_error, - device_name=self.device.name, - error=msg, - ) - except OSError as ose: - log.critical(str(ose)) - raise RestError( - params.messages.connection_error, - device_name=self.device.name, - error="System error", - ) - except CertificateError as cert_error: - log.critical(str(cert_error)) - msg = parse_exception(cert_error) - raise RestError( - params.messages.connection_error, - device_name=self.device.name, - error=f"{msg}: {cert_error}", - ) - - if raw_response.status_code != 200: - log.error("Response code is {}", raw_response.status_code) - raise RestError( - params.messages.connection_error, - device_name=self.device.name, - error=params.messages.general, - ) - - if not responses: - log.error("No response from device {}", self.device.name) - raise RestError( - params.messages.connection_error, - device_name=self.device.name, - error=params.messages.no_response, - ) - - return responses diff --git a/hyperglass/execution/drivers/http_client.py b/hyperglass/execution/drivers/http_client.py new file mode 100644 index 000000000..c2ad2ae31 --- /dev/null +++ b/hyperglass/execution/drivers/http_client.py @@ -0,0 +1,117 @@ +"""Interact with an http-based device.""" + +# Standard Library +import typing as t + +# Third Party +import httpx + +# Project +from hyperglass.util import get_fmt_keys +from hyperglass.exceptions.public import AuthError, RestError, DeviceTimeout, ResponseEmpty + +# Local +from ._common import Connection + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.api import Query + from hyperglass.models.config.devices import Device + from hyperglass.models.config.http_client import HttpConfiguration + + +class HttpClient(Connection): + """Interact with an http-based device.""" + + config: "HttpConfiguration" + client: httpx.AsyncClient + + def __init__(self, device: "Device", query_data: "Query") -> None: + """Initialize base connection and set http config & client.""" + super().__init__(device, query_data) + self.config = device.http + self.client = self.config.create_client(device=device) + + def setup_proxy(self: "Connection"): + """HTTP Client does not support SSH proxies.""" + raise NotImplementedError("HTTP Client does not support SSH proxies.") + + def _query_params(self) -> t.Dict[str, str]: + if self.config.query is None: + return { + self.config._attribute_map.query_target: self.query_data.query_target, + self.config._attribute_map.query_location: self.query_data.query_location, + self.config._attribute_map.query_type: self.query_data.query_type, + } + if isinstance(self.config.query, t.Dict): + return { + key: value.format( + **{ + str(v): str(getattr(self.query_data, k, None)) + for k, v in self.config.attribute_map.model_dump().items() + if v in get_fmt_keys(value) + } + ) + for key, value in self.config.query.items() + } + return {} + + def _body(self) -> t.Dict[str, t.Union[t.Dict[str, t.Any], str]]: + data = { + self.config._attribute_map.query_target: self.query_data.query_target, + self.config._attribute_map.query_location: self.query_data.query_location, + self.config._attribute_map.query_type: self.query_data.query_type, + } + if self.config.body_format == "json": + return {"json": data} + + if self.config.body_format == "yaml": + # Third Party + import yaml + + return {"content": yaml.dump(data), "headers": {"content-type": "text/yaml"}} + + if self.config.body_format == "xml": + # Third Party + import xmltodict # type: ignore + + return { + "content": xmltodict.unparse({"query": data}), + "headers": {"content-type": "application/xml"}, + } + if self.config.body_format == "text": + return {"data": data} + + return {} + + async def collect(self, *args: t.Any, **kwargs: t.Any) -> t.Iterable: + """Collect response data from an HTTP endpoint.""" + + query = self._query_params() + responses = () + + async with self.client as client: + body = {} + if self.config.method in ("POST", "PATCH", "PUT"): + body = self._body() + + try: + response: httpx.Response = await client.request( + method=self.config.method, url=self.config.path, params=query, **body + ) + response.raise_for_status() + data = response.text.strip() + + if len(data) == 0: + raise ResponseEmpty(query=self.query_data) + + responses += (data,) + + except httpx.TimeoutException as error: + raise DeviceTimeout(error=error, device=self.device) from error + + except httpx.HTTPStatusError as error: + if error.response.status_code == 401: + raise AuthError(error=error, device=self.device) from error + raise RestError(error=error, device=self.device) from error + return responses diff --git a/hyperglass/execution/drivers/ssh.py b/hyperglass/execution/drivers/ssh.py index 0c73cafb0..90401d8f9 100644 --- a/hyperglass/execution/drivers/ssh.py +++ b/hyperglass/execution/drivers/ssh.py @@ -1,25 +1,30 @@ """Common Classes or Utilities for SSH Drivers.""" # Standard Library -from typing import Callable +from typing import TYPE_CHECKING # Project from hyperglass.log import log -from hyperglass.exceptions import ScrapeError -from hyperglass.configuration import params -from hyperglass.compat._sshtunnel import BaseSSHTunnelForwarderError, open_tunnel +from hyperglass.state import use_state +from hyperglass.compat import BaseSSHTunnelForwarderError, open_tunnel +from hyperglass.exceptions.public import ScrapeError # Local from ._common import Connection +if TYPE_CHECKING: + # Project + from hyperglass.compat import SSHTunnelForwarder + class SSHConnection(Connection): """Base class for SSH drivers.""" - def setup_proxy(self) -> Callable: + def setup_proxy(self) -> "SSHTunnelForwarder": """Return a preconfigured sshtunnel.SSHTunnelForwarder instance.""" proxy = self.device.proxy + params = use_state("params") def opener(): """Set up an SSH tunnel according to a device's configuration.""" @@ -32,31 +37,27 @@ def opener(): } if proxy.credential._method == "password": # Use password auth if no key is defined. - tunnel_kwargs[ - "ssh_password" - ] = proxy.credential.password.get_secret_value() + tunnel_kwargs["ssh_password"] = proxy.credential.password.get_secret_value() else: # Otherwise, use key auth. tunnel_kwargs["ssh_pkey"] = proxy.credential.key.as_posix() if proxy.credential._method == "encrypted_key": # If the key is encrypted, use the password field as the # private key password. - tunnel_kwargs[ - "ssh_private_key_password" - ] = proxy.credential.password.get_secret_value() + tunnel_kwargs["ssh_private_key_password"] = ( + proxy.credential.password.get_secret_value() + ) try: - return open_tunnel(proxy._target, proxy.port, **tunnel_kwargs) + return open_tunnel( + ssh_address_or_host=proxy._target, ssh_port=proxy.port, **tunnel_kwargs + ) except BaseSSHTunnelForwarderError as scrape_proxy_error: - log.error( - f"Error connecting to device {self.device.name} via " - f"proxy {proxy.name}" + log.bind(device=self.device.name, proxy=proxy.name).error( + "Failed to connect to device via proxy" ) raise ScrapeError( - params.messages.connection_error, - device_name=self.device.name, - proxy=proxy.name, - error=str(scrape_proxy_error), - ) + error=scrape_proxy_error, device=self.device + ) from scrape_proxy_error return opener diff --git a/hyperglass/execution/drivers/ssh_netmiko.py b/hyperglass/execution/drivers/ssh_netmiko.py index 5e055b4f3..f5b31baaf 100644 --- a/hyperglass/execution/drivers/ssh_netmiko.py +++ b/hyperglass/execution/drivers/ssh_netmiko.py @@ -5,10 +5,11 @@ # Standard Library import math +import asyncio from typing import Iterable # Third Party -from netmiko import ( +from netmiko import ( # type: ignore ConnectHandler, NetMikoTimeoutException, NetMikoAuthenticationException, @@ -16,71 +17,68 @@ # Project from hyperglass.log import log -from hyperglass.exceptions import AuthError, ScrapeError, DeviceTimeout -from hyperglass.configuration import params +from hyperglass.state import use_state +from hyperglass.exceptions.public import AuthError, DeviceTimeout, ResponseEmpty # Local from .ssh import SSHConnection -netmiko_nos_globals = { +netmiko_device_globals = { # Netmiko doesn't currently handle Mikrotik echo verification well, # see ktbyers/netmiko#1600 "mikrotik_routeros": {"global_cmd_verify": False}, "mikrotik_switchos": {"global_cmd_verify": False}, } -netmiko_nos_send_args = { - # Netmiko doesn't currently handle the Mikrotik prompt properly, see - # ktbyers/netmiko#1956 - "mikrotik_routeros": {"expect_string": r"\S+\s\>\s$"}, - "mikrotik_switchos": {"expect_string": r"\S+\s\>\s$"}, -} +netmiko_device_send_args = {} class NetmikoConnection(SSHConnection): """Handle a device connection via Netmiko.""" async def collect(self, host: str = None, port: int = None) -> Iterable: + """Run blocking Netmiko operations outside the API event loop.""" + return await asyncio.to_thread(self._collect, host, port) + + def _collect(self, host: str = None, port: int = None) -> Iterable: """Connect directly to a device. Directly connects to the router via Netmiko library, returns the command output. """ - if host is not None: - log.debug( - "Connecting to {} via proxy {} [{}]", - self.device.name, - self.device.proxy.name, - f"{host}:{port}", - ) - else: - log.debug("Connecting directly to {}", self.device.name) + params = use_state("params") + _log = log.bind( + device=self.device.name, + address=f"{host}:{port}", + proxy=str(self.device.proxy.address) if self.device.proxy is not None else None, + ) + + _log.debug("Connecting to device") - global_args = netmiko_nos_globals.get(self.device.nos, {}) + global_args = netmiko_device_globals.get(self.device.platform, {}) - send_args = netmiko_nos_send_args.get(self.device.nos, {}) + send_args = netmiko_device_send_args.get(self.device.platform, {}) driver_kwargs = { "host": host or self.device._target, "port": port or self.device.port, - "device_type": self.device.nos, + "device_type": self.device.get_device_type(), "username": self.device.credential.username, - "global_delay_factor": params.netmiko_delay_factor, + "global_delay_factor": 0.1, "timeout": math.floor(params.request_timeout * 1.25), "session_timeout": math.ceil(params.request_timeout - 1), **global_args, + **self.device.driver_config, } - if "_telnet" in self.device.nos: + if "_telnet" in self.device.platform: # Telnet devices with a low delay factor (default) tend to # throw login errors. driver_kwargs["global_delay_factor"] = 2 if self.device.credential._method == "password": # Use password auth if no key is defined. - driver_kwargs[ - "password" - ] = self.device.credential.password.get_secret_value() + driver_kwargs["password"] = self.device.credential.password.get_secret_value() else: # Otherwise, use key auth. driver_kwargs["use_keys"] = True @@ -88,49 +86,27 @@ async def collect(self, host: str = None, port: int = None) -> Iterable: if self.device.credential._method == "encrypted_key": # If the key is encrypted, use the password field as the # private key password. - driver_kwargs[ - "passphrase" - ] = self.device.credential.password.get_secret_value() + driver_kwargs["passphrase"] = self.device.credential.password.get_secret_value() try: nm_connect_direct = ConnectHandler(**driver_kwargs) responses = () - for query in self.query: - raw = nm_connect_direct.send_command(query, **send_args) - responses += (raw,) - log.debug(f'Raw response for command "{query}":\n{raw}') - - nm_connect_direct.disconnect() + try: + for query in self.query: + raw = nm_connect_direct.send_command(query, **send_args) + responses += (raw,) + finally: + nm_connect_direct.disconnect() except NetMikoTimeoutException as scrape_error: - log.error(str(scrape_error)) - raise DeviceTimeout( - params.messages.connection_error, - device_name=self.device.name, - proxy=None, - error=params.messages.request_timeout, - ) + raise DeviceTimeout(error=scrape_error, device=self.device) from scrape_error + except NetMikoAuthenticationException as auth_error: - log.error( - "Error authenticating to device {loc}: {e}", - loc=self.device.name, - e=str(auth_error), - ) - - raise AuthError( - params.messages.connection_error, - device_name=self.device.name, - proxy=None, - error=params.messages.authentication_error, - ) + raise AuthError(error=auth_error, device=self.device) from auth_error + if not responses: - raise ScrapeError( - params.messages.connection_error, - device_name=self.device.name, - proxy=None, - error=params.messages.no_response, - ) + raise ResponseEmpty(query=self.query_data) return responses diff --git a/hyperglass/execution/drivers/ssh_scrapli.py b/hyperglass/execution/drivers/ssh_scrapli.py deleted file mode 100644 index 935dbeb1c..000000000 --- a/hyperglass/execution/drivers/ssh_scrapli.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Scrapli-Specific Classes & Utilities. - -https://github.com/carlmontanari/scrapli -""" - -# Standard Library -import math -from typing import Sequence - -# Third Party -from scrapli.driver import AsyncGenericDriver -from scrapli.exceptions import ( - ScrapliTimeout, - ScrapliException, - ScrapliAuthenticationFailed, -) -from scrapli.driver.core import ( - AsyncEOSDriver, - AsyncNXOSDriver, - AsyncIOSXEDriver, - AsyncIOSXRDriver, - AsyncJunosDriver, -) - -# Project -from hyperglass.log import log -from hyperglass.exceptions import ( - AuthError, - ScrapeError, - DeviceTimeout, - UnsupportedDevice, -) -from hyperglass.configuration import params - -# Local -from .ssh import SSHConnection - -SCRAPLI_DRIVER_MAP = { - "arista_eos": AsyncEOSDriver, - "bird": AsyncGenericDriver, - "cisco_ios": AsyncIOSXEDriver, - "cisco_nxos": AsyncNXOSDriver, - "cisco_xr": AsyncIOSXRDriver, - "frr": AsyncGenericDriver, - "juniper": AsyncJunosDriver, - "tnsr": AsyncGenericDriver, -} - -driver_global_args = { - # Per-NOS driver keyword arguments - "tnsr": {"comms_prompt_pattern": r"\S+\s\S+[\#\>]"}, - "frr": {"comms_ansi": True}, - "bird": {"comms_ansi": True}, -} - - -def _map_driver(nos: str) -> AsyncGenericDriver: - driver = SCRAPLI_DRIVER_MAP.get(nos) - if driver is None: - raise UnsupportedDevice("{nos} is not supported by scrapli.", nos=nos) - return driver - - -class ScrapliConnection(SSHConnection): - """Handle a device connection via Scrapli.""" - - async def collect(self, host: str = None, port: int = None) -> Sequence: - """Connect directly to a device. - - Directly connects to the router via Netmiko library, returns the - command output. - """ - driver = _map_driver(self.device.nos) - - if host is not None: - log.debug( - "Connecting to {} via proxy {} [{}]", - self.device.name, - self.device.proxy.name, - f"{host}:{port}", - ) - else: - log.debug("Connecting directly to {}", self.device.name) - - global_args = driver_global_args.get(self.device.nos, {}) - - driver_kwargs = { - "host": host or self.device._target, - "port": port or self.device.port, - "auth_username": self.device.credential.username, - "timeout_ops": math.floor(params.request_timeout * 1.25), - "transport": "asyncssh", - "auth_strict_key": False, - "ssh_known_hosts_file": False, - **global_args, - } - - if self.device.credential._method == "password": - # Use password auth if no key is defined. - driver_kwargs[ - "auth_password" - ] = self.device.credential.password.get_secret_value() - else: - # Otherwise, use key auth. - driver_kwargs["auth_private_key"] = self.device.credential.key.as_posix() - if self.device.credential._method == "encrypted_key": - # If the key is encrypted, use the password field as the - # private key password. - driver_kwargs[ - "auth_private_key_passphrase" - ] = self.device.credential.password.get_secret_value() - - driver = driver(**driver_kwargs) - driver.logger = log.bind( - logger_name=f"scrapli.{driver.host}:{driver.port}-driver" - ) - try: - responses = () - async with driver as connection: - await connection.get_prompt() - for query in self.query: - raw = await connection.send_command(query) - responses += (raw.result,) - log.debug(f'Raw response for command "{query}":\n{raw.result}') - - except ScrapliTimeout as err: - log.error(err) - raise DeviceTimeout( - params.messages.connection_error, - device_name=self.device.name, - error=params.messages.request_timeout, - ) - except ScrapliAuthenticationFailed as err: - log.error( - "Error authenticating to device {loc}: {e}", - loc=self.device.name, - e=str(err), - ) - - raise AuthError( - params.messages.connection_error, - device_name=self.device.name, - error=params.messages.authentication_error, - ) - except ScrapliException as err: - log.error(err) - raise ScrapeError( - params.messages.connection_error, - device_name=self.device.name, - error=params.messages.no_response, - ) - - if not responses: - raise ScrapeError( - params.messages.connection_error, - device_name=self.device.name, - error=params.messages.no_response, - ) - - return responses diff --git a/hyperglass/examples/commands.yaml b/hyperglass/execution/drivers/tests/__init__.py similarity index 100% rename from hyperglass/examples/commands.yaml rename to hyperglass/execution/drivers/tests/__init__.py diff --git a/hyperglass/execution/drivers/tests/test_construct.py b/hyperglass/execution/drivers/tests/test_construct.py new file mode 100644 index 000000000..7e5adee2b --- /dev/null +++ b/hyperglass/execution/drivers/tests/test_construct.py @@ -0,0 +1,90 @@ +# Standard Library +import typing as t + +# Third Party +import pytest + +# Project +from hyperglass.state import use_state +from hyperglass.models.api import Query +from hyperglass.configuration import init_ui_params +from hyperglass.models.directive import Directives +from hyperglass.models.config.params import Params +from hyperglass.models.config.devices import Devices + +# Local +from .._construct import Construct + +if t.TYPE_CHECKING: + # Project + from hyperglass.state import HyperglassState + + +@pytest.fixture +def params(): + return {} + + +@pytest.fixture +def devices(): + return [ + { + "name": "test1", + "address": "127.0.0.1", + "credential": {"username": "", "password": ""}, + "platform": "juniper", + "attrs": {"source4": "192.0.2.1", "source6": "2001:db8::1"}, + "directives": ["juniper_bgp_route"], + } + ] + + +@pytest.fixture +def directives(): + return [ + { + "juniper_bgp_route": { + "name": "BGP Route", + "field": {"description": "test"}, + } + } + ] + + +@pytest.fixture +def state( + *, + params: t.Dict[str, t.Any], + directives: t.Sequence[t.Dict[str, t.Any]], + devices: t.Sequence[t.Dict[str, t.Any]], +) -> t.Generator["HyperglassState", None, None]: + """Test fixture to initialize Redis store.""" + _state = use_state() + _params = Params(**params) + _directives = Directives.new(*directives) + + with _state.cache.pipeline() as pipeline: + # Write params and directives to the cache first to avoid a race condition where ui_params + # or devices try to access params or directives before they're available. + pipeline.set("params", _params) + pipeline.set("directives", _directives) + + _devices = Devices(*devices) + ui_params = init_ui_params(params=_params, devices=_devices) + + with _state.cache.pipeline() as pipeline: + pipeline.set("devices", _devices) + pipeline.set("ui_params", ui_params) + + yield _state + _state.clear() + + +def test_construct(state): + query = Query( + queryLocation="test1", + queryTarget="192.0.2.0/24", + queryType="juniper_bgp_route", + ) + constructor = Construct(device=state.devices["test1"], query=query) + assert constructor.target == "192.0.2.0/24" diff --git a/hyperglass/execution/main.py b/hyperglass/execution/main.py index 618cf5df4..2134494d4 100644 --- a/hyperglass/execution/main.py +++ b/hyperglass/execution/main.py @@ -3,94 +3,76 @@ Accepts input from front end application, validates the input and returns errors if input is invalid. Passes validated parameters to construct.py, which is used to build & run the Netmiko connections or -hyperglass-frr API calls, returns the output back to the front end. +http client API calls, returns the output back to the front end. """ # Standard Library -import signal -from typing import Any, Dict, Union, Callable, Sequence +import asyncio +from typing import TYPE_CHECKING, Dict, Union # Project from hyperglass.log import log -from hyperglass.exceptions import DeviceTimeout, ResponseEmpty -from hyperglass.models.api import Query -from hyperglass.configuration import params +from hyperglass.state import use_state +from hyperglass.util.typing import is_series +from hyperglass.exceptions.public import DeviceTimeout, ResponseEmpty + +if TYPE_CHECKING: + from hyperglass.models.api import Query + from .drivers import Connection + from hyperglass.models.data import OutputDataModel # Local -from .drivers import Connection, AgentConnection, NetmikoConnection, ScrapliConnection +from .drivers import HttpClient, NetmikoConnection -def map_driver(driver_name: str) -> Connection: +def map_driver(driver_name: str) -> "Connection": """Get the correct driver class based on the driver name.""" - if driver_name == "scrapli": - return ScrapliConnection - - elif driver_name == "hyperglass_agent": - return AgentConnection + if driver_name == "hyperglass_http_client": + return HttpClient return NetmikoConnection -def handle_timeout(**exc_args: Any) -> Callable: - """Return a function signal can use to raise a timeout exception.""" - - def handler(*args: Any, **kwargs: Any) -> None: - raise DeviceTimeout(**exc_args) - - return handler - - -async def execute(query: Query) -> Union[str, Sequence[Dict]]: +async def execute(query: "Query") -> Union["OutputDataModel", str]: """Initiate query validation and execution.""" - + params = use_state("params") output = params.messages.general - - log.debug("Received query for {}", query.json()) - log.debug("Matched device config: {}", query.device) + _log = log.bind(query=query.summary(), device=query.device.id) + _log.debug("") mapped_driver = map_driver(query.device.driver) - driver = mapped_driver(query.device, query) - - timeout_args = { - "unformatted_msg": params.messages.connection_error, - "device_name": query.device.name, - "error": params.messages.request_timeout, - } - - if query.device.proxy: - timeout_args["proxy"] = query.device.proxy.name - - signal.signal(signal.SIGALRM, handle_timeout(**timeout_args)) - signal.alarm(params.request_timeout - 1) - - if query.device.proxy: - proxy = driver.setup_proxy() - with proxy() as tunnel: - response = await driver.collect( - tunnel.local_bind_host, tunnel.local_bind_port - ) - else: - response = await driver.collect() - - output = await driver.parsed_response(response) - - if isinstance(output, str): - # If the output is a string (not structured) and is empty, - # produce an error. - if output == "" or output == "\n": - raise ResponseEmpty( - params.messages.no_output, device_name=query.device.name - ) - elif isinstance(output, Dict): - # If the output an empty dict, responses have data, produce an - # error. - if not output: - raise ResponseEmpty( - params.messages.no_output, device_name=query.device.name - ) - - log.debug("Output for query: {}:\n{}", query.json(), repr(output)) - signal.alarm(0) + driver: "Connection" = mapped_driver(query.device, query) + + try: + async with asyncio.timeout(params.request_timeout - 1): + if query.device.proxy: + proxy = driver.setup_proxy() + with proxy() as tunnel: + response = await driver.collect(tunnel.local_bind_host, tunnel.local_bind_port) + else: + response = await driver.collect() + + output = await driver.response(response) + + if is_series(output): + if len(output) == 0: + raise ResponseEmpty(query=query) + output = "\n\n".join(output) + + elif isinstance(output, str): + # If the output is a string (not structured) and is empty, + # produce an error. + if output == "" or output == "\n": + raise ResponseEmpty(query=query) + + elif isinstance(output, Dict): + # If the output an empty dict, responses have data, produce an + # error. + if not output: + raise ResponseEmpty(query=query) + + except TimeoutError as error: + raise DeviceTimeout(error=error, device=query.device) from error return output diff --git a/hyperglass/execution/tests/test_timeout.py b/hyperglass/execution/tests/test_timeout.py new file mode 100644 index 000000000..0bff1e0f2 --- /dev/null +++ b/hyperglass/execution/tests/test_timeout.py @@ -0,0 +1,51 @@ +"""Verify SSH work and query timeouts do not block or alarm unrelated requests.""" + +# Standard Library +import time +import asyncio +from types import SimpleNamespace + +# Third Party +import pytest + +# Project +from hyperglass.execution import main +from hyperglass.execution.drivers.ssh_netmiko import NetmikoConnection + + +def test_ssh_collection_keeps_event_loop_responsive(monkeypatch): + connection = object.__new__(NetmikoConnection) + + def slow_collect(*args): + time.sleep(0.1) + return ("SSH output",) + + monkeypatch.setattr(connection, "_collect", slow_collect) + + async def run(): + task = asyncio.create_task(connection.collect()) + await asyncio.sleep(0.01) + assert not task.done() + assert await task == ("SSH output",) + + asyncio.run(run()) + + +def test_query_timeout_is_scoped_to_request(monkeypatch): + params = SimpleNamespace(request_timeout=1.01, messages=SimpleNamespace(general="error")) + monkeypatch.setattr(main, "use_state", lambda _: params) + monkeypatch.setattr(main, "DeviceTimeout", lambda **kwargs: TimeoutError("SSH timed out")) + + class Driver: + def __init__(self, *args): + pass + + async def collect(self): + await asyncio.sleep(10) + + monkeypatch.setattr(main, "map_driver", lambda _: Driver) + query = SimpleNamespace( + device=SimpleNamespace(id="test", driver="netmiko", proxy=None), summary=lambda: "test" + ) + with pytest.raises(TimeoutError, match="SSH timed out"): + asyncio.run(main.execute(query)) diff --git a/hyperglass/external/__init__.py b/hyperglass/external/__init__.py index 358b0d8de..4d9d22e9e 100644 --- a/hyperglass/external/__init__.py +++ b/hyperglass/external/__init__.py @@ -1,5 +1,21 @@ """Functions & handlers for external data.""" # Local -from .ripestat import RIPEStat # noqa: F401 -from .webhooks import Webhook # noqa: F401 +from .rpki import rpki_state +from .slack import SlackHook +from .generic import BaseExternal +from .msteams import MSTeams +from .bgptools import network_info, network_info_sync +from .webhooks import Webhook +from .http_client import HTTPClient + +__all__ = ( + "BaseExternal", + "HTTPClient", + "MSTeams", + "network_info_sync", + "network_info", + "rpki_state", + "SlackHook", + "Webhook", +) diff --git a/hyperglass/external/_base.py b/hyperglass/external/_base.py index b9d368ec6..3954c51dd 100644 --- a/hyperglass/external/_base.py +++ b/hyperglass/external/_base.py @@ -1,24 +1,36 @@ -"""Session handler for RIPEStat Data API.""" +"""Session handler for external http data sources.""" # Standard Library import re import json as _json import socket +import typing as t from json import JSONDecodeError from socket import gaierror # Third Party import httpx -from httpx import StatusCode # Project from hyperglass.log import log -from hyperglass.util import make_repr, parse_exception +from hyperglass.util import parse_exception, repr_from_attrs +from hyperglass.settings import Settings from hyperglass.constants import __version__ -from hyperglass.exceptions import HyperglassError +from hyperglass.models.fields import JsonValue, HttpMethod, Primitives +from hyperglass.exceptions.private import ExternalError +if t.TYPE_CHECKING: + # Standard Library + from types import TracebackType -def _prepare_dict(_dict): + # Project + from hyperglass.exceptions._common import ErrorLevel + from hyperglass.models.config.logging import Http + +D = t.TypeVar("D", bound=t.Dict) + + +def _prepare_dict(_dict: D) -> D: return _json.loads(_json.dumps(_dict, default=str)) @@ -27,16 +39,17 @@ class BaseExternal: def __init__( self, - base_url, - config=None, - uri_prefix="", - uri_suffix="", - verify_ssl=True, - timeout=10, - parse=True, - ): + base_url: str, + config: t.Optional["Http"] = None, + uri_prefix: str = "", + uri_suffix: str = "", + verify_ssl: bool = True, + timeout: int = 10, + parse: bool = True, + ) -> None: """Initialize connection instance.""" self.__name__ = getattr(self, "name", "BaseExternal") + self.name = self.__name__ self.config = config self.base_url = base_url.strip("/") self.uri_prefix = uri_prefix.strip("/") @@ -45,65 +58,95 @@ def __init__( self.timeout = timeout self.parse = parse - session_args = { - "verify": self.verify_ssl, + context = httpx.create_ssl_context(verify=verify_ssl) + + if Settings.ca_cert is not None: + context.load_verify_locations(cafile=str(Settings.ca_cert)) + + client_kwargs = { "base_url": self.base_url, "timeout": self.timeout, + "verify": context, } - self._session = httpx.Client(**session_args) - self._asession = httpx.AsyncClient(**session_args) + + self._session = httpx.Client(**client_kwargs) + self._asession = httpx.AsyncClient(**client_kwargs) @classmethod - def __init_subclass__(cls, name=None, **kwargs): + def __init_subclass__( + cls: "BaseExternal", name: t.Optional[str] = None, **kwargs: t.Any + ) -> None: """Set correct subclass name.""" super().__init_subclass__(**kwargs) cls.name = name or cls.__name__ - async def __aenter__(self): + async def __aenter__(self: "BaseExternal") -> "BaseExternal": """Test connection on entry.""" available = await self._atest() if available: - log.debug("Initialized session with {}", self.base_url) + log.bind(url=self.base_url).debug("Initialized session") return self - else: - raise self._exception(f"Unable to create session to {self.name}") - - async def __aexit__(self, exc_type=None, exc_value=None, traceback=None): + raise self._exception(f"Unable to create session to {self.name}") + + async def __aexit__( + self: "BaseExternal", + exc_type: t.Optional[t.Type[BaseException]] = None, + exc_value: t.Optional[BaseException] = None, + traceback: t.Optional["TracebackType"] = None, + ) -> True: """Close connection on exit.""" - log.debug("Closing session with {}", self.base_url) + log.bind(url=self.base_url).debug("Closing session") + + if exc_type is not None: + log.error(str(exc_value)) await self._asession.aclose() + if exc_value is not None: + raise exc_value return True - def __enter__(self): + def __enter__(self: "BaseExternal") -> "BaseExternal": """Test connection on entry.""" available = self._test() if available: - log.debug("Initialized session with {}", self.base_url) + log.bind(url=self.base_url).debug("Initialized session") return self - else: - raise self._exception(f"Unable to create session to {self.name}") - - def __exit__(self, exc_type=None, exc_value=None, traceback=None): + raise self._exception(f"Unable to create session to {self.name}") + + def __exit__( + self: "BaseExternal", + exc_type: t.Optional[t.Type[BaseException]] = None, + exc_value: t.Optional[BaseException] = None, + exc_traceback: t.Optional["TracebackType"] = None, + ) -> bool: """Close connection on exit.""" if exc_type is not None: - log.error(traceback) + log.error(str(exc_value)) self._session.close() + if exc_value is not None: + raise exc_value + return True - def __repr__(self): + def __repr__(self: "BaseExternal") -> str: """Return user friendly representation of instance.""" - return make_repr(self) - - def _exception(self, message, exc=None, level="warning", **kwargs): + return repr_from_attrs(self, ("name", "base_url", "config", "parse")) + + def _exception( + self: "BaseExternal", + message: str, + exc: t.Optional[BaseException] = None, + level: "ErrorLevel" = "warning", + **kwargs: t.Any, + ) -> ExternalError: """Add stringified exception to message if passed.""" if exc is not None: - message = f"{str(message)}: {str(exc)}" + message = f"{message!s}: {exc!s}" - return HyperglassError(message, str(level), **kwargs) + return ExternalError(message=message, level=level, **kwargs) - def _parse_response(self, response): + def _parse_response(self: "BaseExternal", response: httpx.Response) -> t.Any: if self.parse: parsed = {} try: @@ -112,15 +155,14 @@ def _parse_response(self, response): try: parsed = _json.loads(response) except (JSONDecodeError, TypeError): - log.error("Error parsing JSON for response {}", repr(response)) parsed = {"data": response.text} else: parsed = response return parsed - def _test(self): + def _test(self: "BaseExternal") -> bool: """Open a low-level connection to the base URL to ensure its port is open.""" - log.debug("Testing connection to {}", self.base_url) + log.bind(url=self.base_url).debug("Testing connection") try: # Parse out just the hostname from a URL string. @@ -141,16 +183,16 @@ def _test(self): except gaierror as err: # Raised if the target isn't listening on the port raise self._exception( - f"{self.name} appears to be unreachable", err + f"{self.name!r} appears to be unreachable at {self.base_url!r}", err ) from None return True - async def _atest(self): + async def _atest(self: "BaseExternal") -> bool: """Open a low-level connection to the base URL to ensure its port is open.""" return self._test() - def _build_request(self, **kwargs): + def _build_request(self: "BaseExternal", **kwargs: t.Any) -> t.Dict[str, t.Any]: """Process requests parameters into structure usable by http library.""" # Standard Library from operator import itemgetter @@ -170,8 +212,7 @@ def _build_request(self, **kwargs): if method.upper() not in supported_methods: raise self._exception( - f'Method must be one of {", ".join(supported_methods)}. ' - f"Got: {str(method)}" + f"Method must be one of {', '.join(supported_methods)}. Got: {str(method)}" ) endpoint = "/".join( @@ -208,26 +249,22 @@ def _build_request(self, **kwargs): if not isinstance(timeout, int): try: timeout = int(timeout) - except TypeError: - raise self._exception( - f"Timeout must be an int, got: {str(timeout)}" - ) + except TypeError as err: + raise self._exception(f"Timeout must be an int, got: {str(timeout)}") from err request["timeout"] = timeout - - log.debug("Constructed request parameters {}", request) return request async def _arequest( # noqa: C901 - self, - method, - endpoint, - item=None, - headers=None, - params=None, - data=None, - timeout=None, - response_required=False, - ): + self: "BaseExternal", + method: HttpMethod, + endpoint: str, + item: t.Union[str, int, None] = None, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + response_required: bool = False, + ) -> t.Any: """Run HTTP POST operation.""" request = self._build_request( method=method, @@ -244,10 +281,10 @@ async def _arequest( # noqa: C901 response = await self._asession.request(**request) if response.status_code not in range(200, 300): - status = StatusCode(response.status_code) + status = httpx.codes(response.status_code) error = self._parse_response(response) raise self._exception( - f'{status.name.replace("_", " ")}: {error}', level="danger" + f"{status.name.replace('_', ' ')}: {error}", level="danger" ) from None except httpx.HTTPError as http_err: @@ -255,35 +292,35 @@ async def _arequest( # noqa: C901 return self._parse_response(response) - async def _aget(self, endpoint, **kwargs): + async def _aget(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return await self._arequest(method="GET", endpoint=endpoint, **kwargs) - async def _apost(self, endpoint, **kwargs): + async def _apost(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return await self._arequest(method="POST", endpoint=endpoint, **kwargs) - async def _aput(self, endpoint, **kwargs): + async def _aput(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return await self._arequest(method="PUT", endpoint=endpoint, **kwargs) - async def _adelete(self, endpoint, **kwargs): + async def _adelete(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return await self._arequest(method="DELETE", endpoint=endpoint, **kwargs) - async def _apatch(self, endpoint, **kwargs): + async def _apatch(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return await self._arequest(method="PATCH", endpoint=endpoint, **kwargs) - async def _ahead(self, endpoint, **kwargs): + async def _ahead(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return await self._arequest(method="HEAD", endpoint=endpoint, **kwargs) def _request( # noqa: C901 - self, - method, - endpoint, - item=None, - headers=None, - params=None, - data=None, - timeout=None, - response_required=False, - ): + self: "BaseExternal", + method: HttpMethod, + endpoint: str, + item: t.Union[str, int, None] = None, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + response_required: bool = False, + ) -> t.Any: """Run HTTP POST operation.""" request = self._build_request( method=method, @@ -300,10 +337,10 @@ def _request( # noqa: C901 response = self._session.request(**request) if response.status_code not in range(200, 300): - status = StatusCode(response.status_code) + status = httpx.codes(response.status_code) error = self._parse_response(response) raise self._exception( - f'{status.name.replace("_", " ")}: {error}', level="danger" + f"{status.name.replace('_', ' ')}: {error}", level="danger" ) from None except httpx.HTTPError as http_err: @@ -311,20 +348,20 @@ def _request( # noqa: C901 return self._parse_response(response) - def _get(self, endpoint, **kwargs): + def _get(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return self._request(method="GET", endpoint=endpoint, **kwargs) - def _post(self, endpoint, **kwargs): + def _post(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return self._request(method="POST", endpoint=endpoint, **kwargs) - def _put(self, endpoint, **kwargs): + def _put(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return self._request(method="PUT", endpoint=endpoint, **kwargs) - def _delete(self, endpoint, **kwargs): + def _delete(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return self._request(method="DELETE", endpoint=endpoint, **kwargs) - def _patch(self, endpoint, **kwargs): + def _patch(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return self._request(method="PATCH", endpoint=endpoint, **kwargs) - def _head(self, endpoint, **kwargs): + def _head(self: "BaseExternal", endpoint: str, **kwargs: t.Any) -> t.Any: return self._request(method="HEAD", endpoint=endpoint, **kwargs) diff --git a/hyperglass/external/bgptools.py b/hyperglass/external/bgptools.py index 87b9c3894..f2fd7f6a9 100644 --- a/hyperglass/external/bgptools.py +++ b/hyperglass/external/bgptools.py @@ -6,21 +6,66 @@ # Standard Library import re -import socket +import typing as t import asyncio -from typing import Dict, List +from ipaddress import IPv4Address, IPv6Address, ip_address # Project from hyperglass.log import log -from hyperglass.cache import SyncCache, AsyncCache -from hyperglass.configuration import REDIS_CONFIG, params +from hyperglass.state import use_state DEFAULT_KEYS = ("asn", "ip", "prefix", "country", "rir", "allocated", "org") CACHE_KEY = "hyperglass.external.bgptools" +TargetDetail = t.TypedDict( + "TargetDetail", + {"asn": str, "ip": str, "country": str, "rir": str, "allocated": str, "org": str}, +) -def parse_whois(output: str, targets: List[str]) -> Dict[str, str]: +TargetData = t.Dict[str, TargetDetail] + + +def default_ip_targets(*targets: str) -> t.Tuple[TargetData, t.Tuple[str, ...]]: + """Construct a mapping of default data and other data that should be queried. + + Targets in the mapping don't need to be queried and already have default values. Targets in the + query tuple should be queried. + """ + default_data = {} + query = () + for target in targets: + detail: TargetDetail = dict.fromkeys(DEFAULT_KEYS, "None") + try: + valid: t.Union[IPv4Address, IPv6Address] = ip_address(target) + + checks = ( + (valid.version == 6 and valid.is_site_local, "Site Local Address"), + (valid.is_loopback, "Loopback Address"), + (valid.is_multicast, "Multicast Address"), + (valid.is_link_local, "Link Local Address"), + (valid.is_private, "Private Address"), + ) + for exp, rir in checks: + if exp is True: + detail["rir"] = rir + break + + should_query = any((valid.is_global, valid.is_unspecified, valid.is_reserved)) + + if not should_query: + detail["ip"] = str(target) + default_data[str(target)] = detail + elif should_query: + query += (str(target),) + + except ValueError: + pass + + return default_data, query + + +def parse_whois(output: str, targets: t.List[str]) -> TargetDetail: """Parse raw whois output from bgp.tools. Sample output: @@ -31,15 +76,12 @@ def parse_whois(output: str, targets: List[str]) -> Dict[str, str]: def lines(raw): """Generate clean string values for each column.""" for r in (r for r in raw.split("\n") if r): - fields = ( - re.sub(r"(\n|\r)", "", field).strip(" ") for field in r.split("|") - ) + fields = (re.sub(r"(\n|\r)", "", field).strip(" ") for field in r.split("|")) yield fields data = {} for line in lines(output): - # Unpack each line's parsed values. asn, ip, prefix, country, rir, allocated, org = line @@ -55,12 +97,11 @@ def lines(raw): "allocated": allocated, "org": org, } - - log.debug("Parsed bgp.tools data: {}", data) + log.bind(data=data).debug("Parsed bgp.tools data") return data -async def run_whois(targets: List[str]) -> str: +async def run_whois(targets: t.List[str]) -> str: """Open raw socket to bgp.tools and execute query.""" # Construct bulk query @@ -90,57 +131,28 @@ async def run_whois(targets: List[str]) -> str: return response.decode() -def run_whois_sync(targets: List[str]) -> str: - """Open raw socket to bgp.tools and execute query.""" - - # Construct bulk query - query = "\n".join(("begin", *targets, "end\n")).encode() - - # Open the socket to bgp.tools - log.debug("Opening connection to bgp.tools") - sock = socket.socket() - sock.connect(("bgp.tools", 43)) - sock.send(query) - - # Read the response - response = b"" - while True: - data = sock.recv(128) - if data: - response += data - - else: - log.debug("Closing connection to bgp.tools") - sock.shutdown(1) - sock.close() - break - - return response.decode() - - -async def network_info(*targets: str) -> Dict[str, Dict[str, str]]: +async def network_info(*targets: str) -> TargetData: """Get ASN, Containing Prefix, and other info about an internet resource.""" - targets = [str(t) for t in targets] - cache = AsyncCache(db=params.cache.database, **REDIS_CONFIG) + default_data, query_targets = default_ip_targets(*targets) + + cache = use_state("cache") # Set default data structure. - data = {t: {k: "" for k in DEFAULT_KEYS} for t in targets} + query_data = {t: dict.fromkeys(DEFAULT_KEYS, "") for t in query_targets} # Get all cached bgp.tools data. - cached = await cache.get_dict(CACHE_KEY) + cached = cache.get_map(CACHE_KEY) or {} # Try to use cached data for each of the items in the list of # resources. - for t in targets: - - if t in cached: - # Reassign the cached network info to the matching resource. - data[t] = cached[t] - log.debug("Using cached network info for {}", t) + for target in (target for target in query_targets if target in cached): + # Reassign the cached network info to the matching resource. + query_data[target] = cached[target] + log.bind(target=target).debug("Using cached network info") # Remove cached items from the resource list so they're not queried. - targets = [t for t in targets if t not in cached] + targets = [t for t in query_targets if t not in cached] try: if targets: @@ -148,57 +160,19 @@ async def network_info(*targets: str) -> Dict[str, Dict[str, str]]: if whoisdata: # If the response is not empty, parse it. - data.update(parse_whois(whoisdata, targets)) + query_data.update(parse_whois(whoisdata, targets)) # Cache the response - for t in targets: - await cache.set_dict(CACHE_KEY, t, data[t]) - log.debug("Cached network info for {}", t) + for target in targets: + cache.set_map_item(CACHE_KEY, target, query_data[target]) + log.bind(target=t).debug("Cached network info") except Exception as err: - log.error(str(err)) + log.error(err) - return data + return {**default_data, **query_data} -def network_info_sync(*targets: str) -> Dict[str, Dict[str, str]]: +def network_info_sync(*targets: str) -> TargetData: """Get ASN, Containing Prefix, and other info about an internet resource.""" - - targets = [str(t) for t in targets] - cache = SyncCache(db=params.cache.database, **REDIS_CONFIG) - - # Set default data structure. - data = {t: {k: "" for k in DEFAULT_KEYS} for t in targets} - - # Get all cached bgp.tools data. - cached = cache.get_dict(CACHE_KEY) - - # Try to use cached data for each of the items in the list of - # resources. - for t in targets: - - if t in cached: - # Reassign the cached network info to the matching resource. - data[t] = cached[t] - log.debug("Using cached network info for {}", t) - - # Remove cached items from the resource list so they're not queried. - targets = [t for t in targets if t not in cached] - - try: - if targets: - whoisdata = run_whois_sync(targets) - - if whoisdata: - # If the response is not empty, parse it. - data.update(parse_whois(whoisdata, targets)) - - # Cache the response - for t in targets: - cache.set_dict(CACHE_KEY, t, data[t]) - log.debug("Cached network info for {}", t) - - except Exception as err: - log.error(str(err)) - - return data + return asyncio.run(network_info(*targets)) diff --git a/hyperglass/external/generic.py b/hyperglass/external/generic.py index 4fffa171a..b36cc91d2 100644 --- a/hyperglass/external/generic.py +++ b/hyperglass/external/generic.py @@ -1,27 +1,33 @@ """Session handler for Generic HTTP API endpoint.""" +# Standard Library +import typing as t + # Project from hyperglass.log import log -from hyperglass.external._base import BaseExternal from hyperglass.models.webhook import Webhook +# Local +from ._base import BaseExternal + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.config.logging import Http + class GenericHook(BaseExternal, name="Generic"): """Slack session handler.""" - def __init__(self, config): + def __init__(self: "GenericHook", config: "Http") -> None: """Initialize external base class with http connection details.""" - super().__init__( - base_url=f"{config.host.scheme}://{config.host.host}", config=config - ) + super().__init__(base_url=f"{config.host.scheme}://{config.host.host}", config=config) - async def send(self, query): + async def send(self: "GenericHook", query: t.Dict[str, t.Any]): """Send an incoming webhook to http endpoint.""" payload = Webhook(**query) - - log.debug("Sending query data to {}:\n{}", self.config.host.host, payload) + log.bind(host=self.config.host.host, payload=payload).debug("Sending request") return await self._apost( endpoint=self.config.host.path, diff --git a/hyperglass/external/http_client.py b/hyperglass/external/http_client.py new file mode 100644 index 000000000..dc968c447 --- /dev/null +++ b/hyperglass/external/http_client.py @@ -0,0 +1,234 @@ +"""HTTP Client for plugin use.""" + +# Standard Library +import typing as t + +# Project +from hyperglass.models.fields import JsonValue, Primitives + +# Local +from ._base import BaseExternal + + +class HTTPClient(BaseExternal, name="HTTPClient"): + """Wrapper around a standard HTTP Client.""" + + def __init__(self: "HTTPClient", base_url: str, timeout: int = 10) -> None: + """Create an HTTPClient instance.""" + super().__init__(base_url=base_url, timeout=timeout, parse=False) + + async def aget( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an async HTTP GET request.""" + return await self._arequest( + method="GET", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + async def apost( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an async HTTP POST request.""" + return await self._arequest( + method="POST", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + async def aput( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an async HTTP PUT request.""" + return await self._arequest( + method="PUT", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + async def adelete( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an async HTTP DELETE request.""" + return await self._arequest( + method="DELETE", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + async def apatch( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an async HTTP PATCH request.""" + return await self._arequest( + method="PATCH", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + async def ahead( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an async HTTP HEAD request.""" + return await self._arequest( + method="HEAD", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + def get( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an HTTP GET request.""" + return self._request( + method="GET", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + def post( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an HTTP POST request.""" + return self._request( + method="POST", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + def put( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an HTTP PUT request.""" + return self._request( + method="PUT", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + def delete( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an HTTP DELETE request.""" + return self._request( + method="DELETE", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + def patch( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an HTTP PATCH request.""" + return self._request( + method="PATCH", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) + + def head( + self: "HTTPClient", + endpoint: str, + headers: t.Dict[str, str] = None, + params: t.Dict[str, JsonValue[Primitives]] = None, + data: t.Optional[t.Any] = None, + timeout: t.Optional[int] = None, + ) -> t.Any: + """Perform an HTTP HEAD request.""" + return self._request( + method="HEAD", + endpoint=endpoint, + headers=headers, + params=params, + data=data, + timeout=timeout, + ) diff --git a/hyperglass/external/msteams.py b/hyperglass/external/msteams.py index 9afd102a3..576cdf15b 100644 --- a/hyperglass/external/msteams.py +++ b/hyperglass/external/msteams.py @@ -1,26 +1,30 @@ """Session handler for Microsoft Teams API.""" +# Standard Library +import typing as t + # Project from hyperglass.log import log from hyperglass.external._base import BaseExternal from hyperglass.models.webhook import Webhook +if t.TYPE_CHECKING: + # Project + from hyperglass.models.config.logging import Http + class MSTeams(BaseExternal, name="MSTeams"): """Microsoft Teams session handler.""" - def __init__(self, config): + def __init__(self: "MSTeams", config: "Http") -> None: """Initialize external base class with Microsoft Teams connection details.""" - super().__init__( - base_url="https://outlook.office.com", config=config, parse=False - ) + super().__init__(base_url="https://outlook.office.com", config=config, parse=False) - async def send(self, query): + async def send(self: "MSTeams", query: t.Dict[str, t.Any]): """Send an incoming webhook to Microsoft Teams.""" payload = Webhook(**query) - - log.debug("Sending query data to Microsoft Teams:\n{}", payload) + log.bind(destination="MS Teams", payload=payload).debug("Sending request") return await self._apost(endpoint=self.config.host.path, data=payload.msteams()) diff --git a/hyperglass/external/ripestat.py b/hyperglass/external/ripestat.py deleted file mode 100644 index fe8e6ae47..000000000 --- a/hyperglass/external/ripestat.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Session handler for RIPEStat Data API.""" - -# Standard Library -from ipaddress import ip_address, ip_network - -# Project -from hyperglass.log import log -from hyperglass.external._base import BaseExternal - - -class RIPEStat(BaseExternal, name="RIPEStat"): - """RIPEStat session handler.""" - - def __init__(self): - """Initialize external base class with RIPEStat connection details.""" - - super().__init__( - base_url="https://stat.ripe.net", uri_prefix="/data", uri_suffix="data.json" - ) - - def network_info_sync(self, resource, serialize=False): - """Get network info via RIPE's Network Info API endpoint (synchronously). - - See: https://stat.ripe.net/docs/data_api#network-info - """ - try: - valid_ip = ip_address(resource) - - if not valid_ip.is_global: - log.debug("IP {ip} is not a global address", ip=str(valid_ip)) - return {"prefix": None, "asn": None} - - except ValueError: - log.debug("'{resource}' is not a valid IP address", resource=resource) - return {"prefix": None, "asn": None} - - raw = self._get(endpoint="network-info", params={"resource": valid_ip}) - - data = { - "asns": raw["data"]["asns"], - "prefix": ip_network(raw["data"]["prefix"]), - } - - if serialize: - data["prefix"] = str(data["prefix"]) - data["asns"] = data["asns"][0] - - log.debug("Collected network info from RIPEState: {i}", i=str(data)) - return data - - async def network_info(self, resource, serialize=False): - """Get network info via RIPE's Network Info API endpoint. - - See: https://stat.ripe.net/docs/data_api#network-info - """ - try: - valid_ip = ip_address(resource) - - if not valid_ip.is_global: - log.debug("IP {ip} is not a global address", ip=str(valid_ip)) - return {"prefix": None, "asn": None} - - except ValueError: - log.debug("'{resource}' is not a valid IP address", resource=resource) - return {"prefix": None, "asn": None} - - raw = await self._aget(endpoint="network-info", params={"resource": valid_ip}) - - data = { - "asns": raw["data"]["asns"], - "prefix": ip_network(raw["data"]["prefix"]), - } - - if serialize: - data["prefix"] = str(data["prefix"]) - data["asns"] = data["asns"][0] - - log.debug("Collected network info from RIPEState: {i}", i=str(data)) - return data diff --git a/hyperglass/external/rpki.py b/hyperglass/external/rpki.py index 78b198512..139ccf8fc 100644 --- a/hyperglass/external/rpki.py +++ b/hyperglass/external/rpki.py @@ -1,49 +1,57 @@ """Validate RPKI state via Cloudflare GraphQL API.""" +# Standard Library +import typing as t + # Project from hyperglass.log import log -from hyperglass.cache import SyncCache -from hyperglass.configuration import REDIS_CONFIG, params +from hyperglass.state import use_state from hyperglass.external._base import BaseExternal +if t.TYPE_CHECKING: + # Standard Library + from ipaddress import IPv4Address, IPv6Address + RPKI_STATE_MAP = {"Invalid": 0, "Valid": 1, "NotFound": 2, "DEFAULT": 3} RPKI_NAME_MAP = {v: k for k, v in RPKI_STATE_MAP.items()} CACHE_KEY = "hyperglass.external.rpki" -cache = SyncCache(db=params.cache.database, **REDIS_CONFIG) - -def rpki_state(prefix, asn): +def rpki_state(prefix: t.Union["IPv4Address", "IPv6Address", str], asn: t.Union[int, str]) -> int: """Get RPKI state and map to expected integer.""" - log.debug("Validating RPKI State for {p} via AS{a}", p=prefix, a=asn) + _log = log.bind(prefix=prefix, asn=asn) + _log.debug("Validating RPKI State") + + cache = use_state("cache") state = 3 - ro = f"{prefix}@{asn}" + ro = f"{prefix!s}@{asn!s}" - cached = cache.get_dict(CACHE_KEY, ro) + cached = cache.get_map(CACHE_KEY, ro) if cached is not None: state = cached else: - ql = 'query GetValidation {{ validation(prefix: "{}", asn: {}) {{ state }} }}' query = ql.format(prefix, asn) - + _log.bind(query=query).debug("Cloudflare RPKI GraphQL Query") try: with BaseExternal(base_url="https://rpki.cloudflare.com") as client: response = client._post("/api/graphql", data={"query": query}) - validation_state = ( - response.get("data", {}).get("validation", {}).get("state", "DEFAULT") - ) + try: + validation_state = response["data"]["validation"]["state"] + except KeyError as missing: + _log.error("Response from Cloudflare missing key '{}': {!r}", missing, response) + validation_state = 3 + state = RPKI_STATE_MAP[validation_state] - cache.set_dict(CACHE_KEY, ro, state) + cache.set_map_item(CACHE_KEY, ro, state) except Exception as err: - log.error(str(err)) + log.error(err) + # Don't cache the state when an error produced it. state = 3 - msg = "RPKI Validation State for {} via AS{} is {}".format( - prefix, asn, RPKI_NAME_MAP[state] - ) + msg = "RPKI Validation State for {} via AS{} is {}".format(prefix, asn, RPKI_NAME_MAP[state]) if cached is not None: msg += " [CACHED]" diff --git a/hyperglass/external/slack.py b/hyperglass/external/slack.py index cbc458c81..793663fec 100644 --- a/hyperglass/external/slack.py +++ b/hyperglass/external/slack.py @@ -1,24 +1,30 @@ """Session handler for Slack API.""" +# Standard Library +import typing as t + # Project from hyperglass.log import log from hyperglass.external._base import BaseExternal from hyperglass.models.webhook import Webhook +if t.TYPE_CHECKING: + # Project + from hyperglass.models.config.logging import Http + class SlackHook(BaseExternal, name="Slack"): """Slack session handler.""" - def __init__(self, config): + def __init__(self: "SlackHook", config: "Http") -> None: """Initialize external base class with Slack connection details.""" super().__init__(base_url="https://hooks.slack.com", config=config, parse=False) - async def send(self, query): + async def send(self: "SlackHook", query: t.Dict[str, t.Any]): """Send an incoming webhook to Slack.""" payload = Webhook(**query) - - log.debug("Sending query data to Slack:\n{}", payload) + log.bind(destination="Slack", payload=payload).debug("Sending request") return await self._apost(endpoint=self.config.host.path, data=payload.slack()) diff --git a/hyperglass/external/tests/__init__.py b/hyperglass/external/tests/__init__.py new file mode 100644 index 000000000..4d703de33 --- /dev/null +++ b/hyperglass/external/tests/__init__.py @@ -0,0 +1 @@ +"""External data testing.""" diff --git a/hyperglass/external/tests/conftest.py b/hyperglass/external/tests/conftest.py new file mode 100644 index 000000000..6493c2057 --- /dev/null +++ b/hyperglass/external/tests/conftest.py @@ -0,0 +1,49 @@ +"""Keep external-client unit tests independent of public service availability.""" + +# Standard Library +import json +from functools import partial + +# Third Party +import httpx +import pytest + +# Project +from hyperglass.state import use_state +from hyperglass.external._base import BaseExternal + + +@pytest.fixture(autouse=True) +def external_http(monkeypatch): + """Exercise real request construction and parsing with HTTP transport fixtures.""" + + def respond(request): + if request.url.host == "httpbin.org": + if request.url.path == "/delay/4": + raise httpx.ReadTimeout("Simulated slow response", request=request) + return httpx.Response( + 200, + json={ + "url": str(request.url), + "args": dict(request.url.params), + "json": json.loads(request.content) if request.content else None, + }, + ) + if request.url.host == "rpki.cloudflare.com": + payload = json.loads(request.content) + assert request.url.path == "/api/graphql" + states = { + 'validation(prefix: "103.21.244.0/24", asn: 13335)': "Invalid", + 'validation(prefix: "1.1.1.0/24", asn: 13335)': "Valid", + 'validation(prefix: "192.0.2.0/24", asn: 65000)': "NotFound", + } + for query, state in states.items(): + if query in payload["query"]: + return httpx.Response(200, json={"data": {"validation": {"state": state}}}) + raise AssertionError(f"Unexpected external request: {request.method} {request.url}") + + transport = httpx.MockTransport(respond) + monkeypatch.setattr(httpx, "Client", partial(httpx.Client, transport=transport)) + monkeypatch.setattr(httpx, "AsyncClient", partial(httpx.AsyncClient, transport=transport)) + monkeypatch.setattr(BaseExternal, "_test", lambda self: True) + use_state("cache").delete("hyperglass.external.rpki") diff --git a/hyperglass/external/tests/test_base.py b/hyperglass/external/tests/test_base.py new file mode 100644 index 000000000..8882ccdb4 --- /dev/null +++ b/hyperglass/external/tests/test_base.py @@ -0,0 +1,50 @@ +"""Test external http client.""" + +# Standard Library +import asyncio + +# Third Party +import pytest + +# Project +from hyperglass.exceptions.private import ExternalError +from hyperglass.models.config.logging import Http + +# Local +from .._base import BaseExternal + +config = Http(provider="generic", host="https://httpbin.org") + + +def test_base_external_sync(): + with BaseExternal(base_url="https://httpbin.org", config=config) as client: + res1 = client._get("/get") + res2 = client._get("/get", params={"key": "value"}) + res3 = client._post("/post", data={"strkey": "value", "intkey": 1}) + assert res1["url"] == "https://httpbin.org/get" + assert res2["args"].get("key") == "value" + assert res3["json"].get("strkey") == "value" + assert res3["json"].get("intkey") == 1 + + with pytest.raises(ExternalError): + with BaseExternal(base_url="https://httpbin.org", config=config, timeout=2) as client: + client._get("/delay/4") + + +async def _run_test_base_external_async(): + async with BaseExternal(base_url="https://httpbin.org", config=config) as client: + res1 = await client._aget("/get") + res2 = await client._aget("/get", params={"key": "value"}) + res3 = await client._apost("/post", data={"strkey": "value", "intkey": 1}) + assert res1["url"] == "https://httpbin.org/get" + assert res2["args"].get("key") == "value" + assert res3["json"].get("strkey") == "value" + assert res3["json"].get("intkey") == 1 + + with pytest.raises(ExternalError): + async with BaseExternal(base_url="https://httpbin.org", config=config, timeout=2) as client: + await client._aget("/delay/4") + + +def test_base_external_async(): + asyncio.run(_run_test_base_external_async()) diff --git a/hyperglass/external/tests/test_bgptools.py b/hyperglass/external/tests/test_bgptools.py new file mode 100644 index 000000000..1ded1f7d9 --- /dev/null +++ b/hyperglass/external/tests/test_bgptools.py @@ -0,0 +1,74 @@ +"""Test bgp.tools interactions.""" + +# Standard Library +import asyncio +from unittest.mock import AsyncMock, Mock + +# Third Party +import pytest + +# Project +from hyperglass.state import use_state + +# Local +from ..bgptools import run_whois, parse_whois, network_info + +WHOIS_OUTPUT = """AS | IP | BGP Prefix | CC | Registry | Allocated | AS Name +13335 | 1.1.1.1 | 1.1.1.0/24 | US | ARIN | 2010-07-14 | Cloudflare, Inc.""" + + +@pytest.fixture(autouse=True) +def whois_connection(monkeypatch): + """Exercise WHOIS framing and parsing without opening an internet socket.""" + writer = Mock() + writer.drain = AsyncMock() + writer.can_write_eof.return_value = True + + async def connect(host, port): + assert (host, port) == ("bgp.tools", 43) + reader = asyncio.StreamReader() + reader.feed_data(WHOIS_OUTPUT.encode()) + reader.feed_eof() + return reader, writer + + monkeypatch.setattr(asyncio, "open_connection", connect) + use_state("cache").delete("hyperglass.external.bgptools") + return writer + + +# Ignore asyncio deprecation warning about loop +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +def test_network_info(): + checks = ( + ("192.0.2.1", {"asn": "None", "rir": "Private Address"}), + ("127.0.0.1", {"asn": "None", "rir": "Loopback Address"}), + ("fe80:dead:beef::1", {"asn": "None", "rir": "Link Local Address"}), + ("2001:db8::1", {"asn": "None", "rir": "Private Address"}), + ("1.1.1.1", {"asn": "13335", "rir": "ARIN"}), + ) + for addr, fields in checks: + info = asyncio.run(network_info(addr)) + assert addr in info + for key, expected in fields.items(): + assert info[addr][key] == expected + + +# Ignore asyncio deprecation warning about loop +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +def test_whois(whois_connection): + addr = "192.0.2.1" + response = asyncio.run(run_whois([addr])) + assert isinstance(response, str) + assert response == WHOIS_OUTPUT + whois_connection.write.assert_called_once_with(b"begin\n192.0.2.1\nend\n") + whois_connection.close.assert_called_once() + + +def test_whois_parser(): + addr = "1.1.1.1" + result = parse_whois(WHOIS_OUTPUT, [addr]) + assert isinstance(result, dict) + assert addr in result, "Address missing" + assert result[addr]["asn"] == "13335" + assert result[addr]["rir"] == "ARIN" + assert result[addr]["org"] == "Cloudflare, Inc." diff --git a/hyperglass/external/tests/test_rpki.py b/hyperglass/external/tests/test_rpki.py new file mode 100644 index 000000000..01438e717 --- /dev/null +++ b/hyperglass/external/tests/test_rpki.py @@ -0,0 +1,26 @@ +"""Test RPKI data fetching.""" + +# Third Party +import pytest + +# Local +from ..rpki import RPKI_NAME_MAP, rpki_state + +TEST_STATES = ( + ("103.21.244.0/24", 13335, 0), + ("1.1.1.0/24", 13335, 1), + ("192.0.2.0/24", 65000, 2), +) + + +@pytest.mark.dependency() +def test_rpki(): + for prefix, asn, expected in TEST_STATES: + result = rpki_state(prefix, asn) + result_name = RPKI_NAME_MAP.get(result, "No Name") + expected_name = RPKI_NAME_MAP.get(expected, "No Name") + assert result == expected, ( + "RPKI State for '{}' via AS{!s} '{}' ({}) instead of '{}' ({})".format( + prefix, asn, result, result_name, expected, expected_name + ) + ) diff --git a/hyperglass/external/webhooks.py b/hyperglass/external/webhooks.py index 834852279..41ffb5798 100644 --- a/hyperglass/external/webhooks.py +++ b/hyperglass/external/webhooks.py @@ -1,11 +1,20 @@ """Convenience functions for webhooks.""" +# Standard Library +import typing as t + # Project -from hyperglass.exceptions import HyperglassError -from hyperglass.external._base import BaseExternal -from hyperglass.external.slack import SlackHook -from hyperglass.external.generic import GenericHook -from hyperglass.external.msteams import MSTeams +from hyperglass.exceptions.private import UnsupportedError + +# Local +from ._base import BaseExternal +from .slack import SlackHook +from .generic import GenericHook +from .msteams import MSTeams + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.config.logging import Http PROVIDER_MAP = { "generic": GenericHook, @@ -17,12 +26,13 @@ class Webhook(BaseExternal): """Get webhook for provider name.""" - def __new__(cls, config): + def __new__(cls: "Webhook", config: "Http") -> "BaseExternal": """Return instance for correct provider handler.""" try: provider_class = PROVIDER_MAP[config.provider] return provider_class(config) - except KeyError: - raise HyperglassError( - f"'{config.provider.title()}' is not yet supported as a webhook target." - ) + except KeyError as err: + raise UnsupportedError( + message="{p} is not yet supported as a webhook target.", + p=config.provider.title(), + ) from err diff --git a/hyperglass/frontend/__init__.py b/hyperglass/frontend/__init__.py new file mode 100644 index 000000000..d8e990f2c --- /dev/null +++ b/hyperglass/frontend/__init__.py @@ -0,0 +1,389 @@ +"""Utility functions for frontend-related tasks.""" + +# Standard Library +import os +import json +import math +import shutil +import typing as t +import asyncio +from pathlib import Path + +# Project +from hyperglass.log import log +from hyperglass.util import copyfiles, check_path, move_files, dotenv_to_dict + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.ui import UIParameters + + +def get_ui_build_timeout() -> t.Optional[int]: + """Read the UI build timeout from environment variables or set a default.""" + timeout = None + + if "HYPERGLASS_UI_BUILD_TIMEOUT" in os.environ: + timeout = int(os.environ["HYPERGLASS_UI_BUILD_TIMEOUT"]) + log.bind(timeout=timeout).debug("Found UI build timeout environment variable") + + return timeout + + +async def check_node_modules() -> bool: + """Check if node_modules exists and has contents.""" + + ui_path = Path(__file__).parent.parent / "ui" + node_modules = ui_path / "node_modules" + + exists = node_modules.exists() + valid = exists + + if exists and not tuple(node_modules.iterdir()): + valid = False + + return valid + + +async def read_package_json() -> t.Dict[str, t.Any]: + """Import package.json as a python dict.""" + + package_json_file = Path(__file__).parent.parent / "ui" / "package.json" + + try: + with package_json_file.open("r") as file: + package_json = json.load(file) + + except Exception as err: + raise RuntimeError(f"Error reading package.json: {str(err)}") from err + + return package_json + + +async def node_initial(timeout: int = 180, dev_mode: bool = False) -> str: + """Initialize node_modules.""" + + ui_path = Path(__file__).parent.parent / "ui" + + env_timeout = get_ui_build_timeout() + + if env_timeout is not None and env_timeout > timeout: + timeout = env_timeout + + # Containers carry a complete, locked dependency store. Startup must not + # resolve new packages or depend on package-registry availability. + from hyperglass.settings import Settings + + command = "pnpm install --frozen-lockfile" + if Settings.container: + command += " --offline" + + proc = await asyncio.create_subprocess_shell( + cmd=command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=ui_path, + ) + + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + messages = stdout.decode("utf-8").strip() + errors = stderr.decode("utf-8").strip() + + if proc.returncode != 0: + raise RuntimeError(f"\nMessages:\n{messages}\nErrors:\n{errors}") + + await proc.wait() + + return "\n".join(messages) + + +async def build_ui(app_path: Path): + """Execute `next build` & `next export` from UI directory. + + ### Raises + RuntimeError: Raised if exit code is not 0. + RuntimeError: Raised when any other error occurs. + """ + timeout = get_ui_build_timeout() + + ui_dir = Path(__file__).parent.parent / "ui" + build_dir = app_path / "static" / "ui" + out_dir = ui_dir / "out" + + build_command = "node_modules/.bin/next build" + + all_messages = [] + try: + proc = await asyncio.create_subprocess_shell( + cmd=build_command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=ui_dir, + ) + + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + messages = stdout.decode("utf-8").strip() + errors = stderr.decode("utf-8").strip() + + if proc.returncode != 0: + raise RuntimeError(f"\nMessages:\n{messages}\nErrors:\n{errors}") + + await proc.wait() + all_messages.append(messages) + + except asyncio.TimeoutError as err: + raise RuntimeError(f"{timeout} second timeout exceeded while building UI") from err + + except Exception as err: + log.error(err) + raise RuntimeError(str(err)) from err + + if build_dir.exists(): + shutil.rmtree(build_dir) + shutil.copytree(src=out_dir, dst=build_dir, dirs_exist_ok=False) + log.bind(src=out_dir, dst=build_dir).debug("Migrated Next.JS build output") + + return "\n".join(all_messages) + + +def generate_opengraph( + image_path: Path, + max_width: int, + max_height: int, + target_path: Path, + background_color: str, +): + """Generate an OpenGraph compliant image.""" + # Third Party + from PIL import Image + + def center_point(background: Image, foreground: Image): + """Generate a tuple of center points for PIL.""" + bg_x, bg_y = background.size[0:2] + fg_x, fg_y = foreground.size[0:2] + x1 = math.floor((bg_x / 2) - (fg_x / 2)) + y1 = math.floor((bg_y / 2) - (fg_y / 2)) + x2 = math.floor((bg_x / 2) + (fg_x / 2)) + y2 = math.floor((bg_y / 2) + (fg_y / 2)) + return (x1, y1, x2, y2) + + # Convert image to JPEG format with static name "opengraph.jpg" + dst_path = target_path / "opengraph.jpg" + + # Copy the original image to the target path + copied = shutil.copy2(image_path, target_path) + log.bind(source=str(image_path), destination=str(target_path)).debug("Copied OpenGraph image") + + with Image.open(copied) as src: + # Only resize the image if it needs to be resized + if src.size[0] != max_width or src.size[1] != max_height: + # Resize image while maintaining aspect ratio + log.debug("Opengraph image is not 1200x630, resizing...") + src.thumbnail((max_width, max_height)) + + # Only impose a background image if the original image has + # alpha/transparency channels + if src.mode in ("RGBA", "LA"): + log.debug("Opengraph image has transparency, converting...") + background = Image.new("RGB", (max_width, max_height), background_color) + background.paste(src, box=center_point(background, src)) + dst = background + else: + dst = src + + # Save new image to derived target path + dst.save(dst_path) + + # Delete the copied image + Path(copied).unlink() + + if not dst_path.exists(): + raise RuntimeError(f"Unable to save resized image to {str(dst_path)}") + log.bind(path=str(dst_path)).debug("OpenGraph image ready") + + return True + + +def migrate_images(app_path: Path, params: "UIParameters"): + """Migrate images from source code to install directory.""" + images_dir = app_path / "static" / "images" + favicon_dir = images_dir / "favicons" + check_path(favicon_dir, create=True) + src_files = () + dst_files = () + + for image in ("light", "dark", "favicon"): + src: Path = getattr(params.web.logo, image) + dst = images_dir / f"{image + src.suffix}" + src_files += (src,) + dst_files += (dst,) + return copyfiles(src_files, dst_files) + + +def write_favicon_formats(formats: t.Tuple[t.Dict[str, t.Any]]) -> None: + """Create a TypeScript file in the `ui` directory containing favicon formats. + + This file should stay the same, unless the favicons library updates + supported formats. + """ + # Standard Library + from collections import OrderedDict + + file = Path(__file__).parent.parent / "ui" / "favicon-formats.ts" + + # Sort each favicon definition to ensure the result stays the same + # time the UI build runs. + ordered = json.dumps([OrderedDict(sorted(fmt.items())) for fmt in formats]) + data = "import type {{ Favicon }} from '~/types';export default {} as Favicon[];".format( + ordered + ) + file.write_text(data) + + +def write_custom_files(params: "UIParameters") -> None: + """Write custom files to the `ui` directory so they can be imported and rendered.""" + js = Path(__file__).parent.parent / "ui" / "custom.js" + html = Path(__file__).parent.parent / "ui" / "custom.html" + + # Handle Custom JS. + if params.web.custom_javascript is not None: + copyfiles((params.web.custom_javascript,), (js,)) + else: + with js.open("w") as f: + f.write("") + # Handle Custom HTML. + if params.web.custom_html is not None: + copyfiles((params.web.custom_html,), (html,)) + else: + with html.open("w") as f: + f.write("") + + +async def build_frontend( # noqa: C901 + dev_mode: bool, + dev_url: str, + prod_url: str, + params: "UIParameters", + app_path: Path, + force: bool = False, + timeout: int = 180, + full: bool = False, +) -> bool: + """Perform full frontend UI build process.""" + # Standard Library + import hashlib + + # Third Party + from favicons import Favicons # type:ignore + + # Project + from hyperglass.constants import __version__ + + # Create temporary file. json file extension is added for easy + # webpack JSON parsing. + dot_env_file = Path(__file__).parent.parent / "ui" / ".env" + env_config = {} + + ui_config_file = Path(__file__).parent.parent / "ui" / "hyperglass.json" + + ui_config_file.write_text(params.export_json(by_alias=True)) + + package_json = await read_package_json() + + # Set NextJS production/development mode and base URL based on + # developer_mode setting. + if dev_mode: + env_config.update({"HYPERGLASS_URL": dev_url, "NODE_ENV": "development"}) + + else: + env_config.update({"HYPERGLASS_URL": prod_url, "NODE_ENV": "production"}) + + # Check if hyperglass/ui/node_modules has been initialized. If not, + # initialize it. + initialized = await check_node_modules() + + if initialized: + log.debug("node_modules is already initialized") + + elif not initialized: + log.debug("node_modules has not been initialized. Starting initialization...") + + node_setup = await node_initial(timeout, dev_mode) + + if node_setup == "": + log.debug("Re-initialized node_modules") + + images_dir = app_path / "static" / "images" + favicon_dir = images_dir / "favicons" + + if not favicon_dir.exists(): + favicon_dir.mkdir() + + async with Favicons( + source=params.web.logo.favicon, + output_directory=favicon_dir, + base_url="/images/favicons/", + ) as favicons: + await favicons.generate() + log.bind(count=favicons.completed).debug("Generated favicons") + write_favicon_formats(favicons.formats()) + + build_data = { + "params": params.export_dict(), + "version": __version__, + "package_json": package_json, + } + + build_json = json.dumps(build_data, default=str) + + # Create SHA256 hash from all parameters passed to UI, use as + # build identifier. + build_id = hashlib.sha256(build_json.encode()).hexdigest() + + # Read hard-coded environment file from last build. If build ID + # matches this build's ID, don't run a new build. + if dot_env_file.exists() and not force: + env_data = dotenv_to_dict(dot_env_file) + env_build_id = env_data.get("HYPERGLASS_BUILD_ID", "None") + log.bind(id=env_build_id).debug("Previous build detected") + + if env_build_id == build_id and (app_path / "static" / "ui" / "index.html").is_file(): + log.debug("UI parameters unchanged since last build, skipping UI build...") + return True + + dot_env_file.write_text("\n".join(f"{k}={v}" for k, v in env_config.items())) + log.bind(path=str(dot_env_file)).debug("Wrote UI environment file") + + # Initiate Next.JS export process. + if any((not dev_mode, force, full)): + log.info("Starting UI build") + initialize_result = await node_initial(timeout, dev_mode) + build_result = await build_ui(app_path=app_path) + + if initialize_result: + log.debug(initialize_result) + elif initialize_result == "": + log.debug("Re-initialized node_modules") + + if build_result: + log.info("Completed UI build") + elif dev_mode and not force: + log.debug("Running in developer mode, did not build new UI files") + + migrate_images(app_path, params) + + write_custom_files(params) + + generate_opengraph( + params.web.opengraph.image, + 1200, + 630, + images_dir, + params.web.theme.colors.black, + ) + + # Record success only after the UI and its assets have been generated. + # A failed build must be retried on the next container start. + env_config.update({"HYPERGLASS_BUILD_ID": build_id}) + dot_env_file.write_text("\n".join(f"{k}={v}" for k, v in env_config.items())) + + return True diff --git a/hyperglass/log.py b/hyperglass/log.py index 8791a2e44..aedf30949 100644 --- a/hyperglass/log.py +++ b/hyperglass/log.py @@ -1,20 +1,41 @@ """Logging instance setup & configuration.""" # Standard Library -import os import sys +import typing as t import logging from datetime import datetime # Third Party from loguru import logger as _loguru_logger +from rich.theme import Theme +from rich.console import Console +from rich.logging import RichHandler -_FMT = ( - "[{level}] {time:YYYYMMDD} {time:HH:mm:ss} | {name}:" - "{line} | {function} → {message}" +# Local +from .util import dict_to_kwargs +from .constants import __version__ + +if t.TYPE_CHECKING: + # Standard Library + from pathlib import Path + + # Third Party + from loguru import Logger as Record + from pydantic import ByteSize + + # Project + from hyperglass.models.fields import LogFormat + +_FMT_DEBUG = ( + "[{level}] {time:YYYYMMDD} {time:HH:mm:ss} |" + "{line} | {function} → {message} {extra}" ) -_DATE_FMT = "%Y%m%d %H:%M:%S" -_FMT_BASIC = "{message}" + +_FMT = "[{level}] {time:YYYYMMDD} {time:HH:mm:ss} | {message} {extra}" + +_FMT_FILE = "[{time:YYYYMMDD} {time:HH:mm:ss}] {message} {extra}" +_FMT_BASIC = "{message} {extra}" _LOG_LEVELS = [ {"name": "TRACE", "color": ""}, {"name": "DEBUG", "color": ""}, @@ -25,58 +46,130 @@ {"name": "CRITICAL", "color": ""}, ] +_EXCLUDE_MODULES = ( + "PIL", + "svglib", + "paramiko.transport", +) -def setup_lib_logging() -> None: - """Override the logging handlers for dependency libraries.""" - for name in ( - "gunicorn", - "gunicorn.access", - "gunicorn.error", - "uvicorn", - "uvicorn.access", - "uvicorn.error", - "uvicorn.asgi", - "netmiko", - "scrapli", - "httpx", - ): - _loguru_logger.bind(logger_name=name) - - -def base_logger(level: str = "INFO"): - """Initialize hyperglass logging instance.""" - _loguru_logger.remove() - _loguru_logger.add(sys.stdout, format=_FMT, level=level, enqueue=True) - _loguru_logger.configure(levels=_LOG_LEVELS) - return _loguru_logger +HyperglassConsole = Console( + theme=Theme( + { + "info": "bold cyan", + "warning": "bold yellow", + "error": "bold red", + "success": "bold green", + "critical": "bold bright_red", + "logging.level.info": "bold cyan", + "logging.level.warning": "bold yellow", + "logging.level.error": "bold red", + "logging.level.critical": "bold bright_red", + "logging.level.success": "bold green", + "subtle": "rgb(128,128,128)", + } + ) +) + +log = _loguru_logger -log = base_logger() +def formatter(record: "Record") -> str: + """Format log messages with extra data as kwargs string.""" + msg = record.get("message", "") + extra = record.get("extra", {}) + extra_str = dict_to_kwargs(extra) + return " ".join((msg, extra_str)) -logging.addLevelName(25, "SUCCESS") +def filter_uvicorn_values(record: "Record") -> bool: + """Drop noisy uvicorn messages.""" + drop = ( + "Application startup", + "Application shutdown", + "Finished server process", + "Shutting down", + "Waiting for application", + "Started server process", + "Started parent process", + "Stopping parent process", + ) + for match in drop: + if match in record["message"]: + return False + return True -def _log_success(self, message, *a, **kw): - """Add custom builtin logging handler for the success level.""" - if self.isEnabledFor(25): - self._log(25, message, a, **kw) +class LibInterceptHandler(logging.Handler): + """Custom log handler for integrating third party library logging with hyperglass's logger.""" -logging.Logger.success = _log_success + def emit(self, record): + """Emit log record. + See: https://github.com/Delgan/loguru (Readme) + """ + # Get corresponding Loguru level if it exists + try: + level = _loguru_logger.level(record.levelname).name + except ValueError: + level = record.levelno -def set_log_level(logger, debug): - """Set log level based on debug state.""" - if debug: - os.environ["HYPERGLASS_LOG_LEVEL"] = "DEBUG" - base_logger("DEBUG") + # Find caller from where originated the logged message + frame, depth = logging.currentframe(), 2 + while frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 - if debug: - logger.debug("Debugging enabled") - return True + _loguru_logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) + + +def init_logger(level: t.Union[int, str] = logging.INFO): + """Initialize hyperglass logging instance.""" + + for mod in _EXCLUDE_MODULES: + logging.getLogger(mod).propagate = False + + # Reset built-in Loguru configurations. + _loguru_logger.remove() + + if sys.stdout.isatty(): + # Use Rich for logging if hyperglass started from a TTY. + + _loguru_logger.add( + sink=RichHandler( + console=HyperglassConsole, + rich_tracebacks=True, + tracebacks_show_locals=level == logging.DEBUG, + log_time_format="[%Y%m%d %H:%M:%S]", + ), + format=formatter, + colorize=False, + level=level, + filter=filter_uvicorn_values, + enqueue=True, + ) + else: + # Otherwise, use regular format. + _loguru_logger.add( + sink=sys.stdout, + enqueue=True, + format=_FMT if level == logging.INFO else _FMT_DEBUG, + level=level, + colorize=False, + filter=filter_uvicorn_values, + ) + + _loguru_logger.configure(levels=_LOG_LEVELS) + + return _loguru_logger -def enable_file_logging(logger, log_directory, log_format, log_max_size): +def enable_file_logging( + *, + directory: "Path", + log_format: "LogFormat", + max_size: "ByteSize", + level: t.Union[str, int], +) -> None: """Set up file-based logging from configuration parameters.""" if log_format == "json": @@ -86,52 +179,46 @@ def enable_file_logging(logger, log_directory, log_format, log_max_size): log_file_name = "hyperglass.log" structured = False - log_file = log_directory / log_file_name + log_file = directory / log_file_name if log_format == "text": - now_str = "hyperglass logs for " + datetime.utcnow().strftime( - "%B %d, %Y beginning at %H:%M:%S UTC" - ) - now_str_y = len(now_str) + 6 - now_str_x = len(now_str) + 4 - log_break = ( - "#" * now_str_y, - "\n#" + " " * now_str_x + "#\n", - "# ", - now_str, - " #", - "\n#" + " " * now_str_x + "#\n", - "#" * now_str_y, + now_str = datetime.utcnow().strftime("%B %d, %Y beginning at %H:%M:%S UTC") + header_lines = ( + f"# {line}" + for line in ( + f"hyperglass {__version__}", + f"Logs for {now_str}", + f"Log Level: {'INFO' if level == logging.INFO else 'DEBUG'}", + ) ) + header = "\n" + "\n".join(header_lines) + "\n" with log_file.open("a+") as lf: - lf.write(f'\n\n{"".join(log_break)}\n\n') + lf.write(header) - logger.add( - log_file, - format=_FMT, - rotation=log_max_size, - serialize=structured, + _loguru_logger.add( enqueue=True, + sink=log_file, + format=_FMT_FILE, + serialize=structured, + level=level, + encoding="utf8", + colorize=False, + rotation=max_size.human_readable(), ) + _loguru_logger.bind(path=log_file).debug("Logging to file") - logger.debug("Logging to {} enabled", str(log_file)) - return True - - -def enable_syslog_logging(logger, syslog_host, syslog_port): +def enable_syslog_logging(*, host: str, port: int) -> None: """Set up syslog logging from configuration parameters.""" # Standard Library from logging.handlers import SysLogHandler - logger.add( - SysLogHandler(address=(str(syslog_host), syslog_port)), + _loguru_logger.add( + SysLogHandler(address=(str(host), port)), format=_FMT_BASIC, enqueue=True, + colorize=False, ) - logger.debug( - "Logging to syslog target {}:{} enabled", str(syslog_host), str(syslog_port), - ) - return True + _loguru_logger.bind(host=host, port=port).debug("Logging to syslog target") diff --git a/hyperglass/main.py b/hyperglass/main.py index 00fc39604..51d21da73 100644 --- a/hyperglass/main.py +++ b/hyperglass/main.py @@ -1,21 +1,18 @@ -"""Gunicorn Config File.""" +"""Start hyperglass.""" # Standard Library import sys -import math -import shutil +import typing as t +import asyncio import logging -import platform # Third Party -from gunicorn.arbiter import Arbiter -from gunicorn.app.base import BaseApplication -from gunicorn.glogging import Logger +import uvicorn # Local -from .log import log, setup_lib_logging +from .log import LibInterceptHandler, init_logger, enable_file_logging, enable_syslog_logging +from .util import get_node_version from .constants import MIN_NODE_VERSION, MIN_PYTHON_VERSION, __version__ -from .util.frontend import get_node_version # Ensure the Python version meets the minimum requirements. pretty_version = ".".join(tuple(str(v) for v in MIN_PYTHON_VERSION)) @@ -23,187 +20,171 @@ raise RuntimeError(f"Python {pretty_version}+ is required.") # Ensure the NodeJS version meets the minimum requirements. -node_major, _, __ = get_node_version() +node_major, node_minor, node_patch = get_node_version() if node_major < MIN_NODE_VERSION: - raise RuntimeError(f"NodeJS {MIN_NODE_VERSION}+ is required.") + installed = ".".join(str(v) for v in (node_major, node_minor, node_patch)) + raise RuntimeError(f"NodeJS {MIN_NODE_VERSION!s}+ is required (version {installed} installed)") -# Project -from hyperglass.compat._asyncio import aiorun - # Local -from .util import cpu_count, clear_redis_cache, format_listen_address -from .cache import SyncCache -from .configuration import ( - URL_DEV, - URL_PROD, - CONFIG_PATH, - REDIS_CONFIG, - params, - frontend_params, -) -from .util.frontend import build_frontend - -if params.debug: - workers = 1 - loglevel = "DEBUG" -else: - workers = cpu_count(2) - loglevel = "WARNING" - - -class StubbedGunicornLogger(Logger): - """Custom logging to direct Gunicorn/Uvicorn logs to Loguru/Rich. - - See: https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/ - """ - - def setup(self, cfg): - """Override Gunicorn setup.""" - handler = logging.NullHandler() - self.error_logger = logging.getLogger("gunicorn.error") - self.error_logger.addHandler(handler) - self.access_logger = logging.getLogger("gunicorn.access") - self.access_logger.addHandler(handler) - self.error_logger.setLevel(loglevel) - self.access_logger.setLevel(loglevel) - - -def check_redis_instance() -> bool: - """Ensure Redis is running before starting server.""" - - cache = SyncCache(db=params.cache.database, **REDIS_CONFIG) - cache.test() - log.debug("Redis is running at: {}:{}", REDIS_CONFIG["host"], REDIS_CONFIG["port"]) - return True +from .util import cpu_count +from .state import use_state +from .settings import Settings + +LOG_LEVEL = logging.INFO if Settings.debug is False else logging.DEBUG +logging.basicConfig(handlers=[LibInterceptHandler()], level=0, force=True) +log = init_logger(LOG_LEVEL) async def build_ui() -> bool: """Perform a UI build prior to starting the application.""" + # Local + from .frontend import build_frontend + + state = use_state() await build_frontend( - dev_mode=params.developer_mode, - dev_url=URL_DEV, - prod_url=URL_PROD, - params=frontend_params, - app_path=CONFIG_PATH, + dev_mode=Settings.dev_mode, + dev_url=Settings.dev_url, + prod_url=Settings.prod_url, + params=state.ui_params, + app_path=Settings.app_path, ) return True -async def clear_cache(): - """Clear the Redis cache on shutdown.""" - try: - await clear_redis_cache(db=params.cache.database, config=REDIS_CONFIG) - except RuntimeError as e: - log.error(str(e)) - pass +def register_all_plugins() -> None: + """Validate and register configured plugins.""" + # Local + from .plugins import register_plugin, init_builtin_plugins -def cache_config() -> bool: - """Add configuration to Redis cache as a pickled object.""" - # Standard Library - import pickle + state = use_state() - cache = SyncCache(db=params.cache.database, **REDIS_CONFIG) - cache.set("HYPERGLASS_CONFIG", pickle.dumps(params)) + # Register built-in plugins. + init_builtin_plugins() - return True + failures = () + # Register external directive-based plugins (defined in directives). + for plugin_file, directives in state.devices.directive_plugins().items(): + failures += register_plugin(plugin_file, directives=directives) -def on_starting(server: Arbiter): - """Gunicorn pre-start tasks.""" + # Register external global/common plugins (defined in config). + for plugin_file in state.params.common_plugins(): + failures += register_plugin(plugin_file, common=True) - setup_lib_logging() + for failure in failures: + log.bind(plugin=failure).warning("Invalid hyperglass plugin") - python_version = platform.python_version() - required = ".".join((str(v) for v in MIN_PYTHON_VERSION)) - log.info("Python {} detected ({} required)", python_version, required) - async def runner(): - # Standard Library - from asyncio import gather +def unregister_all_plugins() -> None: + """Unregister all plugins.""" + # Local + from .plugins import InputPluginManager, OutputPluginManager - await gather(build_ui(), cache_config()) + for manager in (InputPluginManager, OutputPluginManager): + manager().reset() - check_redis_instance() - aiorun(build_ui()) - cache_config() - log.success( - "Started hyperglass {v} on http://{h}:{p} with {w} workers", - v=__version__, - h=format_listen_address(params.listen_address), - p=str(params.listen_port), - w=server.app.cfg.settings["workers"].value, - ) +def start(*, log_level: t.Union[str, int], workers: int) -> None: + """Start hyperglass via ASGI server.""" + register_all_plugins() -def on_exit(server: Arbiter): - """Gunicorn shutdown tasks.""" - - log.critical("Stopping hyperglass {}", __version__) - - async def runner(): - if not params.developer_mode: - await clear_cache() - - aiorun(runner()) - - -class HyperglassWSGI(BaseApplication): - """Custom gunicorn app.""" - - def __init__(self, app, options): - """Initialize custom WSGI.""" - self.application = app - self.options = options or {} - super().__init__() - - def load_config(self): - """Load gunicorn config.""" - config = { - key: value - for key, value in self.options.items() - if key in self.cfg.settings and value is not None - } - - for key, value in config.items(): - self.cfg.set(key.lower(), value) - - def load(self): - """Load gunicorn app.""" - return self.application - - -def start(**kwargs): - """Start hyperglass via gunicorn.""" - # Project - from hyperglass.api import app - - HyperglassWSGI( - app=app, - options={ - "worker_class": "uvicorn.workers.UvicornWorker", - "preload": True, - "keepalive": 10, - "command": shutil.which("gunicorn"), - "bind": ":".join( - (format_listen_address(params.listen_address), str(params.listen_port)) - ), - "workers": workers, - "loglevel": loglevel, - "timeout": math.ceil(params.request_timeout * 1.25), - "on_starting": on_starting, - "on_exit": on_exit, - "logger_class": StubbedGunicornLogger, - "accesslog": "-", - "errorlog": "-", - "logconfig_dict": {"formatters": {"generic": {"format": "%(message)s"}}}, - **kwargs, + if not Settings.disable_ui: + asyncio.run(build_ui()) + + uvicorn.run( + app="hyperglass.api:app", + host=str(Settings.host), + port=Settings.port, + workers=workers, + log_level=log_level, + log_config={ + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "format": "%(message)s", + }, + "access": { + "()": "uvicorn.logging.AccessFormatter", + "format": "%(message)s", + }, + }, + "handlers": { + "default": {"formatter": "default", "class": "hyperglass.log.LibInterceptHandler"}, + "access": {"formatter": "access", "class": "hyperglass.log.LibInterceptHandler"}, + }, + "loggers": { + "uvicorn.error": {"level": "ERROR", "handlers": ["default"], "propagate": False}, + "uvicorn.access": {"level": "INFO", "handlers": ["access"], "propagate": False}, + }, }, - ).run() + ) + + +def run(workers: int = None): + """Run hyperglass.""" + # Local + from .configuration import init_user_config + + try: + log.debug(repr(Settings)) + + state = use_state() + state.clear() + + init_user_config() + + enable_file_logging( + directory=state.params.logging.directory, + max_size=state.params.logging.max_size, + log_format=state.params.logging.format, + level=LOG_LEVEL, + ) + + if state.params.logging.syslog is not None: + enable_syslog_logging( + host=state.params.logging.syslog.host, + port=state.params.logging.syslog.port, + ) + _workers = workers + + if workers is None: + if Settings.debug: + _workers = 1 + else: + _workers = cpu_count(2) + + log.bind( + version=__version__, + listening=f"http://{Settings.bind()}", + app_path=f"{Settings.app_path.absolute()!s}", + container=Settings.container, + original_app_path=f"{Settings.original_app_path.absolute()!s}", + workers=_workers, + ).info( + "Starting hyperglass", + ) + + start(log_level=LOG_LEVEL, workers=_workers) + log.bind(version=__version__).critical("Stopping hyperglass") + except Exception as error: + log.critical(error) + # Handle app exceptions. + if not Settings.dev_mode: + state = use_state() + state.clear() + log.debug("Cleared hyperglass state") + unregister_all_plugins() + raise error + except (SystemExit, BaseException): + unregister_all_plugins() + sys.exit(4) if __name__ == "__main__": - start() + run() diff --git a/hyperglass/models/__init__.py b/hyperglass/models/__init__.py index 98929cf87..5aecd3835 100644 --- a/hyperglass/models/__init__.py +++ b/hyperglass/models/__init__.py @@ -1,4 +1,10 @@ """All Data Models used by hyperglass.""" # Local -from .main import HyperglassModel, HyperglassModelExtra +from .main import MultiModel, HyperglassModel, HyperglassModelWithId + +__all__ = ( + "MultiModel", + "HyperglassModel", + "HyperglassModelWithId", +) diff --git a/hyperglass/models/api/__init__.py b/hyperglass/models/api/__init__.py index ffe11f6f6..8f4bfcd2f 100644 --- a/hyperglass/models/api/__init__.py +++ b/hyperglass/models/api/__init__.py @@ -1,4 +1,5 @@ """Query & Response Validation Models.""" + # Local from .query import Query from .response import ( @@ -10,3 +11,14 @@ SupportedQueryResponse, ) from .cert_import import EncodedRequest + +__all__ = ( + "Query", + "QueryError", + "InfoResponse", + "QueryResponse", + "EncodedRequest", + "RoutersResponse", + "CommunityResponse", + "SupportedQueryResponse", +) diff --git a/hyperglass/models/api/cert_import.py b/hyperglass/models/api/cert_import.py index 755841141..215d24888 100644 --- a/hyperglass/models/api/cert_import.py +++ b/hyperglass/models/api/cert_import.py @@ -1,12 +1,10 @@ """hyperglass-agent certificate import models.""" + # Standard Library from typing import Union # Third Party -from pydantic import BaseModel, StrictStr - -# Local -from ..fields import StrictBytes +from pydantic import BaseModel, StrictStr, StrictBytes class EncodedRequest(BaseModel): diff --git a/hyperglass/models/api/query.py b/hyperglass/models/api/query.py index 793efc557..a8791bc06 100644 --- a/hyperglass/models/api/query.py +++ b/hyperglass/models/api/query.py @@ -1,230 +1,146 @@ """Input query validation model.""" # Standard Library -import json +import typing as t import hashlib import secrets from datetime import datetime # Third Party -from pydantic import BaseModel, StrictStr, constr, validator +from pydantic import BaseModel, ConfigDict, field_validator, StringConstraints +from typing_extensions import Annotated # Project -from hyperglass.exceptions import InputInvalid -from hyperglass.configuration import params, devices +from hyperglass.log import log +from hyperglass.util import snake_to_camel, repr_from_attrs +from hyperglass.state import use_state +from hyperglass.plugins import InputPluginManager +from hyperglass.exceptions.public import InputInvalid, QueryTypeNotFound, QueryLocationNotFound +from hyperglass.exceptions.private import InputValidationError # Local -from .types import SupportedQuery -from .validators import ( - validate_ip, - validate_aspath, - validate_community_input, - validate_community_select, -) -from ..config.vrf import Vrf +from ..config.devices import Device -def get_vrf_object(vrf_name: str) -> Vrf: - """Match VRF object from VRF name.""" +QueryLocation = Annotated[str, StringConstraints(strict=True, min_length=1, strip_whitespace=True)] +QueryTarget = Annotated[str, StringConstraints(min_length=1, strip_whitespace=True)] +QueryType = Annotated[str, StringConstraints(strict=True, min_length=1, strip_whitespace=True)] - for vrf_obj in devices.vrf_objects: - if vrf_name is not None: - if vrf_name == vrf_obj._id or vrf_name == vrf_obj.display_name: - return vrf_obj - elif vrf_name == "__hyperglass_default" and vrf_obj.default: - return vrf_obj - elif vrf_name is None: - if vrf_obj.default: - return vrf_obj +class SimpleQuery(BaseModel): + """A simple representation of a post-validated query.""" - raise InputInvalid(params.messages.vrf_not_found, vrf_name=vrf_name) + query_location: str + query_target: t.Union[t.List[str], str] + query_type: str + + def __repr_name__(self) -> str: + """Alias SimpleQuery to Query for clarity in logging.""" + return "Query" class Query(BaseModel): """Validation model for input query parameters.""" - query_location: StrictStr - query_type: SupportedQuery - query_vrf: StrictStr - query_target: constr(strip_whitespace=True, min_length=1) - - class Config: - """Pydantic model configuration.""" - - extra = "allow" - fields = { - "query_location": { - "title": params.web.text.query_location, - "description": "Router/Location Name", - "example": "router01", - }, - "query_type": { - "title": params.web.text.query_type, - "description": "Type of Query to Execute", - "example": "bgp_route", - }, - "query_vrf": { - "title": params.web.text.query_vrf, - "description": "Routing Table/VRF", - "example": "default", - }, - "query_target": { - "title": params.web.text.query_target, - "description": "IP Address, Community, or AS Path", - "example": "1.1.1.0/24", - }, - } - schema_extra = { - "x-code-samples": [{"lang": "Python", "source": "print('stuff')"}] - } - - def __init__(self, **kwargs): + model_config = ConfigDict(extra="allow", alias_generator=snake_to_camel, populate_by_name=True) + + # Device `name` field + query_location: QueryLocation + + query_target: t.Union[t.List[QueryTarget], QueryTarget] + + # Directive `id` field + query_type: QueryType + _kwargs: t.Dict[str, t.Any] + + def __init__(self, **data) -> None: """Initialize the query with a UTC timestamp at initialization time.""" - super().__init__(**kwargs) + super().__init__(**data) + self._kwargs = data self.timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") - def __repr__(self): - """Represent only the query fields.""" - return ( - f"Query(query_location={str(self.query_location)}, " - f"query_type={str(self.query_type)}, query_vrf={str(self.query_vrf)}, " - f"query_target={str(self.query_target)})" + state = use_state() + self._state = state + + query_directives = self.device.directives.matching(self.query_type) + + if len(query_directives) < 1: + raise QueryTypeNotFound(query_type=self.query_type) + + self.directive = query_directives[0] + + self._input_plugin_manager = InputPluginManager() + + try: + self.validate_query_target() + except InputValidationError as err: + raise InputInvalid(**err.kwargs) from err + + self.query_target = self.transform_query_target() + + def summary(self) -> SimpleQuery: + """Summarized and post-validated model of a Query.""" + return SimpleQuery( + query_location=self.query_location, + query_target=self.query_target, + query_type=self.query_type, ) - def digest(self): + def __repr__(self) -> str: + """Represent only the query fields.""" + return repr_from_attrs(self, ("query_location", "query_type", "query_target")) + + def __str__(self) -> str: + """Alias __str__ to __repr__.""" + return repr(self) + + def digest(self) -> str: """Create SHA256 hash digest of model representation.""" return hashlib.sha256(repr(self).encode()).hexdigest() - def random(self): + def random(self) -> str: """Create a random string to prevent client or proxy caching.""" return hashlib.sha256( secrets.token_bytes(8) + repr(self).encode() + secrets.token_bytes(8) ).hexdigest() - @property - def summary(self): - """Create abbreviated representation of instance.""" - items = ( - f"query_location={self.query_location}", - f"query_type={self.query_type}", - f"query_vrf={self.query_vrf.name}", - f"query_target={str(self.query_target)}", - ) - return f'Query({", ".join(items)})' + def validate_query_target(self) -> None: + """Validate a query target after all fields/relationships have been initialized.""" + # Run config/rule-based validations. + self.directive.validate_target(self.query_target) + # Run plugin-based validations. + self._input_plugin_manager.validate(query=self) + log.bind(query=self.summary()).debug("Validation passed") - @property - def device(self): - """Get this query's device object by query_location.""" - return devices[self.query_location] + def transform_query_target(self) -> t.Union[t.List[str], str]: + """Transform a query target based on defined plugins.""" + return self._input_plugin_manager.transform(query=self) - @property - def query(self): - """Get this query's configuration object.""" - return params.queries[self.query_type] - - def export_dict(self, pretty=False): - """Create dictionary representation of instance.""" - - if pretty: - items = { - "query_location": self.device.name, - "query_type": self.query.display_name, - "query_vrf": self.query_vrf.display_name, - "query_target": str(self.query_target), - } - else: - items = { - "query_location": self.query_location, - "query_type": self.query_type, - "query_vrf": self.query_vrf._id, - "query_target": str(self.query_target), - } - return items - - def export_json(self): - """Create JSON representation of instance.""" - return json.dumps(self.export_dict(), default=str) - - @validator("query_type") - def validate_query_type(cls, value): - """Ensure query_type is enabled.""" - - query = params.queries[value] - - if not query.enable: - raise InputInvalid( - params.messages.feature_not_enabled, - level="warning", - feature=query.display_name, - ) + def dict(self) -> t.Dict[str, t.Union[t.List[str], str]]: + """Include only public fields.""" + return super().model_dump(include={"query_location", "query_target", "query_type"}) - return value + @property + def device(self) -> Device: + """Get this query's device object by query_location.""" + return self._state.devices[self.query_location] - @validator("query_location") + @field_validator("query_location") def validate_query_location(cls, value): """Ensure query_location is defined.""" - valid_id = value in devices._ids - valid_hostname = value in devices.hostnames + devices = use_state("devices") + + if not devices.valid_id_or_name(value): + raise QueryLocationNotFound(location=value) - if not any((valid_id, valid_hostname)): - raise InputInvalid( - params.messages.invalid_field, - level="warning", - input=value, - field=params.web.text.query_location, - ) return value - @validator("query_vrf") - def validate_query_vrf(cls, value, values): - """Ensure query_vrf is defined.""" - - vrf_object = get_vrf_object(value) - device = devices[values["query_location"]] - device_vrf = None - - for vrf in device.vrfs: - if vrf == vrf_object: - device_vrf = vrf - break - - if device_vrf is None: - raise InputInvalid( - params.messages.vrf_not_associated, - vrf_name=vrf_object.display_name, - device_name=device.name, - ) - return device_vrf - - @validator("query_target") - def validate_query_target(cls, value, values): - """Validate query target value based on query_type.""" - - query_type = values["query_type"] - value = value.strip() - - # Use relevant function based on query_type. - validator_map = { - "bgp_aspath": validate_aspath, - "bgp_community": validate_community_input, - "bgp_route": validate_ip, - "ping": validate_ip, - "traceroute": validate_ip, - } - validator_args_map = { - "bgp_aspath": (value,), - "bgp_community": (value,), - "bgp_route": (value, values["query_type"], values["query_vrf"]), - "ping": (value, values["query_type"], values["query_vrf"]), - "traceroute": (value, values["query_type"], values["query_vrf"]), - } - - if params.queries.bgp_community.mode == "select": - validator_map["bgp_community"] = validate_community_select - - validate_func = validator_map[query_type] - validate_args = validator_args_map[query_type] - - return validate_func(*validate_args) + @field_validator("query_type") + def validate_query_type(cls, value: t.Any): + """Ensure a requested query type exists.""" + devices = use_state("devices") + if any((device.has_directives(value) for device in devices)): + return value + + raise QueryTypeNotFound(query_type=value) diff --git a/hyperglass/models/api/response.py b/hyperglass/models/api/response.py index 3b83a2c13..188f0b087 100644 --- a/hyperglass/models/api/response.py +++ b/hyperglass/models/api/response.py @@ -1,114 +1,22 @@ """Response model.""" # Standard Library -from typing import Dict, List, Union, Optional +import typing as t # Third Party -from pydantic import BaseModel, StrictInt, StrictStr, StrictBool, constr +from pydantic import Field, BaseModel, StrictInt, StrictStr, ConfigDict, StrictBool, field_validator # Project -from hyperglass.configuration import params +from hyperglass.state import use_state -ErrorName = constr(regex=r"(success|warning|error|danger)") -ResponseLevel = constr(regex=r"success") -ResponseFormat = constr(regex=r"(application\/json|text\/plain)") +ErrorName = t.Literal["success", "warning", "error", "danger"] +ResponseLevel = t.Literal["success"] +ResponseFormat = t.Literal[r"text/plain", r"application/json"] - -class QueryError(BaseModel): - """Query response model.""" - - output: StrictStr = params.messages.general - level: ErrorName = "danger" - id: Optional[StrictStr] - keywords: List[StrictStr] = [] - - class Config: - """Pydantic model configuration.""" - - title = "Query Error" - description = ( - "Response received when there is an error executing the requested query." - ) - fields = { - "output": { - "title": "Output", - "description": "Error Details", - "example": "192.0.2.1/32 is not allowed.", - }, - "level": { - "title": "Level", - "description": "Error Severity", - "example": "danger", - }, - "keywords": { - "title": "Keywords", - "description": "Relevant keyword values contained in the `output` field, which can be used for formatting.", - "example": ["192.0.2.1/32"], - }, - } - schema_extra = { - "examples": [ - { - "output": "192.0.2.1/32 is not allowed.", - "level": "danger", - "keywords": ["192.0.2.1/32"], - } - ] - } - - -class QueryResponse(BaseModel): - """Query response model.""" - - output: Union[Dict, StrictStr] - level: ResponseLevel = "success" - random: StrictStr - cached: StrictBool - runtime: StrictInt - keywords: List[StrictStr] = [] - timestamp: StrictStr - format: ResponseFormat = "text/plain" - - class Config: - """Pydantic model configuration.""" - - title = "Query Response" - description = "Looking glass response" - fields = { - "level": {"title": "Level", "description": "Severity"}, - "cached": { - "title": "Cached", - "description": "`true` if the response is from a previously cached query.", - }, - "random": { - "title": "Random", - "description": "Random string to prevent client or intermediate caching.", - "example": "504cbdb47eb8310ca237bf512c3e10b44b0a3d85868c4b64a20037dc1c3ef857", - }, - "runtime": { - "title": "Runtime", - "description": "Time it took to run the query in seconds.", - "example": 6, - }, - "timestamp": { - "title": "Timestamp", - "description": "UTC Time at which the backend application received the query.", - "example": "2020-04-18 14:45:37", - }, - "format": { - "title": "Format", - "description": "Response [MIME Type](http://www.iana.org/assignments/media-types/media-types.xhtml). Supported values: `text/plain` and `application/json`.", - "example": "text/plain", - }, - "keywords": { - "title": "Keywords", - "description": "Relevant keyword values contained in the `output` field, which can be used for formatting.", - "example": ["1.1.1.0/24", "best #1"], - }, - "output": { - "title": "Output", - "description": "Looking Glass Response", - "example": """ +schema_query_output = { + "title": "Output", + "description": "Looking Glass Response", + "example": """ BGP routing table entry for 1.1.1.0/24, version 224184946 BGP Bestpath: deterministic-med Paths: (12 available, best #1, table default) @@ -119,12 +27,48 @@ class Config: Origin IGP, metric 0, localpref 250, valid, internal Community: 65000:1 65000:2 """, - }, - } - schema_extra = { - "examples": [ - { - "output": """ +} + +schema_query_level = {"title": "Level", "description": "Severity"} + +schema_query_random = { + "title": "Random", + "description": "Random string to prevent client or intermediate caching.", + "example": "504cbdb47eb8310ca237bf512c3e10b44b0a3d85868c4b64a20037dc1c3ef857", +} + +schema_query_cached = { + "title": "Cached", + "description": "`true` if the response is from a previously cached query.", +} + +schema_query_runtime = { + "title": "Runtime", + "description": "Time it took to run the query in seconds.", + "example": 6, +} + +schema_query_keywords = { + "title": "Keywords", + "description": "Relevant keyword values contained in the `output` field, which can be used for formatting.", + "example": ["1.1.1.0/24", "best #1"], +} + +schema_query_timestamp = { + "title": "Timestamp", + "description": "UTC Time at which the backend application received the query.", + "example": "2020-04-18 14:45:37", +} + +schema_query_format = { + "title": "Format", + "description": "Response [MIME Type](http://www.iana.org/assignments/media-types/media-types.xhtml). Supported values: `text/plain` and `application/json`.", + "example": "text/plain", +} + +schema_query_examples = [ + { + "output": """ BGP routing table entry for 1.1.1.0/24, version 224184946 BGP Bestpath: deterministic-med Paths: (12 available, best #1, table default) @@ -135,59 +79,94 @@ class Config: Origin IGP, metric 0, localpref 250, valid, internal Community: 65000:1 65000:2 """, - "level": "success", - "keywords": ["1.1.1.0/24", "best #1"], - } - ] - } + "level": "success", + "keywords": ["1.1.1.0/24", "best #1"], + } +] +schema_query_error_output = { + "title": "Output", + "description": "Error Details", + "example": "192.0.2.1/32 is not allowed.", +} -class Vrf(BaseModel): - """Response model for /api/devices VRFs.""" +schema_query_error_level = {"title": "Level", "description": "Error Severity", "example": "danger"} + +schema_query_error_keywords = { + "title": "Keywords", + "description": "Relevant keyword values contained in the `output` field, which can be used for formatting.", + "example": ["192.0.2.1/32"], +} - name: StrictStr - display_name: StrictStr - class Config: - """Pydantic model configuration.""" +class QueryError(BaseModel): + """Query response model.""" - title = "VRF" - description = "VRF attributes" - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ + "title": "Query Error", + "description": "Response received when there is an error executing the requested query.", "examples": [ - {"name": "default", "display_name": "Global Routing Table"}, - {"name": "customer_vrf_name", "display_name": "Customer Name"}, - ] + { + "output": "192.0.2.1/32 is not allowed.", + "level": "danger", + "keywords": ["192.0.2.1/32"], + } + ], } + ) + output: str = Field(json_schema_extra=schema_query_error_output) + level: ErrorName = Field("danger", json_schema_extra=schema_query_error_level) + # id: t.Optional[StrictStr] + keywords: t.List[StrictStr] = Field([], json_schema_extra=schema_query_error_keywords) -class Network(BaseModel): - """Response model for /api/devices networks.""" + @field_validator("output") + def validate_output(cls: "QueryError", value): + """If no output is specified, use a customizable generic message.""" + if value is None: + (messages := use_state("params").messages) + return messages.general + return value - name: StrictStr - display_name: StrictStr - class Config: - """Pydantic model configuration.""" +class QueryResponse(BaseModel): + """Query response model.""" + + model_config = ConfigDict( + json_schema_extra={ + "title": "Query Response", + "description": "Looking glass response", + "examples": schema_query_examples, + } + ) - title = "Network" - description = "Network/ASN attributes" - schema_extra = {"examples": [{"name": "primary", "display_name": "AS65000"}]} + output: t.Union[t.Dict, StrictStr] = Field(json_schema_extra=schema_query_output) + level: ResponseLevel = Field("success", json_schema_extra=schema_query_level) + random: str = Field(json_schema_extra=schema_query_random) + cached: bool = Field(json_schema_extra=schema_query_cached) + runtime: int = Field(json_schema_extra=schema_query_runtime) + keywords: t.List[str] = Field([], json_schema_extra=schema_query_keywords) + timestamp: str = Field(json_schema_extra=schema_query_timestamp) + format: ResponseFormat = Field("text/plain", json_schema_extra=schema_query_format) class RoutersResponse(BaseModel): """Response model for /api/devices list items.""" - name: StrictStr - network: Network - vrfs: List[Vrf] - - class Config: - """Pydantic model configuration.""" + model_config = ConfigDict( + json_schema_extra={ + "title": "Device", + "description": "Device attributes", + "examples": [ + {"id": "nyc_router_1", "name": "NYC Router 1", "group": "New York City, NY"} + ], + } + ) - title = "Device" - description = "Per-device attributes" - schema_extra = {"examples": [{"name": "router01-nyc01", "location": "nyc01"}]} + id: StrictStr + name: StrictStr + group: t.Union[StrictStr, None] class CommunityResponse(BaseModel): @@ -201,36 +180,26 @@ class CommunityResponse(BaseModel): class SupportedQueryResponse(BaseModel): """Response model for /api/queries list items.""" + model_config = ConfigDict( + json_schema_extra={ + "title": "Query Type", + "description": "If enabled is `true`, the `name` field may be used to specify the query type.", + "examples": [{"name": "bgp_route", "display_name": "BGP Route", "enable": True}], + } + ) + name: StrictStr display_name: StrictStr enable: StrictBool - class Config: - """Pydantic model configuration.""" - - title = "Query Type" - description = "If enabled is `true`, the `name` field may be used to specify the query type." - schema_extra = { - "examples": [ - {"name": "bgp_route", "display_name": "BGP Route", "enable": True} - ] - } - class InfoResponse(BaseModel): """Response model for /api/info endpoint.""" - name: StrictStr - organization: StrictStr - primary_asn: StrictInt - version: StrictStr - - class Config: - """Pydantic model configuration.""" - - title = "System Information" - description = "General information about this looking glass." - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ + "title": "System Information", + "description": "General information about this looking glass.", "examples": [ { "name": "hyperglass", @@ -238,5 +207,11 @@ class Config: "primary_asn": 65000, "version": "hyperglass 1.0.0-beta.52", } - ] + ], } + ) + + name: StrictStr + organization: StrictStr + primary_asn: StrictInt + version: StrictStr diff --git a/hyperglass/models/api/rfc8522.py b/hyperglass/models/api/rfc8522.py index b546e850d..f7310b5f8 100644 --- a/hyperglass/models/api/rfc8522.py +++ b/hyperglass/models/api/rfc8522.py @@ -3,12 +3,12 @@ # Standard Library # flake8: noqa import math +import typing as t import secrets -from typing import List, Union, Optional from datetime import datetime # Third Party -from pydantic import BaseModel, StrictInt, StrictStr, StrictFloat, constr, validator +from pydantic import Field, BaseModel, ConfigDict, field_validator """Patterns: GET /.well-known/looking-glass/v1/ping/2001:DB8::35?protocol=2,1 @@ -22,55 +22,49 @@ GET /.well-known/looking-glass/v1/cmd """ +QueryFormat = t.Literal[r"text/plain", r"application/json"] + class _HyperglassQuery(BaseModel): - class Config: - validate_all = True - validate_assignment = True + model_config = ConfigDict(validate_assignment=True, validate_default=True) class BaseQuery(_HyperglassQuery): - protocol: StrictStr = "1,1" - router: StrictStr - routerindex: StrictInt - random: StrictStr = secrets.token_urlsafe(16) - vrf: Optional[StrictStr] - runtime: StrictInt = 30 - query_format: constr(regex=r"(text\/plain|application\/json)") = "text/plain" - - @validator("runtime") + protocol: str = "1,1" + router: str + routerindex: int + random: str = secrets.token_urlsafe(16) + runtime: int = 30 + query_format: QueryFormat = Field("text/plain", alias="format") + + @field_validator("runtime") def validate_runtime(cls, value): if isinstance(value, float) and math.modf(value)[0] == 0: value = math.ceil(value) return value - class Config: - fields = {"query_format": "format"} - class BaseData(_HyperglassQuery): - router: StrictStr + model_config = ConfigDict(extra="allow") + + router: str performed_at: datetime - runtime: Union[StrictFloat, StrictInt] - output: List[StrictStr] - data_format: StrictStr + runtime: t.Union[float, int] + output: t.List[str] + data_format: str = Field(alias="format") - @validator("runtime") + @field_validator("runtime") def validate_runtime(cls, value): if isinstance(value, float) and math.modf(value)[0] == 0: value = math.ceil(value) return value - class Config: - fields = {"data_format": "format"} - extra = "allow" - class QueryError(_HyperglassQuery): - status: constr(regex=r"error") - message: StrictStr + status: t.Literal["error"] + message: str class QueryResponse(_HyperglassQuery): - status: constr(regex=r"success|fail") + status: t.Literal["success", "fail"] data: BaseData diff --git a/hyperglass/models/api/types.py b/hyperglass/models/api/types.py index e815b1ecf..397ca8ffa 100644 --- a/hyperglass/models/api/types.py +++ b/hyperglass/models/api/types.py @@ -1,26 +1,20 @@ """Custom validation types.""" +# Standard Library +import typing as t + +# Third Party +from pydantic import AfterValidator + # Project from hyperglass.constants import SUPPORTED_QUERY_TYPES -class SupportedQuery(str): - """Query Type Validation Model.""" - - @classmethod - def __get_validators__(cls): - """Pydantic custom type method.""" - yield cls.validate +def validate_query_type(value: str) -> str: + """Ensure query type is supported by hyperglass.""" + if value not in SUPPORTED_QUERY_TYPES: + raise ValueError("'{}' is not a supported query type".format(value)) + return value - @classmethod - def validate(cls, value): - """Ensure query type is supported by hyperglass.""" - if not isinstance(value, str): - raise TypeError("query_type must be a string") - if value not in SUPPORTED_QUERY_TYPES: - raise ValueError(f"'{value}' is not a supported query type") - return value - def __repr__(self): - """Stringify custom field representation.""" - return f"SupportedQuery({super().__repr__()})" +SupportedQuery = t.Annotated[str, AfterValidator(validate_query_type)] diff --git a/hyperglass/models/api/validators.py b/hyperglass/models/api/validators.py deleted file mode 100644 index 48b175646..000000000 --- a/hyperglass/models/api/validators.py +++ /dev/null @@ -1,234 +0,0 @@ -"""Input validation functions for submitted queries.""" - -# Standard Library -import re -from ipaddress import ip_network - -# Project -from hyperglass.log import log -from hyperglass.exceptions import InputInvalid, InputNotAllowed -from hyperglass.configuration import params -from hyperglass.external.bgptools import network_info_sync - - -def _member_of(target, network): - """Check if IP address belongs to network. - - Arguments: - target {object} -- Target IPv4/IPv6 address - network {object} -- ACL network - - Returns: - {bool} -- True if target is a member of network, False if not - """ - log.debug("Checking membership of {} for {}", target, network) - - membership = False - if ( - network.network_address <= target.network_address - and network.broadcast_address >= target.broadcast_address # NOQA: W503 - ): - log.debug("{} is a member of {}", target, network) - membership = True - return membership - - -def _prefix_range(target, ge, le): - """Verify if target prefix length is within ge/le threshold. - - Arguments: - target {IPv4Network|IPv6Network} -- Valid IPv4/IPv6 Network - ge {int} -- Greater than - le {int} -- Less than - - Returns: - {bool} -- True if target in range; False if not - """ - matched = False - if target.prefixlen <= le and target.prefixlen >= ge: - matched = True - return matched - - -def validate_ip(value, query_type, query_vrf): # noqa: C901 - """Ensure input IP address is both valid and not within restricted allocations. - - Arguments: - value {str} -- Unvalidated IP Address - query_type {str} -- Valid query type - query_vrf {object} -- Matched query vrf - Raises: - ValueError: Raised if input IP address is not an IP address. - ValueError: Raised if IP address is valid, but is within a restricted range. - Returns: - Union[IPv4Address, IPv6Address] -- Validated IP address object - """ - query_type_params = getattr(params.queries, query_type) - try: - - # Attempt to use IP object factory to create an IP address object - valid_ip = ip_network(value) - - except ValueError: - raise InputInvalid( - params.messages.invalid_input, - target=value, - query_type=query_type_params.display_name, - ) - - # Test the valid IP address to determine if it is: - # - Unspecified (See RFC5735, RFC2373) - # - Loopback (See RFC5735, RFC2373) - # - Otherwise IETF Reserved - # ...and returns an error if so. - if valid_ip.is_reserved or valid_ip.is_unspecified or valid_ip.is_loopback: - raise InputInvalid( - params.messages.invalid_input, - target=value, - query_type=query_type_params.display_name, - ) - - ip_version = valid_ip.version - - vrf_afi = getattr(query_vrf, f"ipv{ip_version}") - - if vrf_afi is None: - raise InputInvalid( - params.messages.feature_not_enabled, - feature=f"IPv{ip_version}", - device_name=f"VRF {query_vrf.display_name}", - ) - - for ace in [a for a in vrf_afi.access_list if a.network.version == ip_version]: - if _member_of(valid_ip, ace.network): - if query_type == "bgp_route" and _prefix_range(valid_ip, ace.ge, ace.le): - pass - - if ace.action == "permit": - log.debug( - "{t} is allowed by access-list {a}", t=str(valid_ip), a=repr(ace) - ) - break - elif ace.action == "deny": - raise InputNotAllowed( - params.messages.acl_denied, - target=str(valid_ip), - denied_network=str(ace.network), - ) - - # Handling logic for host queries, e.g. 192.0.2.1 vs. 192.0.2.0/24 - if valid_ip.num_addresses == 1: - - # For a host query with ping or traceroute query types, convert - # the query_target to an IP address instead of a network. - if query_type in ("ping", "traceroute"): - new_ip = valid_ip.network_address - - log.debug( - "Converted '{o}' to '{n}' for '{q}' query", - o=valid_ip, - n=new_ip, - q=query_type, - ) - - valid_ip = new_ip - - # Get the containing prefix for a host query if: - # - Query type is bgp_route - # - force_cidr option is enabled - # - Query target is not a private address/network - elif ( - query_type in ("bgp_route",) - and vrf_afi.force_cidr - and not valid_ip.is_private - ): - log.debug("Getting containing prefix for {q}", q=str(valid_ip)) - - ip_str = str(valid_ip.network_address) - network_info = network_info_sync(ip_str) - containing_prefix = network_info.get(ip_str, {}).get("prefix") - - if containing_prefix is None: - log.error( - "Unable to find containing prefix for {}. Got: {}", - str(valid_ip), - network_info, - ) - raise InputInvalid("{q} does not have a containing prefix", q=ip_str) - - try: - - valid_ip = ip_network(containing_prefix) - log.debug("Containing prefix: {p}", p=str(valid_ip)) - - except ValueError as err: - log.error( - "Unable to find containing prefix for {q}. Error: {e}", - q=str(valid_ip), - e=err, - ) - raise InputInvalid( - "{q} does does not have a containing prefix", q=valid_ip - ) - - # For a host query with bgp_route query type and force_cidr - # disabled, convert the host query to a single IP address. - elif query_type in ("bgp_route",) and not vrf_afi.force_cidr: - - valid_ip = valid_ip.network_address - - log.debug("Validation passed for {ip}", ip=value) - return valid_ip - - -def validate_community_input(value): - """Validate input communities against configured or default regex pattern.""" - - # RFC4360: Extended Communities (New Format) - if re.match(params.queries.bgp_community.pattern.extended_as, value): - pass - - # RFC4360: Extended Communities (32 Bit Format) - elif re.match(params.queries.bgp_community.pattern.decimal, value): - pass - - # RFC8092: Large Communities - elif re.match(params.queries.bgp_community.pattern.large, value): - pass - - else: - raise InputInvalid( - params.messages.invalid_input, - target=value, - query_type=params.queries.bgp_community.display_name, - ) - return value - - -def validate_community_select(value): - """Validate selected community against configured communities.""" - - communities = tuple(c.community for c in params.queries.bgp_community.communities) - if value not in communities: - raise InputInvalid( - params.messages.invalid_input, - target=value, - query_type=params.queries.bgp_community.display_name, - ) - return value - - -def validate_aspath(value): - """Validate input AS_PATH against configured or default regext pattern.""" - - mode = params.queries.bgp_aspath.pattern.mode - pattern = getattr(params.queries.bgp_aspath.pattern, mode) - - if not bool(re.match(pattern, value)): - raise InputInvalid( - params.messages.invalid_input, - target=value, - query_type=params.queries.bgp_aspath.display_name, - ) - - return value diff --git a/hyperglass/models/commands/__init__.py b/hyperglass/models/commands/__init__.py deleted file mode 100644 index 7a49b0b0b..000000000 --- a/hyperglass/models/commands/__init__.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Validate command configuration variables.""" - -# Local -from .frr import FRRCommands -from .bird import BIRDCommands -from .tnsr import TNSRCommands -from .vyos import VyosCommands -from ..main import HyperglassModelExtra -from .common import CommandGroup -from .huawei import HuaweiCommands -from .juniper import JuniperCommands -from .cisco_xr import CiscoXRCommands -from .cisco_ios import CiscoIOSCommands -from .arista_eos import AristaEOSCommands -from .cisco_nxos import CiscoNXOSCommands -from .nokia_sros import NokiaSROSCommands -from .mikrotik_routeros import MikrotikRouterOS -from .mikrotik_switchos import MikrotikSwitchOS - -_NOS_MAP = { - "arista_eos": AristaEOSCommands, - "bird": BIRDCommands, - "cisco_ios": CiscoIOSCommands, - "cisco_nxos": CiscoNXOSCommands, - "cisco_xr": CiscoXRCommands, - "frr": FRRCommands, - "huawei": HuaweiCommands, - "juniper": JuniperCommands, - "mikrotik_routeros": MikrotikRouterOS, - "mikrotik_switchos": MikrotikSwitchOS, - "nokia_sros": NokiaSROSCommands, - "tnsr": TNSRCommands, - "vyos": VyosCommands, -} - - -class Commands(HyperglassModelExtra): - """Base class for command definitions.""" - - arista_eos: CommandGroup = AristaEOSCommands() - bird: CommandGroup = BIRDCommands() - cisco_ios: CommandGroup = CiscoIOSCommands() - cisco_nxos: CommandGroup = CiscoNXOSCommands() - cisco_xr: CommandGroup = CiscoXRCommands() - frr: CommandGroup = FRRCommands() - huawei: CommandGroup = HuaweiCommands() - juniper: CommandGroup = JuniperCommands() - mikrotik_routeros: CommandGroup = MikrotikRouterOS() - mikrotik_switchos: CommandGroup = MikrotikSwitchOS() - nokia_sros: CommandGroup = NokiaSROSCommands() - tnsr: CommandGroup = TNSRCommands() - vyos: CommandGroup = VyosCommands() - - @classmethod - def import_params(cls, **input_params): - """Import loaded YAML, initialize per-command definitions. - - Dynamically set attributes for the command class. - - Arguments: - input_params {dict} -- Unvalidated command definitions - - Returns: - {object} -- Validated commands object - """ - obj = Commands() - for nos, cmds in input_params.items(): - nos_cmd_set = _NOS_MAP.get(nos, CommandGroup) - nos_cmds = nos_cmd_set(**cmds) - setattr(obj, nos, nos_cmds) - return obj - - class Config: - """Override pydantic config.""" - - validate_all = False diff --git a/hyperglass/models/commands/_mikrotik_base.py b/hyperglass/models/commands/_mikrotik_base.py deleted file mode 100644 index 86ac480eb..000000000 --- a/hyperglass/models/commands/_mikrotik_base.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Base Mikrotik Commands Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Default commands for ipv4 commands.""" - - bgp_community: StrictStr = "ip route print where bgp-communities={target}" - bgp_aspath: StrictStr = "ip route print where bgp-as-path={target}" - bgp_route: StrictStr = "ip route print where dst-address={target}" - ping: StrictStr = "ping src-address={source} count=5 {target}" - traceroute: StrictStr = "tool traceroute src-address={source} timeout=1 duration=5 count=1 {target}" - - -class _IPv6(CommandSet): - """Default commands for ipv6 commands.""" - - bgp_community: StrictStr = "ipv6 route print where bgp-communities={target}" - bgp_aspath: StrictStr = "ipv6 route print where bgp-as-path={target}" - bgp_route: StrictStr = "ipv6 route print where dst-address={target}" - ping: StrictStr = "ping src-address={source} count=5 {target}" - traceroute: StrictStr = "tool traceroute src-address={source} timeout=1 duration=5 count=1 {target}" - - -class _VPNIPv4(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = "ip route print where bgp-communities={target} routing-mark={vrf}" - bgp_aspath: StrictStr = "ip route print where bgp-as-path={target} routing-mark={vrf}" - bgp_route: StrictStr = "ip route print where dst-address={target} routing-mark={vrf}" - ping: StrictStr = "ping src-address={source} count=5 routing-table={vrf} {target}" - traceroute: StrictStr = "tool traceroute src-address={source} timeout=1 duration=5 count=1 routing-table={vrf} {target}" - - -class _VPNIPv6(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = "ipv6 route print where bgp-communities={target} routing-mark={vrf}" - bgp_aspath: StrictStr = "ipv6 route print where bgp-as-path={target} routing-mark={vrf}" - bgp_route: StrictStr = "ipv6 route print where dst-address={target} routing-mark={vrf}" - ping: StrictStr = "ping src-address={source} count=5 routing-table={vrf} {target}" - traceroute: StrictStr = "tool traceroute src-address={source} timeout=1 duration=5 count=1 routing-table={vrf} {target}" - - -class MikrotikCommands(CommandGroup): - """Validation model for default mikrotik commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/commands/arista_eos.py b/hyperglass/models/commands/arista_eos.py deleted file mode 100644 index 29dbc3ac8..000000000 --- a/hyperglass/models/commands/arista_eos.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Arista EOS Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Validation model for non-default dual afi commands.""" - - bgp_route: StrictStr = "show ip bgp {target}" - bgp_aspath: StrictStr = "show ip bgp regexp {target}" - bgp_community: StrictStr = "show ip bgp community {target}" - ping: StrictStr = "ping ip {target} source {source}" - traceroute: StrictStr = "traceroute ip {target} source {source}" - - -class _IPv6(CommandSet): - """Validation model for non-default ipv4 commands.""" - - bgp_route: StrictStr = "show ipv6 bgp {target}" - bgp_aspath: StrictStr = "show ipv6 bgp regexp {target}" - bgp_community: StrictStr = "show ipv6 bgp community {target}" - ping: StrictStr = "ping ipv6 {target} source {source}" - traceroute: StrictStr = "traceroute ipv6 {target} source {source}" - - -class _VPNIPv4(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = "show ip bgp {target} vrf {vrf}" - bgp_aspath: StrictStr = "show ip bgp regexp {target} vrf {vrf}" - bgp_community: StrictStr = "show ip bgp community {target} vrf {vrf}" - ping: StrictStr = "ping vrf {vrf} ip {target} source {source}" - traceroute: StrictStr = "traceroute vrf {vrf} ip {target} source {source}" - - -class _VPNIPv6(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = "show ipv6 bgp {target} vrf {vrf}" - bgp_aspath: StrictStr = "show ipv6 bgp regexp {target} vrf {vrf}" - bgp_community: StrictStr = "show ipv6 bgp community {target} vrf {vrf}" - ping: StrictStr = "ping vrf {vrf} ipv6 {target} source {source}" - traceroute: StrictStr = "traceroute vrf {vrf} ipv6 {target} source {source}" - - -_structured = CommandGroup( - ipv4_default=CommandSet( - bgp_route="show ip bgp {target} | json", - bgp_aspath="show ip bgp regexp {target} | json", - bgp_community="show ip bgp community {target} | json", - ping="ping ip {target} source {source}", - traceroute="traceroute ip {target} source {source}", - ), - ipv6_default=CommandSet( - bgp_route="show ipv6 bgp {target} | json", - bgp_aspath="show ipv6 bgp regexp {target} | json", - bgp_community="show ipv6 bgp community {target} | json", - ping="ping ipv6 {target} source {source}", - traceroute="traceroute ipv6 {target} source {source}", - ), - ipv4_vpn=CommandSet( - bgp_route="show ip bgp {target} vrf {vrf} | json", - bgp_aspath="show ip bgp regexp {target} vrf {vrf} | json", - bgp_community="show ip bgp community {target} vrf {vrf} | json", - ping="ping vrf {vrf} ip {target} source {source}", - traceroute="traceroute vrf {vrf} ip {target} source {source}", - ), - ipv6_vpn=CommandSet( - bgp_route="show ipv6 bgp {target} vrf {vrf} | json", - bgp_aspath="show ipv6 bgp regexp {target} vrf {vrf} | json", - bgp_community="show ipv6 bgp community {target} vrf {vrf} | json", - ping="ping vrf {vrf} ipv6 {target} source {source}", - traceroute="traceroute vrf {vrf} ipv6 {target} source {source}", - ), -) - - -class AristaEOSCommands(CommandGroup): - """Validation model for default arista_eos commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() - - def __init__(self, **kwargs): - """Initialize command group, ensure structured fields are not overridden.""" - super().__init__(**kwargs) - self.structured = _structured diff --git a/hyperglass/models/commands/bird.py b/hyperglass/models/commands/bird.py deleted file mode 100644 index 9a86f0a54..000000000 --- a/hyperglass/models/commands/bird.py +++ /dev/null @@ -1,56 +0,0 @@ -"""BIRD Routing Daemon Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Default commands for ipv4 commands.""" - - bgp_community: str = 'birdc "show route all where {target} ~ bgp_community"' - bgp_aspath: str = 'birdc "show route all where bgp_path ~ {target}"' - bgp_route: str = 'birdc "show route all where {target} ~ net"' - ping: str = "ping -4 -c 5 -I {source} {target}" - traceroute: str = "traceroute -4 -w 1 -q 1 -s {source} {target}" - - -class _IPv6(CommandSet): - """Default commands for ipv6 commands.""" - - bgp_community: str = 'birdc "show route all where {target} ~ bgp_community"' - bgp_aspath: str = 'birdc "show route all where bgp_path ~ {target}"' - bgp_route: str = 'birdc "show route all where {target} ~ net"' - ping: StrictStr = "ping -6 -c 5 -I {source} {target}" - traceroute: StrictStr = "traceroute -6 -w 1 -q 1 -s {source} {target}" - - -class _VPNIPv4(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: str = 'birdc "show route all table {vrf} where {target} ~ bgp_community"' - bgp_aspath: str = 'birdc "show route all table {vrf} where bgp_path ~ {target}"' - bgp_route: str = 'birdc "show route all table {vrf} where {target} ~ net"' - ping: StrictStr = "ping -4 -c 5 -I {source} {target}" - traceroute: StrictStr = "traceroute -4 -w 1 -q 1 -s {source} {target}" - - -class _VPNIPv6(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: str = 'birdc "show route all table {vrf} where {target} ~ bgp_community"' - bgp_aspath: str = 'birdc "show route all table {vrf} where bgp_path ~ {target}"' - bgp_route: str = 'birdc "show route all table {vrf} where {target} ~ net"' - ping: StrictStr = "ping -6 -c 5 -I {source} {target}" - traceroute: StrictStr = "traceroute -6 -w 1 -q 1 -s {source} {target}" - - -class BIRDCommands(CommandGroup): - """Validation model for default BIRD commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/commands/cisco_ios.py b/hyperglass/models/commands/cisco_ios.py deleted file mode 100644 index affd3e5d4..000000000 --- a/hyperglass/models/commands/cisco_ios.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Cisco IOS Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Default commands for ipv4 commands.""" - - bgp_community: StrictStr = "show bgp ipv4 unicast community {target}" - bgp_aspath: StrictStr = 'show bgp ipv4 unicast quote-regexp "{target}"' - bgp_route: StrictStr = "show bgp ipv4 unicast {target} | exclude pathid:|Epoch" - ping: StrictStr = "ping {target} repeat 5 source {source}" - traceroute: StrictStr = "traceroute {target} timeout 1 probe 2 source {source}" - - -class _IPv6(CommandSet): - """Default commands for ipv6 commands.""" - - bgp_community: StrictStr = "show bgp ipv6 unicast community {target}" - bgp_aspath: StrictStr = 'show bgp ipv6 unicast quote-regexp "{target}"' - bgp_route: StrictStr = "show bgp ipv6 unicast {target} | exclude pathid:|Epoch" - ping: StrictStr = "ping ipv6 {target} repeat 5 source {source}" - traceroute: StrictStr = ( - "traceroute ipv6 {target} timeout 1 probe 2 source {source}" - ) - - -class _VPNIPv4(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = "show bgp vpnv4 unicast vrf {vrf} community {target}" - bgp_aspath: StrictStr = 'show bgp vpnv4 unicast vrf {vrf} quote-regexp "{target}"' - bgp_route: StrictStr = "show bgp vpnv4 unicast vrf {vrf} {target}" - ping: StrictStr = "ping vrf {vrf} {target} repeat 5 source {source}" - traceroute: StrictStr = ( - "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}" - ) - - -class _VPNIPv6(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = "show bgp vpnv6 unicast vrf {vrf} community {target}" - bgp_aspath: StrictStr = 'show bgp vpnv6 unicast vrf {vrf} quote-regexp "{target}"' - bgp_route: StrictStr = "show bgp vpnv6 unicast vrf {vrf} {target}" - ping: StrictStr = "ping vrf {vrf} {target} repeat 5 source {source}" - traceroute: StrictStr = ( - "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}" - ) - - -class CiscoIOSCommands(CommandGroup): - """Validation model for default cisco_ios commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/commands/cisco_nxos.py b/hyperglass/models/commands/cisco_nxos.py deleted file mode 100644 index 58e61fcdb..000000000 --- a/hyperglass/models/commands/cisco_nxos.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Cisco NX-OS Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Validation model for non-default dual afi commands.""" - - bgp_route: StrictStr = "show bgp ipv4 unicast {target}" - bgp_aspath: StrictStr = 'show bgp ipv4 unicast regexp "{target}"' - bgp_community: StrictStr = "show bgp ipv4 unicast community {target}" - ping: StrictStr = "ping {target} source {source}" - traceroute: StrictStr = "traceroute {target} source {source}" - - -class _IPv6(CommandSet): - """Validation model for non-default ipv4 commands.""" - - bgp_route: StrictStr = "show bgp ipv6 unicast {target}" - bgp_aspath: StrictStr = 'show bgp ipv6 unicast regexp "{target}"' - bgp_community: StrictStr = "show bgp ipv6 unicast community {target}" - ping: StrictStr = "ping6 {target} source {source}" - traceroute: StrictStr = "traceroute6 {target} source {source}" - - -class _VPNIPv4(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = "show bgp ipv4 unicast {target} vrf {vrf}" - bgp_aspath: StrictStr = 'show bgp ipv4 unicast regexp "{target}" vrf {vrf}' - bgp_community: StrictStr = "show bgp ipv4 unicast community {target} vrf {vrf}" - ping: StrictStr = "ping {target} source {source} vrf {vrf}" - traceroute: StrictStr = "traceroute {target} source {source} vrf {vrf}" - - -class _VPNIPv6(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = "show bgp ipv6 unicast {target} vrf {vrf}" - bgp_aspath: StrictStr = 'show bgp ipv6 unicast regexp "{target}" vrf {vrf}' - bgp_community: StrictStr = "show bgp ipv6 unicast community {target} vrf {vrf}" - ping: StrictStr = "ping6 {target} source {source} vrf {vrf}" - traceroute: StrictStr = "traceroute6 {target} source {source} vrf {vrf}" - - -class CiscoNXOSCommands(CommandGroup): - """Validation model for default cisco_nxos commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/commands/cisco_xr.py b/hyperglass/models/commands/cisco_xr.py deleted file mode 100644 index 01ed2a50e..000000000 --- a/hyperglass/models/commands/cisco_xr.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Cisco IOS XR Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Validation model for non-default dual afi commands.""" - - bgp_route: StrictStr = "show bgp ipv4 unicast {target}" - bgp_aspath: StrictStr = "show bgp ipv4 unicast regexp {target}" - bgp_community: StrictStr = "show bgp ipv4 unicast community {target}" - ping: StrictStr = "ping ipv4 {target} count 5 source {source}" - traceroute: StrictStr = "traceroute ipv4 {target} timeout 1 probe 2 source {source}" - - -class _IPv6(CommandSet): - """Validation model for non-default ipv4 commands.""" - - bgp_route: StrictStr = "show bgp ipv6 unicast {target}" - bgp_aspath: StrictStr = "show bgp ipv6 unicast regexp {target}" - bgp_community: StrictStr = "show bgp ipv6 unicast community {target}" - ping: StrictStr = "ping ipv6 {target} count 5 source {source}" - traceroute: StrictStr = "traceroute ipv6 {target} timeout 1 probe 2 source {source}" - - -class _VPNIPv4(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = "show bgp vpnv4 unicast vrf {vrf} {target}" - bgp_aspath: StrictStr = "show bgp vpnv4 unicast vrf {vrf} regexp {target}" - bgp_community: StrictStr = "show bgp vpnv4 unicast vrf {vrf} community {target}" - ping: StrictStr = "ping vrf {vrf} {target} count 5 source {source}" - traceroute: StrictStr = "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}" - - -class _VPNIPv6(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = "show bgp vpnv6 unicast vrf {vrf} {target}" - bgp_aspath: StrictStr = "show bgp vpnv6 unicast vrf {vrf} regexp {target}" - bgp_community: StrictStr = "show bgp vpnv6 unicast vrf {vrf} community {target}" - ping: StrictStr = "ping vrf {vrf} {target} count 5 source {source}" - traceroute: StrictStr = "traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}" - - -class CiscoXRCommands(CommandGroup): - """Validation model for default cisco_xr commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/commands/common.py b/hyperglass/models/commands/common.py deleted file mode 100644 index 347efd244..000000000 --- a/hyperglass/models/commands/common.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Models common to entire commands module.""" - -# Third Party -from pydantic import StrictStr - -# Local -from ..main import HyperglassModel, HyperglassModelExtra - - -class CommandSet(HyperglassModel): - """Command set, defined per-AFI.""" - - bgp_route: StrictStr - bgp_aspath: StrictStr - bgp_community: StrictStr - ping: StrictStr - traceroute: StrictStr - - -class CommandGroup(HyperglassModelExtra): - """Validation model for all commands.""" - - ipv4_default: CommandSet - ipv6_default: CommandSet - ipv4_vpn: CommandSet - ipv6_vpn: CommandSet diff --git a/hyperglass/models/commands/frr.py b/hyperglass/models/commands/frr.py deleted file mode 100644 index 2dde708a9..000000000 --- a/hyperglass/models/commands/frr.py +++ /dev/null @@ -1,56 +0,0 @@ -"""FRRouting Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Default commands for ipv4 commands.""" - - bgp_community: StrictStr = 'vtysh -c "show bgp ipv4 unicast community {target}"' - bgp_aspath: StrictStr = 'vtysh -c "show bgp ipv4 unicast regexp {target}"' - bgp_route: StrictStr = 'vtysh -c "show bgp ipv4 unicast {target}"' - ping: StrictStr = "ping -4 -c 5 -I {source} {target}" - traceroute: StrictStr = "traceroute -4 -w 1 -q 1 -s {source} {target}" - - -class _IPv6(CommandSet): - """Default commands for ipv6 commands.""" - - bgp_community: StrictStr = 'vtysh -c "show bgp ipv6 unicast community {target}"' - bgp_aspath: StrictStr = 'vtysh -c "show bgp ipv6 unicast regexp {target}"' - bgp_route: StrictStr = 'vtysh -c "show bgp ipv6 unicast {target}"' - ping: StrictStr = "ping -6 -c 5 -I {source} {target}" - traceroute: StrictStr = "traceroute -6 -w 1 -q 1 -s {source} {target}" - - -class _VPNIPv4(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = 'vtysh -c "show bgp vrf {vrf} ipv4 unicast community {target}"' - bgp_aspath: StrictStr = 'vtysh -c "show bgp vrf {vrf} ipv4 unicast regexp {target}"' - bgp_route: StrictStr = 'vtysh -c "show bgp vrf {vrf} ipv4 unicast {target}"' - ping: StrictStr = "ping -4 -c 5 -I {source} {target}" - traceroute: StrictStr = "traceroute -4 -w 1 -q 1 -s {source} {target}" - - -class _VPNIPv6(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = 'vtysh -c "show bgp vrf {vrf} ipv6 unicast community {target}"' - bgp_aspath: StrictStr = 'vtysh -c "show bgp vrf {vrf} ipv6 unicast regexp {target}"' - bgp_route: StrictStr = 'vtysh -c "show bgp vrf {vrf} ipv6 unicast {target}"' - ping: StrictStr = "ping -6 -c 5 -I {source} {target}" - traceroute: StrictStr = "traceroute -6 -w 1 -q 1 -s {source} {target}" - - -class FRRCommands(CommandGroup): - """Validation model for default FRRouting commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/commands/huawei.py b/hyperglass/models/commands/huawei.py deleted file mode 100644 index eea35d19f..000000000 --- a/hyperglass/models/commands/huawei.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Huawei Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Default commands for ipv4 commands.""" - - bgp_community: StrictStr = "display bgp routing-table regular-expression {target}" - bgp_aspath: StrictStr = "display bgp routing-table regular-expression {target}" - bgp_route: StrictStr = "display bgp routing-table {target}" - ping: StrictStr = "ping -c 5 -a {source} {target}" - traceroute: StrictStr = "tracert -q 2 -f 1 -a {source} {target}" - - -class _IPv6(CommandSet): - """Default commands for ipv6 commands.""" - - bgp_community: StrictStr = "display bgp ipv6 routing-table community {target}" - bgp_aspath: StrictStr = "display bgp ipv6 routing-table regular-expression {target}" - bgp_route: StrictStr = "display bgp ipv6 routing-table {target}" - ping: StrictStr = "ping ipv6 -c 5 -a {source} {target}" - traceroute: StrictStr = "tracert ipv6 -q 2 -f 1 -a {source} {target}" - - -class _VPNIPv4(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = "display bgp vpnv4 vpn-instance {vrf} routing-table regular-expression {target}" - bgp_aspath: StrictStr = "display bgp vpnv4 vpn-instance {vrf} routing-table regular-expression {target}" - bgp_route: StrictStr = "display bgp vpnv4 vpn-instance {vrf} routing-table {target}" - ping: StrictStr = "ping -vpn-instance {vrf} -c 5 -a {source} {target}" - traceroute: StrictStr = "tracert -q 2 -f 1 -vpn-instance {vrf} -a {source} {target}" - - -class _VPNIPv6(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = "display bgp vpnv6 vpn-instance {vrf} routing-table regular-expression {target}" - bgp_aspath: StrictStr = "display bgp vpnv6 vpn-instance {vrf} routing-table regular-expression {target}" - bgp_route: StrictStr = "display bgp vpnv6 vpn-instance {vrf} routing-table {target}" - ping: StrictStr = "ping vpnv6 vpn-instance {vrf} -c 5 -a {source} {target}" - traceroute: StrictStr = "tracert -q 2 -f 1 vpn-instance {vrf} -a {source} {target}" - - -class HuaweiCommands(CommandGroup): - """Validation model for default huawei commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/commands/juniper.py b/hyperglass/models/commands/juniper.py deleted file mode 100644 index c0091ccd6..000000000 --- a/hyperglass/models/commands/juniper.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Juniper Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Validation model for non-default dual afi commands.""" - - bgp_route: StrictStr = 'show route protocol bgp table inet.0 {target} best detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"' - bgp_aspath: StrictStr = 'show route protocol bgp table inet.0 aspath-regex "{target}"' - bgp_community: StrictStr = "show route protocol bgp table inet.0 community {target}" - ping: StrictStr = "ping inet {target} count 5 source {source}" - traceroute: StrictStr = "traceroute inet {target} wait 1 source {source}" - - -class _IPv6(CommandSet): - """Validation model for non-default ipv4 commands.""" - - bgp_route: StrictStr = 'show route protocol bgp table inet6.0 {target} best detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"' - bgp_aspath: StrictStr = 'show route protocol bgp table inet6.0 aspath-regex "{target}"' - bgp_community: StrictStr = "show route protocol bgp table inet6.0 community {target}" - ping: StrictStr = "ping inet6 {target} count 5 source {source}" - traceroute: StrictStr = "traceroute inet6 {target} wait 2 source {source}" - - -class _VPNIPv4(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = 'show route protocol bgp table {vrf}.inet.0 {target} best detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"' - bgp_aspath: StrictStr = 'show route protocol bgp table {vrf}.inet.0 aspath-regex "{target}"' - bgp_community: StrictStr = "show route protocol bgp table {vrf}.inet.0 community {target}" - ping: StrictStr = "ping inet routing-instance {vrf} {target} count 5 source {source}" - traceroute: StrictStr = "traceroute inet routing-instance {vrf} {target} wait 1 source {source}" - - -class _VPNIPv6(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = 'show route protocol bgp table {vrf}.inet6.0 {target} best detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"' - bgp_aspath: StrictStr = 'show route protocol bgp table {vrf}.inet6.0 aspath-regex "{target}"' - bgp_community: StrictStr = "show route protocol bgp table {vrf}.inet6.0 community {target}" - ping: StrictStr = "ping inet6 routing-instance {vrf} {target} count 5 source {source}" - traceroute: StrictStr = "traceroute inet6 routing-instance {vrf} {target} wait 2 source {source}" - - -_structured = CommandGroup( - ipv4_default=CommandSet( - bgp_route="show route protocol bgp table inet.0 {target} best detail | display xml", - bgp_aspath='show route protocol bgp table inet.0 aspath-regex "{target}" detail | display xml', - bgp_community="show route protocol bgp table inet.0 community {target} detail | display xml", - ping="ping inet {target} count 5 source {source}", - traceroute="traceroute inet {target} wait 1 source {source}", - ), - ipv6_default=CommandSet( - bgp_route="show route protocol bgp table inet6.0 {target} best detail | display xml", - bgp_aspath='show route protocol bgp table inet6.0 aspath-regex "{target}" detail | display xml', - bgp_community="show route protocol bgp table inet6.0 community {target} detail | display xml", - ping="ping inet6 {target} count 5 source {source}", - traceroute="traceroute inet6 {target} wait 2 source {source}", - ), - ipv4_vpn=CommandSet( - bgp_route="show route protocol bgp table {vrf}.inet.0 {target} best detail | display xml", - bgp_aspath='show route protocol bgp table {vrf}.inet.0 aspath-regex "{target}" detail | display xml', - bgp_community="show route protocol bgp table {vrf}.inet.0 community {target} detail | display xml", - ping="ping inet routing-instance {vrf} {target} count 5 source {source}", - traceroute="traceroute inet routing-instance {vrf} {target} wait 1 source {source}", - ), - ipv6_vpn=CommandSet( - bgp_route="show route protocol bgp table {vrf}.inet6.0 {target} best detail | display xml", - bgp_aspath='show route protocol bgp table {vrf}.inet6.0 aspath-regex "{target}" detail | display xml', - bgp_community="show route protocol bgp table {vrf}.inet6.0 community {target} detail | display xml", - ping="ping inet6 routing-instance {vrf} {target} count 5 source {source}", - traceroute="traceroute inet6 routing-instance {vrf} {target} wait 2 source {source}", - ), -) - - -class JuniperCommands(CommandGroup): - """Validation model for default juniper commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() - - def __init__(self, **kwargs): - """Initialize command group, ensure structured fields are not overridden.""" - super().__init__(**kwargs) - self.structured = _structured diff --git a/hyperglass/models/commands/mikrotik_routeros.py b/hyperglass/models/commands/mikrotik_routeros.py deleted file mode 100644 index ab986f6f9..000000000 --- a/hyperglass/models/commands/mikrotik_routeros.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Mikrotik RouterOS Commands Model.""" -# Local -from ._mikrotik_base import MikrotikCommands - - -class MikrotikRouterOS(MikrotikCommands): - """Alias for mikrotik_routeros.""" - - pass diff --git a/hyperglass/models/commands/mikrotik_switchos.py b/hyperglass/models/commands/mikrotik_switchos.py deleted file mode 100644 index 4e18c3151..000000000 --- a/hyperglass/models/commands/mikrotik_switchos.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Mikrotik SwitchOS Commands Model.""" -# Local -from ._mikrotik_base import MikrotikCommands - - -class MikrotikSwitchOS(MikrotikCommands): - """Alias for mikrotik_switchos.""" - - pass diff --git a/hyperglass/models/commands/nokia_sros.py b/hyperglass/models/commands/nokia_sros.py deleted file mode 100644 index bc8d5b67c..000000000 --- a/hyperglass/models/commands/nokia_sros.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Nokia SR-OS Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Default commands for ipv4 commands.""" - - bgp_community: StrictStr = "/show router bgp routes community {target}" - bgp_aspath: StrictStr = "/show router bgp routes aspath-regex {target}" - bgp_route: StrictStr = "/show router bgp routes {target} ipv4 hunt" - ping: StrictStr = "/ping {target} source-address {source}" - traceroute: StrictStr = "/traceroute {target} source-address {source} wait 2 seconds" - - -class _IPv6(CommandSet): - """Default commands for ipv6 commands.""" - - bgp_community: StrictStr = "/show router bgp routes community {target}" - bgp_aspath: StrictStr = "/show router bgp routes aspath-regex {target}" - bgp_route: StrictStr = "/show router bgp routes {target} ipv6 hunt" - ping: StrictStr = "/ping {target} source-address {source}" - traceroute: StrictStr = "/traceroute {target} source-address {source} wait 2 seconds" - - -class _VPNIPv4(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = "/show router bgp routes community {target}" - bgp_aspath: StrictStr = "/show router bgp routes aspath-regex {target}" - bgp_route: StrictStr = "/show router bgp routes {target} vpn-ipv4 hunt" - ping: StrictStr = "/ping {target} source-address {source}" - traceroute: StrictStr = "/traceroute {target} source-address {source} wait 2 seconds" - - -class _VPNIPv6(CommandSet): - """Default commands for dual afi commands.""" - - bgp_community: StrictStr = "/show router bgp routes community {target}" - bgp_aspath: StrictStr = "/show router bgp routes aspath-regex {target}" - bgp_route: StrictStr = "/show router bgp routes {target} vpn-ipv6 hunt" - ping: StrictStr = "/ping {target} source-address {source}" - traceroute: StrictStr = "/traceroute {target} source-address {source} wait 2 seconds" - - -class NokiaSROSCommands(CommandGroup): - """Validation model for default nokia_sros commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/commands/tnsr.py b/hyperglass/models/commands/tnsr.py deleted file mode 100644 index 755a79dfa..000000000 --- a/hyperglass/models/commands/tnsr.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Netgate TNSR Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Validation model for default VRF IPv4 commands.""" - - bgp_community: StrictStr = 'dataplane shell sudo vtysh -c "show bgp ipv4 unicast community {target}"' - bgp_aspath: StrictStr = 'dataplane shell sudo vtysh -c "show bgp ipv4 unicast regexp {target}"' - bgp_route: StrictStr = 'dataplane shell sudo vtysh -c "show bgp ipv4 unicast {target}"' - ping: StrictStr = "ping {target} ipv4 source {source} count 5 timeout 1" - traceroute: StrictStr = "traceroute {target} ipv4 source {source} timeout 1 waittime 1" - - -class _IPv6(CommandSet): - """Validation model for default VRF IPv6 commands.""" - - bgp_community: StrictStr = 'dataplane shell sudo vtysh -c "show bgp ipv6 unicast community {target}"' - bgp_aspath: StrictStr = 'dataplane shell sudo vtysh -c "show bgp ipv6 unicast regexp {target}"' - bgp_route: StrictStr = 'dataplane shell sudo vtysh -c "show bgp ipv6 unicast {target}"' - ping: StrictStr = "ping {target} ipv6 source {source} count 5 timeout 1" - traceroute: StrictStr = "traceroute {target} ipv6 source {source} timeout 1 waittime 1" - - -class _VPNIPv4(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_community: StrictStr = 'dataplane shell sudo vtysh -c "show bgp vrf {vrf} ipv4 unicast community {target}"' - bgp_aspath: StrictStr = 'dataplane shell sudo vtysh -c "show bgp vrf {vrf} ipv4 unicast regexp {target}"' - bgp_route: StrictStr = 'dataplane shell sudo vtysh -c "show bgp vrf {vrf} ipv4 unicast {target}"' - ping: StrictStr = "dataplane shell ping -4 -c 5 -W 1 -I {vrf} -S {source} {target}" - traceroute: StrictStr = "dataplane shell traceroute -4 -w 1 -q 1 -i {vrf} -s {source} {target}" - - -class _VPNIPv6(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_community: StrictStr = 'dataplane shell sudo vtysh -c "show bgp vrf {vrf} ipv6 unicast community {target}"' - bgp_aspath: StrictStr = 'dataplane shell sudo vtysh -c "show bgp vrf {vrf} ipv6 unicast regexp {target}"' - bgp_route: StrictStr = 'dataplane shell sudo vtysh -c "show bgp vrf {vrf} ipv6 unicast {target}"' - ping: StrictStr = "dataplane shell ping -6 -c 5 -W 1 -I {vrf} -S {source} {target}" - traceroute: StrictStr = "dataplane shell traceroute -6 -w 1 -q 1 -i {vrf} -s {source} {target}" - - -class TNSRCommands(CommandGroup): - """Validation model for default tnsr commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/commands/vyos.py b/hyperglass/models/commands/vyos.py deleted file mode 100644 index fff5dd924..000000000 --- a/hyperglass/models/commands/vyos.py +++ /dev/null @@ -1,56 +0,0 @@ -"""VyOS Command Model.""" - -# Third Party -from pydantic import StrictStr - -# Local -from .common import CommandSet, CommandGroup - - -class _IPv4(CommandSet): - """Validation model for non-default dual afi commands.""" - - bgp_route: StrictStr = "show ip bgp {target}" - bgp_aspath: StrictStr = 'show ip bgp regexp "{target}"' - bgp_community: StrictStr = "show ip bgp community {target}" - ping: StrictStr = "ping {target} count 5 interface {source}" - traceroute: StrictStr = "mtr -4 -G 1 -c 1 -w -o SAL -a {source} {target}" - - -class _IPv6(CommandSet): - """Validation model for non-default ipv4 commands.""" - - bgp_route: StrictStr = "show ipv6 bgp {target}" - bgp_aspath: StrictStr = 'show ipv6 bgp regexp "{target}"' - bgp_community: StrictStr = "show ipv6 bgp community {target}" - ping: StrictStr = "ping {target} count 5 interface {source}" - traceroute: StrictStr = "mtr -6 -G 1 -c 1 -w -o SAL -a {source} {target}" - - -class _VPNIPv4(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = "show ip bgp {target}" - bgp_aspath: StrictStr = 'show ip bgp regexp "{target}"' - bgp_community: StrictStr = "show ip bgp community {target}" - ping: StrictStr = "ping {target} count 5 vrf {vrf} interface {source}" - traceroute: StrictStr = "ip vrf exec {vrf} mtr -4 -G 1 -c 1 -w -o SAL -a {source} {target}" - - -class _VPNIPv6(CommandSet): - """Validation model for non-default ipv6 commands.""" - - bgp_route: StrictStr = "show ipv6 bgp {target}" - bgp_aspath: StrictStr = 'show ipv6 bgp regexp "{target}"' - bgp_community: StrictStr = "show ipv6 bgp community {target}" - ping: StrictStr = "ping {target} count 5 interface {source}" - traceroute: StrictStr = "ip vrf exec {vrf} mtr -6 -G 1 -c 1 -w -o SAL -a {source} {target}" - - -class VyosCommands(CommandGroup): - """Validation model for default juniper commands.""" - - ipv4_default: _IPv4 = _IPv4() - ipv6_default: _IPv6 = _IPv6() - ipv4_vpn: _VPNIPv4 = _VPNIPv4() - ipv6_vpn: _VPNIPv6 = _VPNIPv6() diff --git a/hyperglass/models/config/_utils.py b/hyperglass/models/config/_utils.py deleted file mode 100644 index ccd6505cd..000000000 --- a/hyperglass/models/config/_utils.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Utility Functions for Pydantic Models.""" - -# Standard Library -import os -from pathlib import Path - -# Project -from hyperglass.log import log - - -def validate_image(value): - """Convert file path to URL path. - - Arguments: - value {FilePath} -- Path to logo file. - - Returns: - {str} -- Formatted logo path - """ - config_path = Path(os.environ["hyperglass_directory"]) - base_path = [v for v in value.split("/") if v != ""] - - if base_path[0] not in ("images", "custom"): - raise ValueError( - f"Logo files must be in the 'custom/' directory of your hyperglass directory. Got: {value}" - ) - - if base_path[0] == "custom": - file = config_path / "static" / "custom" / "/".join(base_path[1:]) - - else: - file = config_path / "static" / "images" / "/".join(base_path[1:]) - - log.error(file) - if not file.exists(): - raise ValueError(f"'{str(file)}' does not exist") - - base_index = file.parts.index(base_path[0]) - - return "/".join(file.parts[base_index:]) diff --git a/hyperglass/models/config/cache.py b/hyperglass/models/config/cache.py index ca4499ca5..f3c3e93e2 100644 --- a/hyperglass/models/config/cache.py +++ b/hyperglass/models/config/cache.py @@ -1,37 +1,11 @@ -"""Validation model for Redis cache config.""" - -# Standard Library -from typing import Union, Optional - -# Third Party -from pydantic import SecretStr, StrictInt, StrictStr, StrictBool, IPvAnyAddress +"""Validation model for cache config.""" # Local from ..main import HyperglassModel class Cache(HyperglassModel): - """Validation model for params.cache.""" - - host: Union[IPvAnyAddress, StrictStr] = "localhost" - port: StrictInt = 6379 - database: StrictInt = 1 - password: Optional[SecretStr] - timeout: StrictInt = 120 - show_text: StrictBool = True - - class Config: - """Pydantic model configuration.""" + """Public cache parameters.""" - title = "Cache" - description = "Redis server & cache timeout configuration." - fields = { - "host": {"description": "Redis server IP address or hostname."}, - "port": {"description": "Redis server TCP port."}, - "database": {"description": "Redis server database ID."}, - "password": {"description": "Redis authentication password."}, - "timeout": { - "description": "Time in seconds query output will be kept in the Redis cache." - }, - "show_test": {description: "Show the cache text in the hyperglass UI."}, - } + timeout: int = 120 + show_text: bool = True diff --git a/hyperglass/models/config/credential.py b/hyperglass/models/config/credential.py index 364fc4e51..905f22e73 100644 --- a/hyperglass/models/config/credential.py +++ b/hyperglass/models/config/credential.py @@ -1,34 +1,35 @@ """Validate credential configuration variables.""" # Standard Library -from typing import Optional +import typing as t # Third Party -from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator +from pydantic import FilePath, SecretStr, model_validator # Local -from ..main import HyperglassModelExtra +from ..main import HyperglassModel -Methods = constr(regex=r"(password|unencrypted_key|encrypted_key)") +AuthMethod = t.Literal["password", "unencrypted_key", "encrypted_key"] -class Credential(HyperglassModelExtra): +class Credential(HyperglassModel, extra="allow"): """Model for per-credential config in devices.yaml.""" - username: StrictStr - password: Optional[SecretStr] - key: Optional[FilePath] + username: str + password: t.Optional[SecretStr] = None + key: t.Optional[FilePath] = None + _method: t.Optional[AuthMethod] = None - @root_validator - def validate_credential(cls, values): + @model_validator(mode="after") + def validate_credential(cls, data: "Credential"): """Ensure either a password or an SSH key is set.""" - if values["key"] is None and values["password"] is None: + if data.key is None and data.password is None: raise ValueError( "Either a password or an SSH key must be specified for user '{}'".format( - values["username"] + data.username ) ) - return values + return data def __init__(self, **kwargs): """Set private attribute _method based on validated model.""" diff --git a/hyperglass/models/config/devices.py b/hyperglass/models/config/devices.py index 51ff1d33c..70cac21ee 100644 --- a/hyperglass/models/config/devices.py +++ b/hyperglass/models/config/devices.py @@ -1,384 +1,371 @@ """Validate router configuration variables.""" # Standard Library -import os import re -from typing import Any, Dict, List, Tuple, Union, Optional +import typing as t from pathlib import Path from ipaddress import IPv4Address, IPv6Address # Third Party -from pydantic import ( - StrictInt, - StrictStr, - StrictBool, - PrivateAttr, - validator, - root_validator, -) +from pydantic import FilePath, ValidationInfo, field_validator +from netmiko.ssh_dispatcher import CLASS_MAPPER # type: ignore # Project from hyperglass.log import log -from hyperglass.util import get_driver, validate_nos, resolve_hostname -from hyperglass.constants import SCRAPE_HELPERS, SUPPORTED_STRUCTURED_OUTPUT -from hyperglass.exceptions import ConfigError, UnsupportedDevice +from hyperglass.util import get_driver, get_fmt_keys, resolve_hostname +from hyperglass.state import use_state +from hyperglass.settings import Settings +from hyperglass.constants import ( + DRIVER_MAP, + SCRAPE_HELPERS, + LINUX_PLATFORMS, + SUPPORTED_STRUCTURED_OUTPUT, +) +from hyperglass.exceptions.private import ConfigError, UnsupportedDevice # Local -from .ssl import Ssl -from .vrf import Vrf, Info -from ..main import HyperglassModel, HyperglassModelExtra +from ..main import MultiModel, HyperglassModel, HyperglassModelWithId +from ..util import check_legacy_fields from .proxy import Proxy from ..fields import SupportedDriver -from .network import Network +from ..directive import Directives from .credential import Credential +from .http_client import HttpConfiguration -_default_vrf = { - "name": "default", - "display_name": "Global", - "info": Info(), - "ipv4": { - "source_address": None, - "access_list": [ - {"network": "0.0.0.0/0", "action": "permit", "ge": 0, "le": 32} - ], - }, - "ipv6": { - "source_address": None, - "access_list": [{"network": "::/0", "action": "permit", "ge": 0, "le": 128}], - }, -} +ALL_DEVICE_TYPES = {*DRIVER_MAP.keys(), *CLASS_MAPPER.keys()} -def find_device_id(values: Dict) -> Tuple[str, Dict]: - """Generate device id & handle legacy display_name field.""" +class APIDevice(t.TypedDict): + """API Response Model for a device.""" - def generate_id(name: str) -> str: - scrubbed = re.sub(r"[^A-Za-z0-9\_\-\s]", "", name) - return "_".join(scrubbed.split()).lower() + id: str + name: str + group: t.Union[str, None] - name = values.pop("name", None) - if name is None: - raise ValueError("name is required.") +class DirectiveOptions(HyperglassModel, extra="ignore"): + """Per-device directive options.""" - legacy_display_name = values.pop("display_name", None) + builtins: t.Union[bool, t.List[str]] = True - if legacy_display_name is not None: - log.warning( - "The 'display_name' field is deprecated. Use the 'name' field instead." - ) - device_id = generate_id(legacy_display_name) - display_name = legacy_display_name - else: - device_id = generate_id(name) - display_name = name - return device_id, {"name": display_name, "display_name": None, **values} - - -class Device(HyperglassModel): +class Device(HyperglassModelWithId, extra="allow"): """Validation model for per-router config in devices.yaml.""" - _id: StrictStr = PrivateAttr() - name: StrictStr - address: Union[IPv4Address, IPv6Address, StrictStr] - network: Network + id: str + name: str + description: t.Optional[str] = None + avatar: t.Optional[FilePath] = None + address: t.Union[IPv4Address, IPv6Address, str] + group: t.Optional[str] = None credential: Credential - proxy: Optional[Proxy] - display_name: Optional[StrictStr] - port: StrictInt = 22 - ssl: Optional[Ssl] - nos: StrictStr - commands: Optional[StrictStr] - vrfs: List[Vrf] = [_default_vrf] - display_vrfs: List[StrictStr] = [] - vrf_names: List[StrictStr] = [] - structured_output: Optional[StrictBool] - driver: Optional[SupportedDriver] - - def __init__(self, **kwargs) -> None: - """Set the device ID.""" - _id, values = find_device_id(kwargs) - super().__init__(**values) - self._id = _id - - def __hash__(self) -> int: - """Make device object hashable so the object can be deduplicated with set().""" - return hash((self.name,)) - - def __eq__(self, other: Any) -> bool: - """Make device object comparable so the object can be deduplicated with set().""" - result = False - - if isinstance(other, HyperglassModel): - result = self.name == other.name - - return result + proxy: t.Optional[Proxy] = None + display_name: t.Optional[str] = None + port: int = 22 + http: HttpConfiguration = HttpConfiguration() + platform: str + structured_output: t.Optional[bool] = None + directives: Directives = Directives() + driver: t.Optional[SupportedDriver] = None + driver_config: t.Dict[str, t.Any] = {} + attrs: t.Dict[str, str] = {} + + def __init__(self, **kw) -> None: + """Check legacy fields and ensure an `id` is set.""" + kw = check_legacy_fields(model="Device", data=kw) + if "id" not in kw: + kw = self._with_id(kw) + super().__init__(**kw) + self._validate_directive_attrs() @property def _target(self): return str(self.address) - @validator("address") - def validate_address(cls, value, values): - """Ensure a hostname is resolvable.""" - if not isinstance(value, (IPv4Address, IPv6Address)): - if not any(resolve_hostname(value)): - raise ConfigError( - "Device '{d}' has an address of '{a}', which is not resolvable.", - d=values["name"], - a=value, - ) - return value - - @validator("structured_output", pre=True, always=True) - def validate_structured_output(cls, value: bool, values: Dict) -> bool: - """Validate structured output is supported on the device & set a default.""" - - if value is True and values["nos"] not in SUPPORTED_STRUCTURED_OUTPUT: - raise ConfigError( - "The 'structured_output' field is set to 'true' on device '{d}' with " - + "NOS '{n}', which does not support structured output", - d=values["name"], - n=values["nos"], - ) - - elif value is None and values["nos"] in SUPPORTED_STRUCTURED_OUTPUT: - value = True - else: - value = False - - return value - - @validator("ssl") - def validate_ssl(cls, value, values): - """Set default cert file location if undefined. - - Arguments: - value {object} -- SSL object - values {dict} -- Other already-validated fields - - Returns: - {object} -- SSL configuration - """ - if value is not None: - if value.enable and value.cert is None: - app_path = Path(os.environ["hyperglass_directory"]) - cert_file = app_path / "certs" / f'{values["name"]}.pem' - if not cert_file.exists(): - log.warning("No certificate found for device {d}", d=values["name"]) - cert_file.touch() - value.cert = cert_file - return value - - @root_validator(pre=True) - def validate_nos_commands(cls, values: "Device") -> "Device": - """Validate & rewrite NOS, set default commands.""" - - nos = values.get("nos", "") - if not nos: - # Ensure nos is defined. - raise ValueError( - f'Device {values["name"]} is missing a `nos` (Network Operating System).' - ) - - if nos in SCRAPE_HELPERS.keys(): - # Rewrite NOS to helper value if needed. - nos = SCRAPE_HELPERS[nos] - - # Verify NOS is supported by hyperglass. - supported, _ = validate_nos(nos) - if not supported: - raise UnsupportedDevice('"{nos}" is not supported.', nos=nos) + @staticmethod + def _with_id(values: t.Dict) -> str: + """Generate device id & handle legacy display_name field.""" - values["nos"] = nos + def generate_id(name: str) -> str: + scrubbed = re.sub(r"[^A-Za-z0-9\_\-\s]", "", name) + return "_".join(scrubbed.split()).lower() - commands = values.get("commands") + name = values.pop("name", None) - if commands is None: - # If no commands are defined, set commands to the NOS. - inferred = values["nos"] + if name is None: + raise ValueError("name is required.") - # If the _telnet prefix is added, remove it from the command - # profile so the commands are the same regardless of - # protocol. - if "_telnet" in inferred: - inferred = inferred.replace("_telnet", "") - - values["commands"] = inferred - - return values + device_id = generate_id(name) + display_name = name - @validator("vrfs", pre=True) - def validate_vrfs(cls, value, values): - """Validate VRF definitions. + return {"id": device_id, "name": display_name, "display_name": None, **values} - - Ensures source IP addresses are set for the default VRF - (global routing table). - - Initializes the default VRF with the DefaultVRF() class so - that specific defaults can be set for the global routing - table. - - If the 'display_name' is not set for a non-default VRF, try - to make one that looks pretty based on the 'name'. + def export_api(self) -> APIDevice: + """Export API-facing device fields.""" + return { + "id": self.id, + "name": self.name, + "group": self.group, + } - Arguments: - value {list} -- List of VRFs - values {dict} -- Other already-validated fields + @property + def directive_commands(self) -> t.List[str]: + """Get all commands associated with the device.""" + return [ + command + for directive in self.directives + for rule in directive.rules + for command in rule.commands + ] - Raises: - ConfigError: Raised if the VRF is missing a source address + @property + def directive_ids(self) -> t.List[str]: + """Get all directive IDs associated with the device.""" + return [directive.id for directive in self.directives] - Returns: - {list} -- List of valid VRFs + @property + def directive_names(self) -> t.List[str]: + """Get all directive names associated with the device.""" + return list({directive.name for directive in self.directives}) + + def has_directives(self, *directive_ids: str) -> bool: + """Determine if a directive is used on this device.""" + for directive_id in directive_ids: + if directive_id in self.directive_ids: + return True + return False + + def get_device_type(self) -> str: + """Get the `device_type` field for use by Netmiko. + + In some cases, the platform might be different than the + device_type. For example, any linux-based platform like FRR, + BIRD, or OpenBGPD will have directives associated with those + platforms, but the `device_type` sent to Netmiko needs to be + `linux_ssh`. """ - vrfs = [] - for vrf in value: - vrf_default = vrf.get("default", False) - - for afi in ("ipv4", "ipv6"): - vrf_afi = vrf.get(afi) - - # If AFI is actually defined (enabled), and if the - # source_address field is not set, raise an error - if vrf_afi is not None and vrf_afi.get("source_address") is None: - raise ConfigError( - ( - "VRF '{vrf}' in router '{router}' is missing a source " - "{afi} address." - ), - vrf=vrf.get("name"), - router=values.get("name"), - afi=afi.replace("ip", "IP"), - ) - - # If no display_name is set for a non-default VRF, try - # to make one by replacing non-alphanumeric characters - # with whitespaces and using str.title() to make each - # word look "pretty". - if not vrf_default and not isinstance(vrf.get("display_name"), str): - new_name = vrf["name"] - new_name = re.sub(r"[^a-zA-Z0-9]", " ", new_name) - new_name = re.split(" ", new_name) - vrf["display_name"] = " ".join([w.title() for w in new_name]) - - log.debug( - f'Field "display_name" for VRF "{vrf["name"]}" was not set. ' - f"Generated '{vrf['display_name']}'" + if self.platform in LINUX_PLATFORMS: + return "linux_ssh" + return self.platform + + def _validate_directive_attrs(self) -> None: + # Set of all keys except for built-in key `target`. + keys = { + key + for group in [get_fmt_keys(command) for command in self.directive_commands] + for key in group + if key != "target" + } + + attrs = {k: v for k, v in self.attrs.items() if k in keys} + + # Verify all keys in associated commands contain values in device's `attrs`. + for key in keys: + if key not in attrs: + raise ConfigError( + "Device '{d}' has a command that references attribute '{a}', but '{a}' is missing from device attributes", + d=self.name, + a=key, ) - elif vrf_default and vrf.get("display_name") is None: - vrf["display_name"] = "Global" + @field_validator("address") + def validate_address( + cls, value: t.Union[IPv4Address, IPv6Address, str], info: ValidationInfo + ) -> t.Union[IPv4Address, IPv6Address, str]: + """Ensure a hostname is resolvable.""" - # Validate the non-default VRF against the standard - # Vrf() class. - vrf = Vrf(**vrf) + if not isinstance(value, (IPv4Address, IPv6Address)): + if not any(resolve_hostname(value)): + raise ConfigError( + "Device '{d}' has an address of '{a}', which is not resolvable.", + d=info.data["name"], + a=value, + ) + return value - vrfs.append(vrf) - return vrfs + @field_validator("avatar") + def validate_avatar( + cls, value: t.Union[FilePath, None], info: ValidationInfo + ) -> t.Union[FilePath, None]: + """Migrate avatar to static directory.""" + if value is not None: + # Standard Library + import shutil + + # Third Party + from PIL import Image + + target = Settings.static_path / "images" / value.name + copied = shutil.copy2(value, target) + log.bind( + device=info.data["name"], + source=str(value), + destination=str(target), + ).debug("Copied device avatar") + + with Image.open(copied) as src: + if src.width > 512: + src.thumbnail((512, 512 * src.height / src.width)) + src.save(target) + return value - @validator("driver") - def validate_driver(cls, value: Optional[str], values: Dict) -> Dict: - """Set the correct driver and override if supported.""" - return get_driver(values["nos"], value) + @field_validator("platform", mode="before") + def validate_platform(cls: "Device", value: t.Any, info: ValidationInfo) -> str: + """Validate & rewrite device platform, set default `directives`.""" + if value == "http": + if info.data.get("http") is None: + raise ConfigError( + "Device '{device}' has platform 'http' configured, but no http parameters are defined.", + device=info.data["name"], + ) -class Devices(HyperglassModelExtra): - """Validation model for device configurations.""" + if value is None: + if info.data.get("http") is not None: + value = "http" + else: + # Ensure device platform is defined. + raise ConfigError( + "Device '{device}' is missing a 'platform' (Network Operating System) property", + device=info.data["name"], + ) - _ids: List[StrictStr] = [] - hostnames: List[StrictStr] = [] - vrfs: List[StrictStr] = [] - display_vrfs: List[StrictStr] = [] - vrf_objects: List[Vrf] = [] - objects: List[Device] = [] - all_nos: List[StrictStr] = [] - default_vrf: Vrf = Vrf(name="default", display_name="Global") + if value in SCRAPE_HELPERS.keys(): + # Rewrite platform to helper value if needed. + value = SCRAPE_HELPERS[value] - def __init__(self, input_params: List[Dict]) -> None: - """Import loaded YAML, initialize per-network definitions. + # Verify device platform is supported by hyperglass. + if value not in ALL_DEVICE_TYPES: + raise UnsupportedDevice(value) - Remove unsupported characters from device names, dynamically - set attributes for the devices class. Builds lists of common - attributes for easy access in other modules. + return value - Arguments: - input_params {dict} -- Unvalidated router definitions + @field_validator("structured_output", mode="before") + def validate_structured_output(cls, value: bool, info: ValidationInfo) -> bool: + """Validate structured output is supported on the device & set a default.""" - Returns: - {object} -- Validated routers object - """ - vrfs = set() - display_vrfs = set() - vrf_objects = set() - all_nos = set() - objects = set() - hostnames = set() - _ids = set() - - init_kwargs = {} - - for definition in input_params: - # Validate each router config against Router() model/schema - device = Device(**definition) - - # Add router-level attributes (assumed to be unique) to - # class lists, e.g. so all hostnames can be accessed as a - # list with `devices.hostnames`, same for all router - # classes, for when iteration over all routers is required. - hostnames.add(device.name) - _ids.add(device._id) - objects.add(device) - all_nos.add(device.commands) - - for vrf in device.vrfs: - - # For each configured router VRF, add its name and - # display_name to a class set (for automatic de-duping). - vrfs.add(vrf.name) - display_vrfs.add(vrf.display_name) - - # Also add the names to a router-level list so each - # router's VRFs and display VRFs can be easily accessed. - device.display_vrfs.append(vrf.display_name) - device.vrf_names.append(vrf.name) - - # Add a 'default_vrf' attribute to the devices class - # which contains the configured default VRF display name. - if vrf.name == "default" and not hasattr(self, "default_vrf"): - init_kwargs["default_vrf"] = Vrf( - name=vrf.name, display_name=vrf.display_name - ) - - # Add the native VRF objects to a set (for automatic - # de-duping), but exlcude device-specific fields. - vrf_objects.add( - vrf.copy( - deep=True, - exclude={ - "ipv4": {"source_address"}, - "ipv6": {"source_address"}, - }, - ) + if value is True: + if info.data.get("platform") not in SUPPORTED_STRUCTURED_OUTPUT: + raise ConfigError( + "The 'structured_output' field is set to 'true' on device '{}' with " + + "platform '{}', which does not support structured output", + info.data.get("name"), + info.data.get("platform"), ) + return value + if value is None and info.data.get("platform") in SUPPORTED_STRUCTURED_OUTPUT: + value = True + else: + value = False + return value + + @field_validator("directives", mode="before") + def validate_directives( + cls: "Device", value: t.Optional[t.List[str]], info: ValidationInfo + ) -> "Directives": + """Associate directive IDs to loaded directive objects.""" + directives = use_state("directives") + + directive_ids = value or [] + structured_output = info.data.get("structured_output", False) + platform = info.data.get("platform") + + # Directive options + directive_options = DirectiveOptions( + **{ + k: v + for statement in directive_ids + if isinstance(statement, t.Dict) + for k, v in statement.items() + } + ) - # Convert the de-duplicated sets to a standard list, add lists - # as class attributes. Sort router list by router name attribute - init_kwargs["_ids"] = list(_ids) - init_kwargs["hostnames"] = list(hostnames) - init_kwargs["all_nos"] = list(all_nos) - init_kwargs["vrfs"] = list(vrfs) - init_kwargs["display_vrfs"] = list(vrfs) - init_kwargs["vrf_objects"] = list(vrf_objects) - init_kwargs["objects"] = sorted(objects, key=lambda x: x.name) - - super().__init__(**init_kwargs) - - def __getitem__(self, accessor: str) -> Device: - """Get a device by its name.""" - for device in self.objects: - if device._id == accessor: - return device - elif device.name == accessor: - return device - - raise AttributeError(f"No device named '{accessor}'") + # String directive IDs, excluding builtins and options. + directive_ids = [ + statement + for statement in directive_ids + if isinstance(statement, str) and not statement.startswith("__") + ] + # Directives matching provided IDs. + device_directives = directives.filter(*directive_ids) + # Matching built-in directives for this device's platform. + builtins = directives.device_builtins(platform=platform, table_output=structured_output) + + if directive_options.builtins is True: + # Add all builtins. + device_directives += builtins + elif isinstance(directive_options.builtins, t.List): + # If the user provides a list of builtin directives to include, add only those. + device_directives += builtins.matching(*directive_options.builtins) + + return device_directives + + @field_validator("driver") + def validate_driver(cls: "Device", value: t.Optional[str], info: ValidationInfo) -> str: + """Set the correct driver and override if supported.""" + return get_driver(info.data.get("platform"), value) + + +class Devices(MultiModel, model=Device, unique_by="id"): + """Container for all devices.""" + + def __init__(self: "Devices", *items: t.Dict[str, t.Any]) -> None: + """Generate IDs prior to validation.""" + with_id = (Device._with_id(item) for item in items) + super().__init__(*with_id) + + def export_api(self: "Devices") -> t.List[APIDevice]: + """Export API-facing device fields.""" + return [d.export_api() for d in self] + + def valid_id_or_name(self: "Devices", value: str) -> bool: + """Determine if a value is a valid device name or ID.""" + for device in self: + if value == device.id or value == device.name: + return True + return False + + def directive_plugins(self: "Devices") -> t.Dict[Path, t.Tuple[str]]: + """Get a mapping of plugin paths to associated directive IDs.""" + result: t.Dict[Path, t.Set[str]] = {} + # Unique set of all directives. + directives = {directive for device in self for directive in device.directives} + # Unique set of all plugin file names. + plugin_names = {plugin for directive in directives for plugin in directive.plugins} + + for directive in directives: + # Convert each plugin file name to a `Path` object. + for plugin in (Path(p) for p in directive.plugins if p in plugin_names): + if plugin not in result: + result[plugin] = set() + result[plugin].add(directive.id) + # Convert the directive set to a tuple. + return {k: tuple(v) for k, v in result.items()} + + def directive_names(self) -> t.List[str]: + """Get all directive names for all devices.""" + return list({directive.name for device in self for directive in device.directives}) + + def frontend(self: "Devices") -> t.List[t.Dict[str, t.Any]]: + """Export grouped devices for UIParameters.""" + groups = {device.group for device in self} + return [ + { + "group": group, + "locations": [ + { + "group": group, + "id": device.id, + "name": device.name, + "avatar": f"/images/{device.avatar.name}" + if device.avatar is not None + else None, + "description": device.description, + "directives": [d.frontend() for d in device.directives], + } + for device in self + if device.group == group + ], + } + for group in groups + ] diff --git a/hyperglass/models/config/docs.py b/hyperglass/models/config/docs.py index abb288396..d219a75aa 100644 --- a/hyperglass/models/config/docs.py +++ b/hyperglass/models/config/docs.py @@ -1,28 +1,32 @@ """Configuration for API docs feature.""" + +# Standard Library +import typing as t + # Third Party -from pydantic import Field, HttpUrl, StrictStr, StrictBool, constr +from pydantic import Field, HttpUrl # Local from ..main import HyperglassModel from ..fields import AnyUri -DocsMode = constr(regex=r"(swagger|redoc)") +DocsMode = t.Literal["swagger", "redoc"] class EndpointConfig(HyperglassModel): """Validation model for per API endpoint documentation.""" - title: StrictStr = Field( + title: str = Field( ..., title="Endpoint Title", description="Displayed as the header text above the API endpoint section.", ) - description: StrictStr = Field( + description: str = Field( ..., title="Endpoint Description", description="Displayed inside each API endpoint section.", ) - summary: StrictStr = Field( + summary: str = Field( ..., title="Endpoint Summary", description="Displayed beside the API endpoint URI.", @@ -32,35 +36,23 @@ class EndpointConfig(HyperglassModel): class Docs(HyperglassModel): """Validation model for params.docs.""" - enable: StrictBool = Field( - True, title="Enable", description="Enable or disable API documentation." - ) - mode: DocsMode = Field( - "redoc", - title="Docs Mode", - description="OpenAPI UI library to use for the hyperglass API docs. Currently, the options are [Swagger UI](/fixme) and [Redoc](/fixme).", - ) + enable: bool = Field(True, title="Enable", description="Enable or disable API documentation.") base_url: HttpUrl = Field( "https://lg.example.net", title="Base URL", description="Base URL used in request samples.", ) - uri: AnyUri = Field( + path: AnyUri = Field( "/api/docs", title="URI", description="HTTP URI/path where API documentation can be accessed.", ) - openapi_uri: AnyUri = Field( - "/openapi.json", - title="OpenAPI URI", - description="Path to the automatically generated `openapi.json` file.", - ) - title: StrictStr = Field( + title: str = Field( "{site_title} API Documentation", title="Title", description="API documentation title. `{site_title}` may be used to display the `site_title` parameter.", ) - description: StrictStr = Field( + description: str = Field( "", title="Description", description="API documentation description appearing below the title.", @@ -80,37 +72,8 @@ class Docs(HyperglassModel): description="List of supported query types.", summary="Query Types", ) - communities: EndpointConfig = EndpointConfig( - title="BGP Communities", - description="List of BGP communities.", - summary="BGP Communities List", - ) info: EndpointConfig = EndpointConfig( title="System Information", description="General information about this looking glass.", summary="System Information", ) - - class Config: - """Pydantic model configuration.""" - - title = "API Docs" - description = "API documentation configuration parameters" - fields = { - "query": { - "title": "Query API Endpoint", - "description": "`/api/query/` API documentation options.", - }, - "devices": { - "title": "Devices API Endpoint", - "description": "`/api/devices` API documentation options.", - }, - "queries": { - "title": "Queries API Endpoint", - "description": "`/api/devices` API documentation options.", - }, - "communities": { - "title": "BGP Communities API Endpoint", - "description": "`/api/communities` API documentation options.", - }, - } diff --git a/hyperglass/models/config/http_client.py b/hyperglass/models/config/http_client.py new file mode 100644 index 000000000..a2f2ff160 --- /dev/null +++ b/hyperglass/models/config/http_client.py @@ -0,0 +1,130 @@ +"""Configuration models for hyperglass http client.""" + +# Standard Library +import typing as t + +# Third Party +import httpx +from pydantic import FilePath, SecretStr, PrivateAttr, IPvAnyAddress + +# Project +from hyperglass.models import HyperglassModel +from hyperglass.constants import __version__ + +# Local +from ..fields import IntFloat, HttpMethod, Primitives + +if t.TYPE_CHECKING: + # Local + from .devices import Device + +DEFAULT_QUERY_PARAMETERS: t.Dict[str, str] = { + "query_target": "{query_target}", + "query_type": "{query_type}", + "query_location": "{query_location}", +} + +BodyFormat = t.Literal["json", "yaml", "xml", "text"] +Scheme = t.Literal["http", "https"] + + +class AttributeMapConfig(HyperglassModel): + """Allow the user to 'rewrite' hyperglass field names to their own values.""" + + query_target: t.Optional[str] = None + query_type: t.Optional[str] = None + query_location: t.Optional[str] = None + + +class AttributeMap(HyperglassModel): + """Merged implementation of attribute map configuration.""" + + query_target: str + query_type: str + query_location: str + + +class HttpBasicAuth(HyperglassModel): + """Configuration model for HTTP basic authentication.""" + + username: str + password: SecretStr + + +class HttpConfiguration(HyperglassModel): + """HTTP client configuration.""" + + _attribute_map: AttributeMap = PrivateAttr() + path: str = "/" + method: HttpMethod = "GET" + scheme: Scheme = "https" + query: t.Optional[t.Union[t.Literal[False], t.Dict[str, Primitives]]] = None + verify_ssl: bool = True + ssl_ca: t.Optional[FilePath] = None + ssl_client: t.Optional[FilePath] = None + source: t.Optional[IPvAnyAddress] = None + timeout: IntFloat = 5 + headers: t.Dict[str, str] = {} + follow_redirects: bool = False + basic_auth: t.Optional[HttpBasicAuth] = None + attribute_map: AttributeMapConfig = AttributeMapConfig() + body_format: BodyFormat = "json" + retries: int = 0 + + def __init__(self, **data: t.Any) -> None: + """Create HTTP Client Configuration Definition.""" + + super().__init__(**data) + self._attribute_map = self._create_attribute_map() + + def _create_attribute_map(self) -> AttributeMap: + """Create AttributeMap instance with defined overrides.""" + + return AttributeMap( + query_location=self.attribute_map.query_location or "query_location", + query_type=self.attribute_map.query_type or "query_type", + query_target=self.attribute_map.query_target or "query_target", + ) + + def create_client(self, *, device: "Device") -> httpx.AsyncClient: + """Create a pre-configured http client.""" + + # Use the CA certificates for SSL verification, if present. + verify = self.verify_ssl + if self.ssl_ca is not None: + verify = httpx.create_ssl_context(verify=str(self.ssl_ca)) + + transport_constructor = {"retries": self.retries} + + # Use `source` IP address as httpx transport's `local_address`, if defined. + if self.source is not None: + transport_constructor["local_address"] = str(self.source) + + transport = httpx.AsyncHTTPTransport(**transport_constructor) + + # Add the port to the URL only if it is not 22, 80, or 443. + base_url = f"{self.scheme}://{device.address!s}".strip("/") + if device.port not in (22, 80, 443): + base_url += f":{device.port!s}" + + parameters = { + "verify": verify, + "transport": transport, + "timeout": self.timeout, + "follow_redirects": self.follow_redirects, + "base_url": f"{self.scheme}://{device.address!s}".strip("/"), + "headers": {"user-agent": f"hyperglass/{__version__}", **self.headers}, + } + + # Use client certificate authentication, if defined. + if self.ssl_client is not None: + parameters["cert"] = str(self.ssl_client) + + # Use basic authentication, if defined. + if self.basic_auth is not None: + parameters["auth"] = httpx.BasicAuth( + username=self.basic_auth.username, + password=self.basic_auth.password.get_secret_value(), + ) + + return httpx.AsyncClient(**parameters) diff --git a/hyperglass/models/config/logging.py b/hyperglass/models/config/logging.py index db718ebc6..3604dd0a9 100644 --- a/hyperglass/models/config/logging.py +++ b/hyperglass/models/config/logging.py @@ -1,73 +1,58 @@ """Validate logging configuration.""" # Standard Library -import base64 -from ast import literal_eval -from typing import Dict, Union, Optional +import typing as t from pathlib import Path # Third Party -from pydantic import ( - ByteSize, - SecretStr, - StrictInt, - StrictStr, - AnyHttpUrl, - StrictBool, - StrictFloat, - DirectoryPath, - constr, - validator, -) +from pydantic import ByteSize, SecretStr, AnyHttpUrl, DirectoryPath, field_validator # Project from hyperglass.constants import __version__ # Local -from ..main import HyperglassModel, HyperglassModelExtra - -HttpAuthMode = constr(regex=r"(basic|api_key)") -HttpProvider = constr(regex=r"(msteams|slack|generic)") -LogFormat = constr(regex=r"(text|json)") +from ..main import HyperglassModel +from ..fields import LogFormat, HttpAuthMode, HttpProvider class Syslog(HyperglassModel): """Validation model for syslog configuration.""" - enable: StrictBool = True - host: StrictStr - port: StrictInt = 514 + enable: bool = True + host: str + port: int = 514 class HttpAuth(HyperglassModel): """HTTP hook authentication parameters.""" mode: HttpAuthMode = "basic" - username: Optional[StrictStr] + username: t.Optional[str] = None password: SecretStr + header: str = "x-api-key" - def api_key(self, header_name="X-API-Key"): + def api_key(self): """Represent authentication as an API key header.""" - return {header_name: self.password.get_secret_value()} + return {self.header: self.password.get_secret_value()} def basic(self): """Represent HTTP basic authentication.""" return (self.username, self.password.get_secret_value()) -class Http(HyperglassModelExtra): +class Http(HyperglassModel, extra="allow"): """HTTP logging parameters.""" - enable: StrictBool = True + enable: bool = True provider: HttpProvider = "generic" host: AnyHttpUrl - authentication: Optional[HttpAuth] - headers: Dict[StrictStr, Union[StrictStr, StrictInt, StrictBool, None]] = {} - params: Dict[StrictStr, Union[StrictStr, StrictInt, StrictBool, None]] = {} - verify_ssl: StrictBool = True - timeout: Union[StrictFloat, StrictInt] = 5.0 + authentication: t.Optional[HttpAuth] = None + headers: t.Dict[str, t.Union[str, int, bool, None]] = {} + params: t.Dict[str, t.Union[str, int, bool, None]] = {} + verify_ssl: bool = True + timeout: t.Union[float, int] = 5.0 - @validator("headers", "params") + @field_validator("headers", "params") def stringify_headers_params(cls, value): """Ensure headers and URL parameters are strings.""" for k, v in value.items(): @@ -92,12 +77,6 @@ def __init__(self, **kwargs): else: dumped["auth"] = self.authentication.basic() - self._obscured_params = base64.encodestring(str(dumped).encode()) - - def decoded(self): - """Decode connection details.""" - return literal_eval(base64.decodestring(self._obscured_params).decode()) - class Logging(HyperglassModel): """Validation model for logging configuration.""" @@ -105,5 +84,5 @@ class Logging(HyperglassModel): directory: DirectoryPath = Path("/tmp") # noqa: S108 format: LogFormat = "text" max_size: ByteSize = "50MB" - syslog: Optional[Syslog] - http: Optional[Http] + syslog: t.Optional[Syslog] = None + http: t.Optional[Http] = None diff --git a/hyperglass/models/config/messages.py b/hyperglass/models/config/messages.py index 86ab54618..cb47f0d8c 100644 --- a/hyperglass/models/config/messages.py +++ b/hyperglass/models/config/messages.py @@ -1,7 +1,7 @@ """Validate error message configuration variables.""" # Third Party -from pydantic import Field, StrictStr +from pydantic import Field, ConfigDict # Local from ..main import HyperglassModel @@ -10,84 +10,84 @@ class Messages(HyperglassModel): """Validation model for params.messages.""" - no_input: StrictStr = Field( + model_config = ConfigDict( + title="Messages", + description="Customize almost all user-facing UI & API messages.", + json_schema_extra={"level": 2}, + ) + + no_input: str = Field( "{field} must be specified.", title="No Input", description="Displayed when no a required field is not specified. `{field}` may be used to display the `display_name` of the field that was omitted.", ) - acl_denied: StrictStr = Field( - "{target} is a member of {denied_network}, which is not allowed.", - title="ACL - Denied", - description="Displayed when a query target is explicitly denied by a matched VRF's ACL entry. `{target}` and `{denied_network}` may be used to display the denied query target and the ACL entry that caused it to be denied.", - ) - acl_not_allowed: StrictStr = Field( + target_not_allowed: str = Field( "{target} is not allowed.", - title="ACL - Not Allowed", - description="Displayed when a query target is implicitly denied by a matched VRF's ACL. `{target}` may be used to display the denied query target.", + title="Target Not Allowed", + description="Displayed when a query target is implicitly denied by a configured rule. `{target}` will be used to display the denied query target.", ) - feature_not_enabled: StrictStr = Field( + feature_not_enabled: str = Field( "{feature} is not enabled.", title="Feature Not Enabled", description="Displayed when a query type is submitted that is not supported or disabled. The hyperglass UI performs validation of supported query types prior to submitting any requests, so this is primarily relevant to the hyperglass API. `{feature}` may be used to display the disabled feature.", ) - invalid_input: StrictStr = Field( - "{target} is not a valid {query_type} target.", + invalid_input: str = Field( + "{target} is not valid.", title="Invalid Input", - description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` and `{query_type}` maybe used to display the invalid target and corresponding query type.", + description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` may be used to display the invalid target.", + ) + invalid_query: str = Field( + "{target} is not a valid {query_type} target.", + title="Invalid Query", + description="Displayed when a query target's value is invalid in relation to the corresponding query type. `{target}` and `{query_type}` may be used to display the invalid target and corresponding query type.", ) - invalid_field: StrictStr = Field( + invalid_field: str = Field( "{input} is an invalid {field}.", title="Invalid Field", description="Displayed when a query field contains an invalid or unsupported value. `{input}` and `{field}` may be used to display the invalid input value and corresponding field name.", ) - general: StrictStr = Field( + general: str = Field( "Something went wrong.", title="General Error", description="Displayed when generalized errors occur. Seeing this error message may indicate a bug in hyperglass, as most other errors produced are highly contextual. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.", ) - request_timeout: StrictStr = Field( + not_found: str = Field( + "{type} '{name}' not found.", + title="Not Found", + description="Displayed when an object property does not exist in the configuration. `{type}` corresponds to a user-friendly name of the object type (for example, 'Device'), `{name}` corresponds to the object name that was not found.", + ) + request_timeout: str = Field( "Request timed out.", title="Request Timeout", description="Displayed when the [request_timeout](/fixme) time expires.", ) - connection_error: StrictStr = Field( + connection_error: str = Field( "Error connecting to {device_name}: {error}", title="Displayed when hyperglass is unable to connect to a configured device. Usually, this indicates a configuration error. `{device_name}` and `{error}` may be used to display the device in question and the specific connection error.", ) - authentication_error: StrictStr = Field( + authentication_error: str = Field( "Authentication error occurred.", title="Authentication Error", description="Displayed when hyperglass is unable to authenticate to a configured device. Usually, this indicates a configuration error.", ) - no_response: StrictStr = Field( + no_response: str = Field( "No response.", title="No Response", description="Displayed when hyperglass can connect to a device, but no output able to be read. Seeing this error may indicate a bug in hyperglas or one of its dependencies. If you see this in the wild, try enabling [debug mode](/fixme) and review the logs to pinpoint the source of the error.", ) - vrf_not_associated: StrictStr = Field( - "VRF {vrf_name} is not associated with {device_name}.", - title="VRF Not Associated", - description="Displayed when a query request's VRF field value contains a VRF that is not configured or associated with the corresponding location/device. The hyperglass UI automatically filters out VRFs that are not configured on a selected device, so this error is most likely to appear when using the hyperglass API. `{vrf_name}` and `{device_name}` may be used to display the VRF in question and corresponding device.", - ) - vrf_not_found: StrictStr = Field( - "VRF {vrf_name} is not defined.", - title="VRF Not Found", - description="Displayed when a query VRF is not configured on any devices. The hyperglass UI only shows configured VRFs, so this error is most likely to appear when using the hyperglass API. `{vrf_name}` may be used to display the VRF in question.", - ) - no_output: StrictStr = Field( + no_output: str = Field( "The query completed, but no matching results were found.", title="No Output", description="Displayed when hyperglass can connect to a device and execute a query, but the response is empty.", ) - parsing_error: StrictStr = Field( - "An error occurred while parsing the query output.", - title="Parsing Error", - description="Displayed when hyperglass can connect to a device and execute a query, but the response cannot be parsed.", - ) - class Config: - """Pydantic model configuration.""" + def has(self, attr: str) -> bool: + """Determine if message type exists in Messages model.""" + return attr in self.model_dump().keys() + + def __getitem__(self, attr: str) -> str: + """Make messages subscriptable.""" - title = "Messages" - description = "Customize almost all user-facing UI & API messages." - schema_extra = {"level": 2} + if not self.has(attr): + raise KeyError(f"'{attr}' does not exist on Messages model") + return getattr(self, attr) diff --git a/hyperglass/models/config/network.py b/hyperglass/models/config/network.py deleted file mode 100644 index 0248c4627..000000000 --- a/hyperglass/models/config/network.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Validate network configuration variables.""" - -# Third Party -from pydantic import Field, StrictStr - -# Local -from ..main import HyperglassModel - - -class Network(HyperglassModel): - """Validation Model for per-network/asn config in devices.yaml.""" - - name: StrictStr = Field( - ..., - title="Network Name", - description="Internal name of the device's primary network.", - ) - display_name: StrictStr = Field( - ..., - title="Network Display Name", - description="Display name of the device's primary network.", - ) diff --git a/hyperglass/models/config/opengraph.py b/hyperglass/models/config/opengraph.py index e80947f1f..5406ca500 100644 --- a/hyperglass/models/config/opengraph.py +++ b/hyperglass/models/config/opengraph.py @@ -1,16 +1,14 @@ """Validate OpenGraph Configuration Parameters.""" # Standard Library -import os from pathlib import Path # Third Party -from pydantic import FilePath, validator +from pydantic import FilePath, field_validator # Local from ..main import HyperglassModel -CONFIG_PATH = Path(os.environ["hyperglass_directory"]) DEFAULT_IMAGES = Path(__file__).parent.parent.parent / "images" @@ -19,22 +17,13 @@ class OpenGraph(HyperglassModel): image: FilePath = DEFAULT_IMAGES / "hyperglass-opengraph.jpg" - @validator("image") + @field_validator("image") def validate_opengraph(cls, value): - """Ensure the opengraph image is a supported format. - - Arguments: - value {FilePath} -- Path to opengraph image file. - - Returns: - {Path} -- Opengraph image file path object - """ + """Ensure the opengraph image is a supported format.""" supported_extensions = (".jpg", ".jpeg", ".png") if value is not None and value.suffix not in supported_extensions: raise ValueError( - "OpenGraph image must be one of {e}".format( - e=", ".join(supported_extensions) - ) + "OpenGraph image must be one of {e}".format(e=", ".join(supported_extensions)) ) return value diff --git a/hyperglass/models/config/params.py b/hyperglass/models/config/params.py index dc35064e0..1e35ba2b0 100644 --- a/hyperglass/models/config/params.py +++ b/hyperglass/models/config/params.py @@ -1,185 +1,168 @@ """Configuration validation entry point.""" # Standard Library -from typing import List, Union, Optional -from ipaddress import ip_address +import typing as t +import urllib.parse +from pathlib import Path # Third Party -from pydantic import ( - Field, - StrictInt, - StrictStr, - StrictBool, - IPvAnyAddress, - constr, - validator, -) +from pydantic import Field, HttpUrl, ConfigDict, ValidationInfo, field_validator + +# Project +from hyperglass.settings import Settings +from hyperglass.constants import __version__ # Local from .web import Web from .docs import Docs from ..main import HyperglassModel from .cache import Cache -from ..fields import IntFloat from .logging import Logging -from .queries import Queries from .messages import Messages from .structured import Structured -Localhost = constr(regex=r"localhost") +Localhost = t.Literal["localhost"] -class Params(HyperglassModel): - """Validation model for all configuration variables.""" +class APIParams(t.TypedDict): + """/api/info response model.""" - # Top Level Params - debug: StrictBool = Field( - False, - title="Debug", - description="Enable debug mode. Warning: this will generate a *lot* of log output.", - ) - developer_mode: StrictBool = Field( - False, - title="Developer Mode", - description='Enable developer mode. If enabled, the hyperglass backend (Python) and frontend (React/Javascript) applications are "unlinked", so that React tools can be used for front end development. A `` convenience component is also displayed in the UI for easier UI development.', - ) - fake_output: StrictBool = Field( - False, - title="Fake Output", - description="If enabled, the hyperglass backend will return static fake output for development/testing purposes.", + name: str + organization: str + primary_asn: int + version: str + + +class ParamsPublic(HyperglassModel): + """Public configuration parameters.""" + + request_timeout: int = Field( + 90, + title="Request Timeout", + description="Global timeout in seconds for all requests. The frontend application (UI) uses this field's exact value when submitting queries. The backend application uses this field's value, minus one second, for its own timeout handling. This is to ensure a contextual timeout error is presented to the end user in the event of a backend application timeout.", ) - primary_asn: Union[StrictInt, StrictStr] = Field( + primary_asn: t.Union[int, str] = Field( "65001", title="Primary ASN", description="Your network's primary ASN. This field is used to set some useful defaults such as the subtitle and PeeringDB URL.", ) - org_name: StrictStr = Field( + org_name: str = Field( "Beloved Hyperglass User", title="Organization Name", description="Your organization's name. This field is used in the UI & API documentation to set fields such as `` HTML tags for SEO and the terms & conditions footer component.", ) - site_title: StrictStr = Field( + site_title: str = Field( "hyperglass", title="Site Title", description="The name of your hyperglass site. This field is used in the UI & API documentation to set fields such as the `` HTML tag, and the terms & conditions footer component.", ) - site_description: StrictStr = Field( + site_description: str = Field( "{org_name} Network Looking Glass", title="Site Description", description='A short description of your hyperglass site. This field is used in th UI & API documentation to set the `<meta name="description"/>` tag. `{org_name}` may be used to insert the value of the `org_name` field.', ) - site_keywords: List[StrictStr] = Field( - [ - "hyperglass", - "looking glass", - "lg", - "peer", - "peering", - "ip", - "ipv4", - "ipv6", - "transit", - "community", - "communities", - "bgp", - "routing", - "network", - "isp", - "internet service provider", - ], - title="Site Keywords", - description='Keywords pertaining to your hyperglass site. This field is used to generate `<meta name="keywords"/>` HTML tags, which helps tremendously with SEO.', - ) - request_timeout: StrictInt = Field( - 90, - title="Request Timeout", - description="Global timeout in seconds for all requests. The frontend application (UI) uses this field's exact value when submitting queries. The backend application uses this field's value, minus one second, for its own timeout handling. This is to ensure a contextual timeout error is presented to the end user in the event of a backend application timeout.", - ) - listen_address: Optional[Union[IPvAnyAddress, Localhost]] = Field( - None, - title="Listen Address", - description="Local IP Address or hostname the hyperglass application listens on to serve web traffic.", - ) - listen_port: StrictInt = Field( - 8001, - title="Listen Port", - description="Local TCP port the hyperglass application listens on to serve web traffic.", + + +class Params(ParamsPublic, HyperglassModel): + """Validation model for all configuration variables.""" + + model_config = ConfigDict(json_schema_extra={"level": 1}) + + # Top Level Params + + fake_output: bool = Field( + False, + title="Fake Output", + description="If enabled, the hyperglass backend will return static fake output for development/testing purposes.", ) - cors_origins: List[StrictStr] = Field( + cors_origins: t.List[str] = Field( [], title="Cross-Origin Resource Sharing", description="Allowed CORS hosts. By default, no CORS hosts are allowed.", ) - netmiko_delay_factor: IntFloat = Field( - 0.1, - title="Netmiko Delay Factor", - description="Override the netmiko global delay factor.", - ) - google_analytics: Optional[StrictStr] + plugins: t.List[str] = [] # Sub Level Params cache: Cache = Cache() docs: Docs = Docs() logging: Logging = Logging() messages: Messages = Messages() - queries: Queries = Queries() structured: Structured = Structured() web: Web = Web() - class Config: - """Pydantic model configuration.""" - - schema_extra = {"level": 1} - - @validator("listen_address", pre=True, always=True) - def validate_listen_address(cls, value, values): - """Set default listen_address based on debug mode. - - Arguments: - value {str|IPvAnyAddress|None} -- listen_address - values {dict} -- already-validated entries before listen_address - - Returns: - {str} -- Validated listen_address - """ - if value is None and not values["debug"]: - listen_address = ip_address("127.0.0.1") - elif value is None and values["debug"]: - listen_address = ip_address("0.0.0.0") # noqa: S104 - elif isinstance(value, str) and value != "localhost": - try: - listen_address = ip_address(value) - except ValueError: - raise ValueError(str(value)) - elif isinstance(value, str) and value == "localhost": - listen_address = ip_address("127.0.0.1") - else: - raise ValueError(str(value)) - return listen_address - - @validator("site_description") - def validate_site_description(cls, value, values): - """Format the site descripion with the org_name field. - - Arguments: - value {str} -- site_description - values {str} -- Values before site_description - - Returns: - {str} -- Formatted description - """ - return value.format(org_name=values["org_name"]) - - @validator("primary_asn") - def validate_primary_asn(cls, value): - """Stringify primary_asn if passed as an integer. - - Arguments: - value {str|int} -- Unvalidated Primary ASN - - Returns: - {str} -- Stringified Primary ASN. - """ - if not isinstance(value, str): - value = str(value) - return value + def __init__(self, **kw: t.Any) -> None: + return super().__init__(**self.convert_paths(kw)) + + @field_validator("site_description") + def validate_site_description(cls: "Params", value: str, info: ValidationInfo) -> str: + """Format the site description with the org_name field.""" + return value.format(org_name=info.data.get("org_name")) + + @field_validator("primary_asn") + def validate_primary_asn(cls: "Params", value: t.Union[int, str]) -> str: + """Stringify primary_asn if passed as an integer.""" + return str(value) + + @field_validator("plugins") + def validate_plugins(cls: "Params", value: t.List[str]) -> t.List[str]: + """Validate and register configured plugins.""" + plugin_dir = Settings.app_path / "plugins" + + if plugin_dir.exists(): + # Path objects whose file names match configured file names, should work + # whether or not file extension is specified. + matching_plugins = ( + f + for f in plugin_dir.iterdir() + if f.name.split(".")[0] in (p.split(".")[0] for p in value) + ) + return [str(f) for f in matching_plugins] + return [] + + @field_validator("web", mode="after") + @classmethod + def validate_web(cls, web: Web, info: ValidationInfo) -> Web: + """String-format Link URLs.""" + for link in web.links: + url = urllib.parse.unquote(str(link.url), encoding="utf-8", errors="replace").format( + primary_asn=info.data.get("primary_asn", "65000") + ) + link.url = HttpUrl(url) + + for menu in web.menus: + menu.content = menu.content.format( + site_title=info.data.get("site_title", "hyperglass"), + org_name=info.data.get("org_name", "hyperglass"), + version=__version__, + ) + return web + + def common_plugins(self) -> t.Tuple[Path, ...]: + """Get all validated external common plugins as Path objects.""" + return tuple(Path(p) for p in self.plugins) + + def export_api(self) -> APIParams: + """Export API-specific parameters.""" + return { + "name": self.site_title, + "organization": self.org_name, + "primary_asn": int(self.primary_asn), + "version": __version__, + } + + def frontend(self) -> t.Dict[str, t.Any]: + """Export UI-specific parameters.""" + + return self.export_dict( + include={ + "cache": {"show_text", "timeout"}, + "developer_mode": ..., + "primary_asn": ..., + "request_timeout": ..., + "org_name": ..., + "site_title": ..., + "site_description": ..., + "web": ..., + "messages": ..., + } + ) diff --git a/hyperglass/models/config/proxy.py b/hyperglass/models/config/proxy.py index fbaec0cb0..2e9b29b8d 100644 --- a/hyperglass/models/config/proxy.py +++ b/hyperglass/models/config/proxy.py @@ -1,58 +1,59 @@ """Validate SSH proxy configuration variables.""" # Standard Library -from typing import Union +import typing as t from ipaddress import IPv4Address, IPv6Address # Third Party -from pydantic import StrictInt, StrictStr, validator +from pydantic import ValidationInfo, field_validator # Project from hyperglass.util import resolve_hostname -from hyperglass.exceptions import ConfigError, UnsupportedDevice +from hyperglass.exceptions.private import ConfigError, UnsupportedDevice # Local from ..main import HyperglassModel +from ..util import check_legacy_fields from .credential import Credential class Proxy(HyperglassModel): """Validation model for per-proxy config in devices.yaml.""" - name: StrictStr - address: Union[IPv4Address, IPv6Address, StrictStr] - port: StrictInt = 22 + address: t.Union[IPv4Address, IPv6Address, str] + port: int = 22 credential: Credential - nos: StrictStr = "linux_ssh" + platform: str = "linux_ssh" + + def __init__(self: "Proxy", **kwargs: t.Any) -> None: + """Check for legacy fields.""" + kwargs = check_legacy_fields(model="Proxy", data=kwargs) + super().__init__(**kwargs) @property def _target(self): return str(self.address) - @validator("address") - def validate_address(cls, value, values): + @field_validator("address") + def validate_address(cls, value): """Ensure a hostname is resolvable.""" + if not isinstance(value, (IPv4Address, IPv6Address)): if not any(resolve_hostname(value)): raise ConfigError( - "Device '{d}' has an address of '{a}', which is not resolvable.", - d=values["name"], + "Proxy '{a}' is not resolvable.", a=value, ) return value - @validator("nos") - def supported_nos(cls, value, values): - """Verify NOS is supported by hyperglass. - - Raises: - UnsupportedDevice: Raised if NOS is not supported. + @field_validator("platform", mode="before") + def validate_type(cls: "Proxy", value: t.Any, info: ValidationInfo) -> str: + """Validate device type.""" - Returns: - {str} -- Valid NOS name - """ - if not value == "linux_ssh": + if value != "linux_ssh": raise UnsupportedDevice( - f"Proxy '{values['name']}' uses NOS '{value}', which is currently unsupported." + "Proxy '{}' uses platform '{}', which is currently unsupported.", + info.data.get("address"), + value, ) return value diff --git a/hyperglass/models/config/queries.py b/hyperglass/models/config/queries.py deleted file mode 100644 index dfe3b53a0..000000000 --- a/hyperglass/models/config/queries.py +++ /dev/null @@ -1,235 +0,0 @@ -"""Validate query configuration parameters.""" - -# Standard Library -from typing import List - -# Third Party -from pydantic import Field, StrictStr, StrictBool, constr - -# Project -from hyperglass.constants import SUPPORTED_QUERY_TYPES - -# Local -from ..main import HyperglassModel - -ASPathMode = constr(regex=r"asplain|asdot") -CommunityInput = constr(regex=r"(input|select)") - - -class BgpCommunityPattern(HyperglassModel): - """Validation model for bgp_community regex patterns.""" - - decimal: StrictStr = Field( - r"^[0-9]{1,10}$", - title="Decimal Community", - description="Regular expression pattern for validating decimal type BGP Community strings.", - ) - extended_as: StrictStr = Field( - r"^([0-9]{0,5})\:([0-9]{1,5})$", - title="Extended AS Community", - description="Regular expression pattern for validating extended AS type BGP Community strings, e.g. `65000:1`", - ) - large: StrictStr = Field( - r"^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$", - title="Large Community", - description="Regular expression pattern for validating large community strings, e.g. `65000:65001:65002`", - ) - - class Config: - """Pydantic model configuration.""" - - title = "Pattern" - description = ( - "Regular expression patterns used to validate BGP Community queries." - ) - - -class BgpAsPathPattern(HyperglassModel): - """Validation model for bgp_aspath regex patterns.""" - - mode: ASPathMode = Field( - "asplain", - title="AS Path Mode", - description="Set ASN display mode. This field is dependent on how your network devices are configured.", - ) - asplain: StrictStr = Field( - r"^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$", - title="AS Plain", - description="Regular expression pattern for validating [AS Plain](/fixme) type BGP AS Path queries.", - ) - asdot: StrictStr = Field( - r"^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$", - title="AS Dot", - description="Regular expression pattern for validating [AS Dot](/fixme) type BGP AS Path queries.", - ) - - class Config: - """Pydantic model configuration.""" - - title = "Pattern" - description = ( - "Regular expression patterns used to validate BGP AS Path queries." - ) - - -class Community(HyperglassModel): - """Validation model for bgp_community communities.""" - - display_name: StrictStr - description: StrictStr - community: StrictStr - - -class BgpCommunity(HyperglassModel): - """Validation model for bgp_community configuration.""" - - enable: StrictBool = Field( - True, - title="Enable", - description="Enable or disable the BGP Community query type.", - ) - display_name: StrictStr = Field( - "BGP Community", - title="Display Name", - description="Text displayed for the BGP Community query type in the hyperglas UI.", - ) - pattern: BgpCommunityPattern = BgpCommunityPattern() - mode: CommunityInput = "input" - communities: List[Community] = [] - - -class BgpRoute(HyperglassModel): - """Validation model for bgp_route configuration.""" - - enable: StrictBool = Field( - True, title="Enable", description="Enable or disable the BGP Route query type." - ) - display_name: StrictStr = Field( - "BGP Route", - title="Display Name", - description="Text displayed for the BGP Route query type in the hyperglas UI.", - ) - - -class BgpAsPath(HyperglassModel): - """Validation model for bgp_aspath configuration.""" - - enable: StrictBool = Field( - True, - title="Enable", - description="Enable or disable the BGP AS Path query type.", - ) - display_name: StrictStr = Field( - "BGP AS Path", - title="Display Name", - description="Text displayed for the BGP AS Path query type in the hyperglas UI.", - ) - pattern: BgpAsPathPattern = BgpAsPathPattern() - - -class Ping(HyperglassModel): - """Validation model for ping configuration.""" - - enable: StrictBool = Field( - True, title="Enable", description="Enable or disable the Ping query type." - ) - display_name: StrictStr = Field( - "Ping", - title="Display Name", - description="Text displayed for the Ping query type in the hyperglas UI.", - ) - - -class Traceroute(HyperglassModel): - """Validation model for traceroute configuration.""" - - enable: StrictBool = Field( - True, title="Enable", description="Enable or disable the Traceroute query type." - ) - display_name: StrictStr = Field( - "Traceroute", - title="Display Name", - description="Text displayed for the Traceroute query type in the hyperglas UI.", - ) - - -class Queries(HyperglassModel): - """Validation model for all query types.""" - - @property - def map(self): - """Return a dict of all query display names, internal names, and enable state. - - Returns: - {dict} -- Dict of queries. - """ - _map = {} - for query in SUPPORTED_QUERY_TYPES: - query_obj = getattr(self, query) - _map[query] = { - "name": query, - **query_obj.export_dict( - include={"display_name", "enable", "mode", "communities"} - ), - } - return _map - - @property - def list(self): - """Return a list of all query display names, internal names, and enable state. - - Returns: - {list} -- Dict of queries. - """ - _list = [] - for query in SUPPORTED_QUERY_TYPES: - query_obj = getattr(self, query) - _list.append( - { - "name": query, - "display_name": query_obj.display_name, - "enable": query_obj.enable, - } - ) - return _list - - bgp_route: BgpRoute = BgpRoute() - bgp_community: BgpCommunity = BgpCommunity() - bgp_aspath: BgpAsPath = BgpAsPath() - ping: Ping = Ping() - traceroute: Traceroute = Traceroute() - - def __getitem__(self, query_type: str): - """Get a query's object by name.""" - if hasattr(self, query_type): - return getattr(self, query_type) - - raise AttributeError(f"Query '{query_type}' is invalid") - - class Config: - """Pydantic model configuration.""" - - title = "Queries" - description = "Enable, disable, or configure query types." - fields = { - "bgp_route": { - "title": "BGP Route", - "description": "Enable, disable, or configure the BGP Route query type.", - }, - "bgp_community": { - "title": "BGP Community", - "description": "Enable, disable, or configure the BGP Community query type.", - }, - "bgp_aspath": { - "title": "BGP AS Path", - "description": "Enable, disable, or configure the BGP AS Path query type.", - }, - "ping": { - "title": "Ping", - "description": "Enable, disable, or configure the Ping query type.", - }, - "traceroute": { - "title": "Traceroute", - "description": "Enable, disable, or configure the Traceroute query type.", - }, - } diff --git a/hyperglass/models/config/ssl.py b/hyperglass/models/config/ssl.py deleted file mode 100644 index e9dc32ebd..000000000 --- a/hyperglass/models/config/ssl.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Validate SSL configuration variables.""" - -# Standard Library -from typing import Optional - -# Third Party -from pydantic import Field, FilePath, StrictBool - -# Local -from ..main import HyperglassModel - - -class Ssl(HyperglassModel): - """Validate SSL config parameters.""" - - enable: StrictBool = Field( - True, - title="Enable SSL", - description="If enabled, hyperglass will use HTTPS to connect to the configured device running [hyperglass-agent](/fixme). If enabled, a certificate file must be specified (hyperglass does not support connecting to a device over an unverified SSL session.)", - ) - cert: Optional[FilePath] - - class Config: - """Pydantic model configuration.""" - - title = "SSL" - description = "SSL configuration for devices running hyperglass-agent." - fields = { - "cert": { - "title": "Certificate", - "description": "Valid path to an SSL certificate. This certificate must be the public key used to serve the hyperglass-agent API on the device running hyperglass-agent.", - } - } diff --git a/hyperglass/models/config/structured.py b/hyperglass/models/config/structured.py index f3790d441..4373e8187 100644 --- a/hyperglass/models/config/structured.py +++ b/hyperglass/models/config/structured.py @@ -1,23 +1,20 @@ """Structured data configuration variables.""" # Standard Library -from typing import List - -# Third Party -from pydantic import StrictStr, constr +import typing as t # Local from ..main import HyperglassModel -StructuredCommunityMode = constr(regex=r"(permit|deny)") -StructuredRPKIMode = constr(regex=r"(router|external)") +StructuredCommunityMode = t.Literal["permit", "deny"] +StructuredRPKIMode = t.Literal["router", "external"] class StructuredCommunities(HyperglassModel): - """Control structured data response for BGP communties.""" + """Control structured data response for BGP communities.""" mode: StructuredCommunityMode = "deny" - items: List[StrictStr] = [] + items: t.List[str] = [] class StructuredRpki(HyperglassModel): diff --git a/hyperglass/models/config/vrf.py b/hyperglass/models/config/vrf.py deleted file mode 100644 index a46666bd2..000000000 --- a/hyperglass/models/config/vrf.py +++ /dev/null @@ -1,299 +0,0 @@ -"""Validate VRF configuration variables.""" - -# Standard Library -import re -from typing import Dict, List, Union, Optional -from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network - -# Third Party -from pydantic import ( - Field, - FilePath, - StrictStr, - StrictBool, - PrivateAttr, - conint, - constr, - validator, - root_validator, -) -from typing_extensions import Literal - -# Project -from hyperglass.log import log - -# Local -from ..main import HyperglassModel, HyperglassModelExtra - -ACLAction = constr(regex=r"permit|deny") -AddressFamily = Union[Literal[4], Literal[6]] - - -def find_vrf_id(values: Dict) -> str: - """Generate (private) VRF ID.""" - - def generate_id(name: str) -> str: - scrubbed = re.sub(r"[^A-Za-z0-9\_\-\s]", "", name) - return "_".join(scrubbed.split()).lower() - - display_name = values.get("display_name") - - if display_name is None: - raise ValueError("display_name is required.") - - return generate_id(display_name) - - -class AccessList4(HyperglassModel): - """Validation model for IPv4 access-lists.""" - - network: IPv4Network = Field( - "0.0.0.0/0", - title="Network", - description="IPv4 Network/Prefix that should be permitted or denied. ", - ) - action: ACLAction = Field( - "permit", - title="Action", - description="Permit or deny any networks contained within the prefix.", - ) - ge: conint(ge=0, le=32) = Field( - 0, - title="Greater Than or Equal To", - description="Similar to `ge` in a Cisco prefix-list, the `ge` field defines the **bottom** threshold for prefix size. For example, a value of `24` would result in a query for `192.0.2.0/23` being denied, but a query for `192.0.2.0/32` would be permitted. If this field is set to a value smaller than the `network` field's prefix length, this field's value will be overwritten to the prefix length of the prefix in the `network` field.", - ) - le: conint(ge=0, le=32) = Field( - 32, - title="Less Than or Equal To", - description="Similar to `le` in a Cisco prefix-list, the `le` field defines the **top** threshold for prefix size. For example, a value of `24` would result in a query for `192.0.2.0/23` being permitted, but a query for `192.0.2.0/32` would be denied.", - ) - - @validator("ge") - def validate_model(cls, value, values): - """Ensure ge is at least the size of the input prefix. - - Arguments: - value {int} -- Initial ge value - values {dict} -- Other post-validation fields - - Returns: - {int} -- Validated ge value - """ - net_len = values["network"].prefixlen - if net_len > value: - value = net_len - return value - - -class AccessList6(HyperglassModel): - """Validation model for IPv6 access-lists.""" - - network: IPv6Network = Field( - "::/0", - title="Network", - description="IPv6 Network/Prefix that should be permitted or denied. ", - ) - action: ACLAction = Field( - "permit", - title="Action", - description="Permit or deny any networks contained within the prefix.", - ) - ge: conint(ge=0, le=128) = Field( - 0, - title="Greater Than or Equal To", - description="Similar to `ge` in a Cisco prefix-list, the `ge` field defines the **bottom** threshold for prefix size. For example, a value of `64` would result in a query for `2001:db8::/48` being denied, but a query for `2001:db8::1/128` would be permitted. If this field is set to a value smaller than the `network` field's prefix length, this field's value will be overwritten to the prefix length of the prefix in the `network` field.", - ) - le: conint(ge=0, le=128) = Field( - 128, - title="Less Than or Equal To", - description="Similar to `le` in a Cisco prefix-list, the `le` field defines the **top** threshold for prefix size. For example, a value of `64` would result in a query for `2001:db8::/48` being permitted, but a query for `2001:db8::1/128` would be denied.", - ) - - @validator("ge") - def validate_model(cls, value, values): - """Ensure ge is at least the size of the input prefix. - - Arguments: - value {int} -- Initial ge value - values {dict} -- Other post-validation fields - - Returns: - {int} -- Validated ge value - """ - net_len = values["network"].prefixlen - if net_len > value: - value = net_len - return value - - -class InfoConfigParams(HyperglassModelExtra): - """Validation model for per-help params.""" - - title: Optional[StrictStr] - - class Config: - """Pydantic model configuration.""" - - title = "Help Parameters" - description = "Set dynamic or reusable values which may be used in the help/information content. Params my be access by using Python string formatting syntax, e.g. `{param_name}`. Any arbitrary values may be added." - - -class InfoConfig(HyperglassModel): - """Validation model for help configuration.""" - - enable: StrictBool = True - file: Optional[FilePath] - params: InfoConfigParams = InfoConfigParams() - - class Config: - """Pydantic model configuration.""" - - fields = { - "enable": { - "title": "Enable", - "description": "Enable or disable the display of help/information content for this query type.", - }, - "file": { - "title": "File Name", - "description": "Path to a valid text or Markdown file containing custom content.", - }, - } - - -class Info(HyperglassModel): - """Validation model for per-VRF, per-Command help.""" - - bgp_aspath: InfoConfig = InfoConfig() - bgp_community: InfoConfig = InfoConfig() - bgp_route: InfoConfig = InfoConfig() - ping: InfoConfig = InfoConfig() - traceroute: InfoConfig = InfoConfig() - - class Config: - """Pydantic model configuration.""" - - title = "VRF Information" - description = "Per-VRF help & information content." - fields = { - "bgp_aspath": { - "title": "BGP AS Path", - "description": "Show information about bgp_aspath queries when selected.", - }, - "bgp_community": { - "title": "BGP Community", - "description": "Show information about bgp_community queries when selected.", - }, - "bgp_route": { - "title": "BGP Route", - "description": "Show information about bgp_route queries when selected.", - }, - "ping": { - "title": "Ping", - "description": "Show information about ping queries when selected.", - }, - "traceroute": { - "title": "Traceroute", - "description": "Show information about traceroute queries when selected.", - }, - } - - -class DeviceVrf4(HyperglassModelExtra): - """Validation model for IPv4 AFI definitions.""" - - source_address: IPv4Address - access_list: List[AccessList4] = [AccessList4()] - force_cidr: StrictBool = True - - -class DeviceVrf6(HyperglassModelExtra): - """Validation model for IPv6 AFI definitions.""" - - source_address: IPv6Address - access_list: List[AccessList6] = [AccessList6()] - force_cidr: StrictBool = True - - -class Vrf(HyperglassModel): - """Validation model for per VRF/afi config in devices.yaml.""" - - _id: StrictStr = PrivateAttr() - name: StrictStr - display_name: StrictStr - info: Info = Info() - ipv4: Optional[DeviceVrf4] - ipv6: Optional[DeviceVrf6] - default: StrictBool = False - - def __init__(self, **kwargs) -> None: - """Set the VRF ID.""" - _id = find_vrf_id(kwargs) - super().__init__(**kwargs) - self._id = _id - - @root_validator - def set_dynamic(cls, values: Dict) -> Dict: - """Set dynamic attributes before VRF initialization.""" - - if values["name"] == "default": - log.warning( - """You have set the VRF name to 'default'. This is no longer the way to -designate a VRF as the default (or global routing table) VRF. Instead, -add 'default: true' to the VRF definition. -""" - ) - - if values.get("default", False) is True: - protocol4 = "ipv4_default" - protocol6 = "ipv6_default" - - else: - protocol4 = "ipv4_vpn" - protocol6 = "ipv6_vpn" - - if values.get("ipv4") is not None: - values["ipv4"].protocol = protocol4 - values["ipv4"].version = 4 - - if values.get("ipv6") is not None: - values["ipv6"].protocol = protocol6 - values["ipv6"].version = 6 - - if values.get("default", False) and values.get("display_name") is None: - values["display_name"] = "Global" - - return values - - def __getitem__(self, i: AddressFamily) -> Union[DeviceVrf4, DeviceVrf6]: - """Access the VRF's AFI by IP protocol number.""" - if i not in (4, 6): - raise AttributeError(f"Must be 4 or 6, got '{i}'") - - return getattr(self, f"ipv{i}") - - def __hash__(self) -> int: - """Make VRF object hashable so the object can be deduplicated with set().""" - return hash((self.name,)) - - def __eq__(self, other: object) -> bool: - """Make VRF object comparable so the object can be deduplicated with set().""" - result = False - if isinstance(other, HyperglassModel): - result = self.name == other.name - return result - - class Config: - """Pydantic model configuration.""" - - title = "VRF" - description = "Per-VRF configuration." - fields = { - "name": { - "title": "Name", - "description": "VRF name as configured on the router/device.", - }, - "display_name": { - "title": "Display Name", - "description": "Display name of VRF for use in the hyperglass UI. If none is specified, hyperglass will attempt to generate one.", - }, - } diff --git a/hyperglass/models/config/web.py b/hyperglass/models/config/web.py index 53d8627cd..7916d9e9b 100644 --- a/hyperglass/models/config/web.py +++ b/hyperglass/models/config/web.py @@ -1,21 +1,12 @@ """Validate branding configuration variables.""" # Standard Library -from typing import Union, Optional, Sequence +import typing as t from pathlib import Path # Third Party -from pydantic import ( - HttpUrl, - FilePath, - StrictInt, - StrictStr, - StrictBool, - constr, - validator, - root_validator, -) -from pydantic.color import Color +from pydantic import Field, HttpUrl, FilePath, ValidationInfo, field_validator, model_validator +from pydantic_extra_types.color import Color # Project from hyperglass.defaults import DEFAULT_HELP, DEFAULT_TERMS @@ -26,91 +17,65 @@ from .opengraph import OpenGraph DEFAULT_IMAGES = Path(__file__).parent.parent.parent / "images" +DOH_PROVIDERS_PATTERN = "|".join(DNS_OVER_HTTPS.keys()) +PERCENTAGE_PATTERN = r"^([1-9][0-9]?|100)\%?$" -Percentage = constr(regex=r"^([1-9][0-9]?|100)\%$") -TitleMode = constr(regex=("logo_only|text_only|logo_title|logo_subtitle|all")) -ColorMode = constr(regex=r"light|dark") -DOHProvider = constr(regex="|".join(DNS_OVER_HTTPS.keys())) -Title = constr(max_length=32) -Side = constr(regex=r"left|right") - - -class Analytics(HyperglassModel): - """Validation model for Google Analytics.""" - - enable: StrictBool = False - id: Optional[StrictStr] - - @validator("id") - def validate_id(cls, value, values): - """Ensure ID is set if analytics is enabled. - - Arguments: - value {str|None} -- Google Analytics ID - values {[type]} -- Already-validated model parameters - - Raises: - ValueError: Raised if analytics is enabled but no ID is set. - - Returns: - {str|None} -- Google Analytics ID if enabled. - """ - if values["enable"] and value is None: - raise ValueError("Analytics is enabled, but no ID is set.") - return value +Percentage = Field(pattern=r"^([1-9][0-9]?|100)\%$") +TitleMode = t.Literal["logo_only", "text_only", "logo_subtitle", "all"] +ColorMode = t.Literal["light", "dark"] +Side = t.Literal["left", "right"] +LocationDisplayMode = t.Literal["auto", "dropdown", "gallery"] class Credit(HyperglassModel): """Validation model for developer credit.""" - enable: StrictBool = True + enable: bool = True class Link(HyperglassModel): """Validation model for generic link.""" - title: StrictStr + title: str url: HttpUrl - show_icon: StrictBool = True + show_icon: bool = True side: Side = "left" - order: StrictInt = 0 + order: int = 0 class Menu(HyperglassModel): """Validation model for generic menu.""" - title: StrictStr - content: StrictStr + title: str + content: str side: Side = "left" - order: StrictInt = 0 + order: int = 0 - @validator("content") - def validate_content(cls, value): + @field_validator("content") + def validate_content(cls: "Menu", value: str) -> str: """Read content from file if a path is provided.""" if len(value) < 260: path = Path(value) - if path.exists(): + if path.is_file() and path.exists(): with path.open("r") as f: return f.read() - else: - return value return value class Greeting(HyperglassModel): """Validation model for greeting modal.""" - enable: StrictBool = False - file: Optional[FilePath] - title: StrictStr = "Welcome" - button: StrictStr = "Continue" - required: StrictBool = False + enable: bool = False + file: t.Optional[FilePath] = None + title: str = "Welcome" + button: str = "Continue" + required: bool = False - @validator("file") - def validate_file(cls, value, values): + @field_validator("file") + def validate_file(cls, value: str, info: ValidationInfo): """Ensure file is specified if greeting is enabled.""" - if values["enable"] and value is None: + if info.data.get("enable") and value is None: raise ValueError("Greeting is enabled, but no file is specified.") return value @@ -121,42 +86,45 @@ class Logo(HyperglassModel): light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg" dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg" favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg" - width: Optional[Union[StrictInt, Percentage]] = "100%" - height: Optional[Union[StrictInt, Percentage]] + width: str = Field(default="50%", pattern=PERCENTAGE_PATTERN) + height: t.Optional[str] = Field(default=None, pattern=PERCENTAGE_PATTERN) + + +class LogoPublic(Logo): + """Public logo configuration.""" + + light_format: str + dark_format: str class Text(HyperglassModel): """Validation model for params.branding.text.""" title_mode: TitleMode = "logo_only" - title: Title = "hyperglass" - subtitle: Title = "Network Looking Glass" - query_location: StrictStr = "Location" - query_type: StrictStr = "Query Type" - query_target: StrictStr = "Target" - query_vrf: StrictStr = "Routing Table" - fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript - fqdn_message: StrictStr = "Your browser has resolved {fqdn} to" # Formatted by Javascript - fqdn_error: StrictStr = "Unable to resolve {fqdn}" # Formatted by Javascript - fqdn_error_button: StrictStr = "Try Again" - cache_prefix: StrictStr = "Results cached for " - cache_icon: StrictStr = "Cached from {time} UTC" # Formatted by Javascript - complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript - rpki_invalid: StrictStr = "Invalid" - rpki_valid: StrictStr = "Valid" - rpki_unknown: StrictStr = "No ROAs Exist" - rpki_unverified: StrictStr = "Not Verified" - no_communities: StrictStr = "No Communities" - - @validator("title_mode") - def validate_title_mode(cls, value): - """Set legacy logo_title to logo_subtitle.""" - if value == "logo_title": - value = "logo_subtitle" - return value - - @validator("cache_prefix") - def validate_cache_prefix(cls, value): + title: str = Field(default="hyperglass", max_length=32) + subtitle: str = Field(default="Network Looking Glass", max_length=32) + query_location: str = "Location" + query_type: str = "Query Type" + query_target: str = "Target" + fqdn_tooltip: str = "Use {protocol}" # Formatted by Javascript + fqdn_message: str = "Your browser has resolved {fqdn} to" # Formatted by Javascript + fqdn_error: str = "Unable to resolve {fqdn}" # Formatted by Javascript + fqdn_error_button: str = "Try Again" + cache_prefix: str = "Results cached for " + cache_icon: str = "Cached from {time} UTC" # Formatted by Javascript + complete_time: str = "Completed in {seconds}" # Formatted by Javascript + rpki_invalid: str = "Invalid" + rpki_valid: str = "Valid" + rpki_unknown: str = "No ROAs Exist" + rpki_unverified: str = "Not Verified" + no_communities: str = "No Communities" + ip_error: str = "Unable to determine IP Address" + no_ip: str = "No {protocol} Address" + ip_select: str = "Select an IP Address" + ip_button: str = "My IP" + + @field_validator("cache_prefix") + def validate_cache_prefix(cls: "Text", value: str) -> str: """Ensure trailing whitespace.""" return " ".join(value.split()) + " " @@ -178,29 +146,22 @@ class ThemeColors(HyperglassModel): cyan: Color = "#118ab2" pink: Color = "#f2607d" purple: Color = "#8d30b5" - primary: Optional[Color] - secondary: Optional[Color] - success: Optional[Color] - warning: Optional[Color] - error: Optional[Color] - danger: Optional[Color] - - @validator(*FUNC_COLOR_MAP.keys(), pre=True, always=True) - def validate_colors(cls, value, values, field): - """Set default functional color mapping. - - Arguments: - value {str|None} -- Functional color - values {str} -- Already-validated colors - Returns: - {str} -- Mapped color. - """ + primary: t.Optional[Color] = None + secondary: t.Optional[Color] = None + success: t.Optional[Color] = None + warning: t.Optional[Color] = None + error: t.Optional[Color] = None + danger: t.Optional[Color] = None + + @field_validator(*FUNC_COLOR_MAP.keys(), mode="before") + def validate_colors(cls: "ThemeColors", value: str, info: ValidationInfo) -> str: + """Set default functional color mapping.""" if value is None: - default_color = FUNC_COLOR_MAP[field.name] - value = str(values[default_color]) + default_color = FUNC_COLOR_MAP[info.field_name] + value = str(info.data[default_color]) return value - def dict(self, *args, **kwargs): + def dict(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, str]: """Return dict for colors only.""" return {k: v.as_hex() for k, v in self.__dict__.items()} @@ -208,37 +169,58 @@ def dict(self, *args, **kwargs): class ThemeFonts(HyperglassModel): """Validation model for theme fonts.""" - body: StrictStr = "Nunito" - mono: StrictStr = "Fira Code" + body: str = "Nunito" + mono: str = "Fira Code" class Theme(HyperglassModel): """Validation model for theme variables.""" colors: ThemeColors = ThemeColors() - default_color_mode: Optional[ColorMode] + default_color_mode: t.Optional[ColorMode] = None fonts: ThemeFonts = ThemeFonts() class DnsOverHttps(HyperglassModel): """Validation model for DNS over HTTPS resolution.""" - name: DOHProvider = "cloudflare" - url: StrictStr = "" - - @root_validator - def validate_dns(cls, values): - """Assign url field to model based on selected provider. - - Arguments: - values {dict} -- Dict of selected provider - - Returns: - {dict} -- Dict with url attribute - """ - provider = values["name"] - values["url"] = DNS_OVER_HTTPS[provider] - return values + name: str = "cloudflare" + url: str = "" + + @model_validator(mode="before") + def validate_dns(cls, data: "DnsOverHttps") -> t.Dict[str, str]: + """Assign url field to model based on selected provider.""" + name = data.get("name", "cloudflare") + url = data.get("url", DNS_OVER_HTTPS["cloudflare"]) + if url not in DNS_OVER_HTTPS.values(): + return { + "name": "custom", + "url": url, + } + url = DNS_OVER_HTTPS[name] + return { + "name": name, + "url": url, + } + + +class HighlightPattern(HyperglassModel): + """Validation model for highlight pattern configuration.""" + + pattern: str + label: t.Optional[str] = None + color: str = "primary" + + @field_validator("color") + def validate_color(cls: "HighlightPattern", value: str) -> str: + """Ensure highlight color is a valid theme color.""" + colors = list(ThemeColors.model_fields.keys()) + color_list = "\n - ".join(("", *colors)) + if value not in colors: + raise ValueError( + "{!r} is not a supported color. Must be one of:{!s}".format(value, color_list) + ) + return value class Web(HyperglassModel): @@ -246,10 +228,10 @@ class Web(HyperglassModel): credit: Credit = Credit() dns_provider: DnsOverHttps = DnsOverHttps() - links: Sequence[Link] = [ + links: t.Sequence[Link] = [ Link(title="PeeringDB", url="https://www.peeringdb.com/asn/{primary_asn}") ] - menus: Sequence[Menu] = [ + menus: t.Sequence[Menu] = [ Menu(title="Terms", content=DEFAULT_TERMS), Menu(title="Help", content=DEFAULT_HELP), ] @@ -258,3 +240,13 @@ class Web(HyperglassModel): opengraph: OpenGraph = OpenGraph() text: Text = Text() theme: Theme = Theme() + location_display_mode: LocationDisplayMode = "auto" + custom_javascript: t.Optional[FilePath] = None + custom_html: t.Optional[FilePath] = None + highlight: t.List[HighlightPattern] = [] + + +class WebPublic(Web): + """Public web configuration.""" + + logo: LogoPublic diff --git a/hyperglass/models/data/__init__.py b/hyperglass/models/data/__init__.py new file mode 100644 index 000000000..02d8f276d --- /dev/null +++ b/hyperglass/models/data/__init__.py @@ -0,0 +1,14 @@ +"""Data structure models.""" + +# Standard Library +from typing import Union + +# Local +from .bgp_route import BGPRouteTable + +OutputDataModel = Union[BGPRouteTable] + +__all__ = ( + "BGPRouteTable", + "OutputDataModel", +) diff --git a/hyperglass/models/data/bgp_route.py b/hyperglass/models/data/bgp_route.py new file mode 100644 index 000000000..a00af4e13 --- /dev/null +++ b/hyperglass/models/data/bgp_route.py @@ -0,0 +1,124 @@ +"""Device-Agnostic Parsed Response Data Model.""" + +# Standard Library +import re +import typing as t +from ipaddress import ip_network + +# Third Party +from pydantic import ValidationInfo, field_validator + +# Project +from hyperglass.state import use_state +from hyperglass.external.rpki import rpki_state + +# Local +from ..main import HyperglassModel + +WinningWeight = t.Literal["low", "high"] + + +class BGPRoute(HyperglassModel): + """Post-parsed BGP route.""" + + prefix: str + active: bool + age: int + weight: int + med: int + local_preference: int + as_path: t.List[int] + communities: t.List[str] + next_hop: str + source_as: int + source_rid: str + peer_rid: str + rpki_state: int + + @field_validator("communities") + def validate_communities(cls, value): + """Filter returned communities against configured policy. + + Actions: + permit: only permit matches + deny: only deny matches + """ + + (structured := use_state("params").structured) + + def _permit(comm): + """Only allow matching patterns.""" + valid = False + for pattern in structured.communities.items: + if re.match(pattern, comm): + valid = True + break + return valid + + def _deny(comm): + """Allow any except matching patterns.""" + valid = True + for pattern in structured.communities.items: + if re.match(pattern, comm): + valid = False + break + return valid + + func_map = {"permit": _permit, "deny": _deny} + func = func_map[structured.communities.mode] + + return [c for c in value if func(c)] + + @field_validator("rpki_state") + def validate_rpki_state(cls, value, info: ValidationInfo): + """If external RPKI validation is enabled, get validation state.""" + + (structured := use_state("params").structured) + + if structured.rpki.mode == "router": + # If router validation is enabled, return the value as-is. + return value + + if structured.rpki.mode == "external": + # If external validation is enabled, validate the prefix + # & asn with Cloudflare's RPKI API. + as_path = info.data.get("as_path", []) + + if len(as_path) == 0: + # If the AS_PATH length is 0, i.e. for an internal route, + # return RPKI Unknown state. + return 3 + # Get last ASN in path + asn = as_path[-1] + + try: + net = ip_network(info.data["prefix"]) + except ValueError: + return 3 + + # Only do external RPKI lookups for global prefixes. + if net.is_global: + return rpki_state(prefix=info.data["prefix"], asn=asn) + + return value + + +class BGPRouteTable(HyperglassModel): + """Post-parsed BGP route table.""" + + vrf: str + count: int = 0 + routes: t.List[BGPRoute] + winning_weight: WinningWeight + + def __init__(self, **kwargs): + """Sort routes by prefix after validation.""" + super().__init__(**kwargs) + self.routes = sorted(self.routes, key=lambda r: r.prefix) + + def __add__(self: "BGPRouteTable", other: "BGPRouteTable") -> "BGPRouteTable": + """Merge another BGP table instance with this instance.""" + if isinstance(other, BGPRouteTable): + self.routes = sorted([*self.routes, *other.routes], key=lambda r: r.prefix) + self.count = len(self.routes) + return self diff --git a/hyperglass/models/directive.py b/hyperglass/models/directive.py new file mode 100644 index 000000000..c88c2c10b --- /dev/null +++ b/hyperglass/models/directive.py @@ -0,0 +1,396 @@ +"""Generic command models.""" + +# Standard Library +import re +import typing as t +from ipaddress import IPv4Network, IPv6Network, ip_network + +# Third Party +from pydantic import Field, FilePath, PrivateAttr, IPvAnyNetwork, field_validator + +# Project +from hyperglass.log import log +from hyperglass.types import Series +from hyperglass.settings import Settings +from hyperglass.exceptions.private import InputValidationError + +# Local +from .main import MultiModel, HyperglassModel, HyperglassUniqueModel +from .fields import Action + +StringOrArray = t.Union[str, t.List[str]] +Condition = t.Union[str, None] +RuleValidation = t.Union[t.Literal["ipv4", "ipv6", "pattern"], None] +PassedValidation = t.Union[bool, None] +IPFamily = t.Literal["ipv4", "ipv6"] +RuleTypeAttr = t.Literal["ipv4", "ipv6", "pattern", "none"] + + +class Input(HyperglassModel): + """Base input field.""" + + _type: PrivateAttr + description: str + + @property + def is_select(self) -> bool: + """Determine if this field is a select field.""" + return self._type == "select" + + @property + def is_text(self) -> bool: + """Determine if this field is an input/text field.""" + return self._type == "text" + + +class Text(Input): + """Text/input field model.""" + + _type: PrivateAttr = PrivateAttr("text") + validation: t.Optional[str] = None + + +class Option(HyperglassModel): + """Select option model.""" + + name: t.Optional[str] = None + description: t.Optional[str] = None + value: str + + +class Select(Input): + """Select field model.""" + + _type: PrivateAttr = PrivateAttr("select") + options: t.List[Option] + + +class Rule(HyperglassModel): + """Base rule.""" + + _type: RuleTypeAttr = "none" + _passed: PassedValidation = PrivateAttr(None) + condition: Condition + action: Action = "permit" + commands: t.List[str] = Field([], alias="command") + + @field_validator("commands", mode="before") + def validate_commands(cls, value: t.Union[str, t.List[str]]) -> t.List[str]: + """Ensure commands is a list.""" + if isinstance(value, str): + return [value] + return value + + def validate_target(self, target: str, *, multiple: bool) -> bool: + """Validate a query target (Placeholder signature).""" + raise NotImplementedError( + f"{self._type} rule does not implement a 'validate_target()' method" + ) + + +class RuleWithIP(Rule): + """Base IP-based rule.""" + + condition: t.Union[IPv4Network, IPv6Network] + allow_reserved: bool = False + allow_unspecified: bool = False + allow_loopback: bool = False + ge: int + le: int + + def __init__(self, **kw) -> None: + super().__init__(**kw) + if self.condition.network_address.version == 4: + self._type = "ipv4" + else: + self._type = "ipv6" + + def membership(self, target: IPvAnyNetwork, network: IPvAnyNetwork) -> bool: + """Check if IP address belongs to network.""" + _log = log.bind(target=str(target), network=str(network)) + _log.debug("Checking target membership") + if ( + network.network_address <= target.network_address + and network.broadcast_address >= target.broadcast_address + ): + _log.debug("Target membership verified") + return True + return False + + def in_range(self, target: IPvAnyNetwork) -> bool: + """Verify if target prefix length is within ge/le threshold.""" + if target.prefixlen <= self.le and target.prefixlen >= self.ge: + log.bind(target=str(target), range=f"{self.ge!s}-{self.le!s}").debug( + "Target is in range" + ) + return True + + return False + + def validate_target(self, target: str, *, multiple: bool) -> bool: + """Validate an IP address target against this rule's conditions.""" + + if isinstance(target, t.List): + if len(target) > 1: + self._passed = False + raise InputValidationError(error="Target must be a single value", target=target) + target = target[0] + + try: + # Attempt to use IP object factory to create an IP address object + valid_target = ip_network(target) + + except ValueError as err: + raise InputValidationError(error=str(err), target=target) from err + + if valid_target.version != self.condition.version: + log.bind(target=str(target), condition=str(self.condition)).debug( + "Mismatching IP version" + ) + return False + + is_member = self.membership(valid_target, self.condition) + in_range = self.in_range(valid_target) + + if all((is_member, in_range, self.action == "permit")): + self._passed = True + return True + + if is_member and not in_range: + self._passed = False + raise InputValidationError( + error="Prefix-length is not within range {ge}-{le}", + target=target, + ge=self.ge, + le=self.le, + ) + + if is_member and self.action == "deny": + self._passed = False + raise InputValidationError( + error="Member of denied network '{network}'", + target=target, + network=str(self.condition), + ) + + return False + + +class RuleWithIPv4(RuleWithIP): + """A rule by which to evaluate an IPv4 target.""" + + _type: RuleTypeAttr = "ipv4" + condition: IPv4Network + ge: int = Field(0, ge=0, le=32) + le: int = Field(32, ge=0, le=32) + + +class RuleWithIPv6(RuleWithIP): + """A rule by which to evaluate an IPv6 target.""" + + _type: RuleTypeAttr = "ipv6" + condition: IPv6Network + ge: int = Field(0, ge=0, le=128) + le: int = Field(128, ge=0, le=128) + + +class RuleWithPattern(Rule): + """A rule validated by a regular expression pattern.""" + + _type: RuleTypeAttr = "pattern" + condition: str + + def validate_target(self, target: str, *, multiple: bool) -> str: # noqa: C901 + """Validate a string target against configured regex patterns.""" + + def validate_single_value(value: str) -> t.Union[bool, BaseException]: + if self.condition == "*": + pattern = re.compile(".+", re.IGNORECASE) + else: + pattern = re.compile(self.condition, re.IGNORECASE) + is_match = pattern.match(value) + + if is_match and self.action == "permit": + return True + if is_match and self.action == "deny": + return InputValidationError(target=value, error="Denied") + return False + + if isinstance(target, t.List): + for result in (validate_single_value(v) for v in target): + if isinstance(result, BaseException): + self._passed = False + raise result + if result is False: + self._passed = False + return result + self._passed = True + return True + + result = validate_single_value(target) + + if isinstance(result, BaseException): + self._passed = False + raise result + self._passed = result + return result + + +class RuleWithoutValidation(Rule): + """A rule with no validation.""" + + _type: RuleTypeAttr = "none" + condition: None = None + + def validate_target(self, target: str, *, multiple: bool) -> t.Literal[True]: + """Don't validate a target. Always returns `True`.""" + self._passed = True + return True + + +RuleType = t.Union[ + RuleWithIPv4, + RuleWithIPv6, + RuleWithPattern, + RuleWithoutValidation, +] + + +class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")): + """A directive contains commands that can be run on a device, as long as defined rules are met.""" + + _hyperglass_builtin: bool = PrivateAttr(False) + + id: str + name: str + rules: t.List[RuleType] = [RuleWithoutValidation()] + field: t.Union[Text, Select, None] + info: t.Optional[FilePath] = None + plugins: t.List[str] = [] + table_output: t.Optional[str] = None + groups: t.List[str] = [] + multiple: bool = False + multiple_separator: str = " " + + @field_validator("rules", mode="before") + @classmethod + def validate_rules(cls, rules: t.List[t.Dict[str, t.Any]]): + """Initialize the correct rule type based on condition value.""" + out_rules: t.List[RuleType] = [] + for rule in rules: + if isinstance(rule, dict): + condition = rule.get("condition") + if condition is None: + out_rules.append(RuleWithoutValidation(**rule)) + else: + try: + condition_net = ip_network(condition) + if condition_net.version == 4: + out_rules.append(RuleWithIPv4(**rule)) + if condition_net.version == 6: + out_rules.append(RuleWithIPv6(**rule)) + except ValueError: + out_rules.append(RuleWithPattern(**rule)) + elif isinstance(rule, Rule): + out_rules.append(rule) + return out_rules + + def validate_target(self, target: str) -> bool: + """Validate a target against all configured rules.""" + for rule in self.rules: + valid = rule.validate_target(target, multiple=self.multiple) + if valid is True: + return True + continue + raise InputValidationError(error="No matched validation rules", target=target) + + @property + def field_type(self) -> t.Literal["text", "select", None]: + """Get the linked field type.""" + if self.field is None: + return None + if self.field.is_select: + return "select" + if self.field.is_text or self.field.is_ip: + return "text" + return None + + @field_validator("plugins") + def validate_plugins(cls: "Directive", plugins: t.List[str]) -> t.List[str]: + """Validate and register configured plugins.""" + plugin_dir = Settings.app_path / "plugins" + + if plugin_dir.exists(): + # Path objects whose file names match configured file names, should work + # whether or not file extension is specified. + matching_plugins = ( + f + for f in plugin_dir.iterdir() + if f.name.split(".")[0] in (p.split(".")[0] for p in plugins) + ) + return [str(f) for f in matching_plugins] + return [] + + def frontend(self: "Directive") -> t.Dict[str, t.Any]: + """Prepare a representation of the directive for the UI.""" + + value = { + "id": self.id, + "name": self.name, + "field_type": self.field_type, + "groups": self.groups, + "description": self.field.description if self.field is not None else '', + "info": None, + } + + if self.info is not None: + with self.info.open() as md: + value["info"] = md.read() + + if self.field is not None and self.field.is_select: + value["options"] = [o.export_dict() for o in self.field.options if o is not None] + + return value + + +class BuiltinDirective(Directive, unique_by=("id", "table_output", "platforms")): + """Natively-supported directive.""" + + _hyperglass_builtin: bool = PrivateAttr(True) + platforms: Series[str] = [] + + +DirectiveT = t.Union[BuiltinDirective, Directive] + + +class Directives(MultiModel[Directive], model=Directive, unique_by="id"): + """Collection of directives.""" + + def device_builtins(self, *, platform: str, table_output: bool): + """Get builtin directives for a device.""" + + return Directives( + *( + self.table_if_available(directive) if table_output else directive # noqa: IF100 GFY + for directive in self + if directive._hyperglass_builtin is True + and platform in getattr(directive, "platforms", ()) + ) + ) + + def table_if_available(self, directive: "Directive") -> "Directive": + """Get the table-output variant of a directive if it exists.""" + for _directive in self: + if _directive.id == directive.table_output: + return _directive + return directive + + @classmethod + def new(cls, /, *raw_directives: t.Dict[str, t.Any]) -> "Directives": + """Create a new Directives collection from raw directive configurations.""" + directives = ( + Directive(id=name, **directive) + for raw_directive in raw_directives + for name, directive in raw_directive.items() + ) + return Directives(*directives) diff --git a/hyperglass/models/fields.py b/hyperglass/models/fields.py index bd4d73428..6314a91d9 100644 --- a/hyperglass/models/fields.py +++ b/hyperglass/models/fields.py @@ -2,80 +2,56 @@ # Standard Library import re -from typing import TypeVar +import typing as t # Third Party -from pydantic import StrictInt, StrictFloat, constr - -IntFloat = TypeVar("IntFloat", StrictInt, StrictFloat) - -SupportedDriver = constr(regex=r"(scrapli|netmiko|hyperglass_agent)") - - -class StrictBytes(bytes): - """Custom data type for a strict byte string. - - Used for validating the encoded JWT request payload. - """ - - @classmethod - def __get_validators__(cls): - """Yield Pydantic validator function. - - See: https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types - - Yields: - {function} -- Validator - """ - yield cls.validate - - @classmethod - def validate(cls, value): - """Validate type. - - Arguments: - value {Any} -- Pre-validated input - - Raises: - TypeError: Raised if value is not bytes - - Returns: - {object} -- Instantiated class - """ - if not isinstance(value, bytes): - raise TypeError("bytes required") - return cls() - - def __repr__(self): - """Return representation of object. - - Returns: - {str} -- Representation - """ - return f"StrictBytes({super().__repr__()})" - - -class AnyUri(str): - """Custom field type for HTTP URI, e.g. /example.""" - - @classmethod - def __get_validators__(cls): - """Pydantic custim field method.""" - yield cls.validate - - @classmethod - def validate(cls, value): - """Ensure URI string contains a leading forward-slash.""" - uri_regex = re.compile(r"^(\/.*)$") - if not isinstance(value, str): - raise TypeError("AnyUri type must be a string") - match = uri_regex.fullmatch(value) - if not match: - raise ValueError( - "Invalid format. A URI must begin with a forward slash, e.g. '/example'" - ) - return cls(match.group()) - - def __repr__(self): - """Stringify custom field representation.""" - return f"AnyUri({super().__repr__()})" +from pydantic import AfterValidator, BeforeValidator + +IntFloat = t.TypeVar("IntFloat", int, float) +J = t.TypeVar("J") + +SupportedDriver = t.Literal["netmiko", "hyperglass_agent"] +HttpAuthMode = t.Literal["basic", "api_key"] +HttpProvider = t.Literal["msteams", "slack", "generic"] +LogFormat = t.Literal["text", "json"] +Primitives = t.Union[None, float, int, bool, str] +JsonValue = t.Union[J, t.Sequence[J], t.Dict[str, J]] +ActionValue = t.Literal["permit", "deny"] +HttpMethodValue = t.Literal[ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE", +] + + +def validate_uri(value: str) -> str: + """Ensure URI string contains a leading forward-slash.""" + uri_regex = re.compile(r"^(\/.*)$") + match = uri_regex.fullmatch(value) + if not match: + raise ValueError("Invalid format. A URI must begin with a forward slash, e.g. '/example'") + return match.group() + + +def validate_action(value: str) -> ActionValue: + """Ensure action is an allowed value or acceptable alias.""" + permits = ("permit", "allow", "accept") + denies = ("deny", "block", "reject") + value = value.strip().lower() + if value in permits: + return "permit" + if value in denies: + return "deny" + + raise ValueError("Action must be one of '{}'".format(", ".join((*permits, *denies)))) + + +AnyUri = t.Annotated[str, AfterValidator(validate_uri)] +Action = t.Annotated[ActionValue, AfterValidator(validate_action)] +HttpMethod = t.Annotated[HttpMethodValue, BeforeValidator(str.upper)] diff --git a/hyperglass/models/main.py b/hyperglass/models/main.py index 7e41a21c9..d4fa3ae60 100644 --- a/hyperglass/models/main.py +++ b/hyperglass/models/main.py @@ -1,77 +1,121 @@ """Data models used throughout hyperglass.""" +# Standard Library + # Standard Library import re +import json +import typing as t +from pathlib import Path # Third Party -from pydantic import HttpUrl, BaseModel +from pydantic import HttpUrl, BaseModel, RootModel, ConfigDict, PrivateAttr + +# Project +from hyperglass.log import log +from hyperglass.util import compare_init, snake_to_camel, repr_from_attrs +from hyperglass.types import Series + +MultiModelT = t.TypeVar("MultiModelT", bound=BaseModel) -_WEBHOOK_TITLE = "hyperglass received a valid query with the following data" -_ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png" +PathTypeT = t.TypeVar("PathTypeT") -def clean_name(_name: str) -> str: +def alias_generator(field: str) -> str: """Remove unsupported characters from field names. - Converts any "desirable" seperators to underscore, then removes all + Converts any "desirable" separators to underscore, then removes all characters that are unsupported in Python class variable names. Also removes leading numbers underscores. """ - _replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", _name) + _replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", field) _scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced)) - return _scrubbed.lower() + snake_field = _scrubbed.lower() + return snake_to_camel(snake_field) class HyperglassModel(BaseModel): """Base model for all hyperglass configuration models.""" - class Config: - """Default Pydantic configuration. + model_config = ConfigDict( + extra="forbid", + json_encoders={HttpUrl: lambda v: str(v), Path: str}, + populate_by_name=True, + validate_assignment=True, + validate_default=True, + alias_generator=alias_generator, + ) - See https://pydantic-docs.helpmanual.io/usage/model_config + def convert_paths(self, value: t.Type[PathTypeT]) -> PathTypeT: + """Change path to relative to app_path. + + This is required when running hyperglass in a container so that + the original app_path on the host system is not passed through + to the container. """ + # Project + from hyperglass.settings import Settings + + if isinstance(value, Path): + if Settings.container: + return Settings.default_app_path.joinpath( + *( + p + for p in value.parts + if p not in Settings.original_app_path.absolute().parts + ) + ) + + if isinstance(value, str) and str(Settings.original_app_path.absolute()) in value: + if Settings.container: + path = Path(value) + return str( + Settings.default_app_path.joinpath( + *( + p + for p in path.parts + if p not in Settings.original_app_path.absolute().parts + ) + ) + ) - validate_all = True - extra = "forbid" - validate_assignment = True - alias_generator = clean_name - json_encoders = {HttpUrl: lambda v: str(v)} + if isinstance(value, t.Tuple): + return tuple(self.convert_paths(v) for v in value) + if isinstance(value, t.List): + return [self.convert_paths(v) for v in value] + if isinstance(value, t.Generator): + return (self.convert_paths(v) for v in value) + if isinstance(value, t.Dict): + return {k: self.convert_paths(v) for k, v in value.items()} + return value + + def _repr_from_attrs(self, attrs: Series[str]) -> str: + """Alias to `hyperglass.util:repr_from_attrs` in the context of this model.""" + return repr_from_attrs(self, attrs) def export_json(self, *args, **kwargs): - """Return instance as JSON. + """Return instance as JSON.""" - Returns: - {str} -- Stringified JSON. - """ + export_kwargs = {"by_alias": False, "exclude_unset": False} - export_kwargs = { - "by_alias": True, - "exclude_unset": False, - **kwargs, - } + for key in kwargs.keys(): + export_kwargs.pop(key, None) - return self.json(*args, **export_kwargs) + return self.model_dump_json(*args, **export_kwargs, **kwargs) def export_dict(self, *args, **kwargs): - """Return instance as dictionary. + """Return instance as dictionary.""" - Returns: - {dict} -- Python dictionary. - """ - export_kwargs = { - "by_alias": True, - "exclude_unset": False, - **kwargs, - } + export_kwargs = {"by_alias": False, "exclude_unset": False} - return self.dict(*args, **export_kwargs) + for key in kwargs.keys(): + export_kwargs.pop(key, None) + + return self.model_dump(*args, **export_kwargs, **kwargs) def export_yaml(self, *args, **kwargs): - """Return instance as YAML. + """Return instance as YAML.""" - Returns: - {str} -- Stringified YAML. - """ # Standard Library import json @@ -79,21 +123,236 @@ def export_yaml(self, *args, **kwargs): import yaml export_kwargs = { - "by_alias": kwargs.pop("by_alias", True), - "exclude_unset": kwargs.pop("by_alias", False), + "by_alias": kwargs.pop("by_alias", False), + "exclude_unset": kwargs.pop("exclude_unset", False), } - return yaml.safe_dump( - json.loads(self.export_json(**export_kwargs)), *args, **kwargs + return yaml.safe_dump(json.loads(self.export_json(**export_kwargs)), *args, **kwargs) + + +class HyperglassUniqueModel(HyperglassModel): + """hyperglass model that is unique by its `id` field.""" + + _unique_fields: t.ClassVar[Series[str]] = () + + def __init_subclass__(cls, *, unique_by: Series[str], **kw: t.Any) -> None: + """Assign unique fields to class.""" + cls._unique_fields = tuple(unique_by) + return super().__init_subclass__(**kw) + + def __eq__(self: "HyperglassUniqueModel", other: "HyperglassUniqueModel") -> bool: + """Other model is equal to this model.""" + if not isinstance(other, self.__class__): + return False + if hash(self) == hash(other): + return True + return False + + def __ne__(self: "HyperglassUniqueModel", other: "HyperglassUniqueModel") -> bool: + """Other model is not equal to this model.""" + return not self.__eq__(other) + + def __hash__(self: "HyperglassUniqueModel") -> int: + """Create a hashed representation of this model's name.""" + fields = dict(zip(self._unique_fields, (getattr(self, f) for f in self._unique_fields))) + return hash(json.dumps(fields)) + + +class HyperglassModelWithId(HyperglassModel): + """hyperglass model that is unique by its `id` field.""" + + id: str + + def __eq__(self: "HyperglassModelWithId", other: "HyperglassModelWithId") -> bool: + """Other model is equal to this model.""" + if not isinstance(other, self.__class__): + return False + if hasattr(other, "id"): + return other and self.id == other.id + return False + + def __ne__(self: "HyperglassModelWithId", other: "HyperglassModelWithId") -> bool: + """Other model is not equal to this model.""" + return not self.__eq__(other) + + def __hash__(self: "HyperglassModelWithId") -> int: + """Create a hashed representation of this model's name.""" + return hash(self.id) + + +class MultiModel(RootModel[MultiModelT]): + """Extension of HyperglassModel for managing multiple models as a list.""" + + model_config = ConfigDict( + validate_default=True, + validate_assignment=True, + ) + + model: t.ClassVar[MultiModelT] + unique_by: t.ClassVar[str] + _model_name: t.ClassVar[str] = "MultiModel" + + root: t.List[MultiModelT] = [] + _count: int = PrivateAttr() + + def __init__(self, *items: t.Union[MultiModelT, t.Dict[str, t.Any]]) -> None: + """Validate items.""" + for cls_var in ("model", "unique_by"): + if getattr(self, cls_var, None) is None: + raise AttributeError(f"MultiModel is missing class variable '{cls_var}'") + valid = self._valid_items(*items) + super().__init__(root=valid) + self._count = len(self.root) + + def __init_subclass__(cls, **kw: t.Any) -> None: + """Add class variables from keyword arguments.""" + model = kw.pop("model", None) + cls.model = model + cls.unique_by = kw.pop("unique_by", None) + cls._model_name = getattr(model, "__name__", "MultiModel") + super().__init_subclass__() + + def __repr__(self) -> str: + """Represent model.""" + return repr_from_attrs(self, ["_count", "unique_by", "_model_name"], strip="_") + + def __iter__(self) -> t.Iterator[MultiModelT]: + """Iterate items.""" + return iter(self.root) + + def __getitem__(self, value: t.Union[int, str]) -> MultiModelT: + """Get an item by its `unique_by` property.""" + if not isinstance(value, (str, int)): + raise TypeError( + "Value of {}.{!s} should be a string or integer. Got {!r} ({!s})".format( + self.__class__.__name__, self.unique_by, value, type(value) + ) + ) + if isinstance(value, int): + return self.root[value] + + for item in self: + if hasattr(item, self.unique_by) and getattr(item, self.unique_by) == value: + return item + raise IndexError( + "No match found for {!s}.{!s}={!r}".format( + self.model.__class__.__name__, self.unique_by, value + ), + ) + + def __add__(self, other: MultiModelT) -> MultiModelT: + """Merge another MultiModel with this one. + + Note: If you're subclassing `HyperglassMultiModel` and overriding `__init__`, you need to + override this too. + """ + valid = all( + ( + isinstance(other, self.__class__), + hasattr(other, "model"), + getattr(other, "model", None) == self.model, + ), + ) + if not valid: + raise TypeError(f"Cannot add {other!r} to {self.__class__.__name__}") + merged = self._merge_with(*other, unique_by=self.unique_by) + + if compare_init(self.__class__, other.__class__): + return self.__class__(*merged) + raise TypeError( + f"{self.__class__.__name__} and {other.__class__.__name__} have different `__init__` " + "signatures. You probably need to override `MultiModel.__add__`" + ) + + def __len__(self) -> int: + """Get number of items.""" + return len(self.root) + + @property + def ids(self) -> t.Tuple[t.Any, ...]: + """Get values of all items by `unique_by` property.""" + return tuple(sorted(getattr(item, self.unique_by) for item in self)) + + @property + def count(self) -> int: + """Access item count.""" + return self._count + + @classmethod + def create(cls, name: str, *, model: MultiModelT, unique_by: str) -> "MultiModel": + """Create a MultiModel.""" + new = type(name, (cls,), cls.__dict__) + new.model = model + new.unique_by = unique_by + new._model_name = getattr(model, "__name__", "MultiModel") + return new + + def _valid_items( + self, *to_validate: t.List[t.Union[MultiModelT, t.Dict[str, t.Any]]] + ) -> t.List[MultiModelT]: + items = [ + item + for item in to_validate + if any( + ( + (isinstance(item, self.model) and hasattr(item, self.unique_by)), + (isinstance(item, t.Dict) and self.unique_by in item), + ), + ) + ] + for index, item in enumerate(items): + if isinstance(item, t.Dict): + items[index] = self.model(**item) + return items + + def _merge_with(self, *items, unique_by: t.Optional[str] = None) -> Series[MultiModelT]: + to_add = self._valid_items(*items) + if unique_by is not None: + unique_by_values = { + getattr(obj, unique_by) for obj in (*self, *to_add) if hasattr(obj, unique_by) + } + unique_by_objects = { + v: o + for v in unique_by_values + for o in (*self, *to_add) + if getattr(o, unique_by) == v + } + return tuple(unique_by_objects.values()) + return (*self.root, *to_add) + + def filter(self, *properties: str) -> MultiModelT: + """Get only items with `unique_by` properties matching values in `properties`.""" + return self.__class__( + *(item for item in self if getattr(item, self.unique_by, None) in properties) ) + def matching(self, *unique: str) -> MultiModelT: + """Get a new instance containing partial matches from `accessors`.""" -class HyperglassModelExtra(HyperglassModel): - """Model for hyperglass configuration models with dynamic fields.""" + def matches(*searches: str) -> t.Generator[MultiModelT, None, None]: + """Get any matching items by unique_by property. - pass + For example, if `unique` is `('one', 'two')`, and `Model.<unique_by>` is `'one'`, + `Model` is yielded. + """ + for search in searches: + pattern = re.compile(rf".*{search}.*", re.IGNORECASE) + for item in self: + if pattern.match(getattr(item, self.unique_by)): + yield item - class Config: - """Default pydantic configuration.""" + return self.__class__(*matches(*unique)) - extra = "allow" + def add(self, *items, unique_by: t.Optional[str] = None) -> None: + """Add an item to the model.""" + new = self._merge_with(*items, unique_by=unique_by) + self.root = new + self._count = len(self.root) + for item in new: + log.debug( + "Added {} '{!s}' to {}".format( + item.__class__.__name__, + getattr(item, self.unique_by), + self.__class__.__name__, + ) + ) diff --git a/hyperglass/models/parsing/arista_eos.py b/hyperglass/models/parsing/arista_eos.py index 4643457d0..6f41ad186 100644 --- a/hyperglass/models/parsing/arista_eos.py +++ b/hyperglass/models/parsing/arista_eos.py @@ -1,15 +1,18 @@ """Data Models for Parsing Arista JSON Response.""" # Standard Library -from typing import Dict, List, Optional +import typing as t from datetime import datetime +# Third Party +from pydantic import ConfigDict + # Project from hyperglass.log import log +from hyperglass.models.data import BGPRouteTable # Local from ..main import HyperglassModel -from .serialized import ParsedRoutes RPKI_STATE_MAP = { "invalid": 0, @@ -29,16 +32,14 @@ def _alias_generator(field: str) -> str: class _AristaBase(HyperglassModel): """Base Model for Arista validation.""" - class Config: - extra = "ignore" - alias_generator = _alias_generator + model_config = ConfigDict(extra="ignore", alias_generator=_alias_generator) class AristaAsPathEntry(_AristaBase): """Validation model for Arista asPathEntry.""" as_path_type: str = "External" - as_path: Optional[str] = "" + as_path: t.Optional[str] = "" class AristaPeerEntry(_AristaBase): @@ -55,25 +56,25 @@ class AristaRouteType(_AristaBase): suppressed: bool valid: bool active: bool - origin_validity: Optional[str] = "notVerified" + origin_validity: t.Optional[str] = "notVerified" class AristaRouteDetail(_AristaBase): """Validation for Arista routeDetail.""" origin: str - label_stack: List = [] - ext_community_list: List[str] = [] - ext_community_list_raw: List[str] = [] - community_list: List[str] = [] - large_community_list: List[str] = [] + label_stack: t.List = [] + ext_community_list: t.List[str] = [] + ext_community_list_raw: t.List[t.Union[str, int]] = [] + community_list: t.List[str] = [] + large_community_list: t.List[str] = [] class AristaRoutePath(_AristaBase): """Validation model for Arista bgpRoutePaths.""" as_path_entry: AristaAsPathEntry - med: int + med: int = 0 local_preference: int weight: int peer_entry: AristaPeerEntry @@ -81,24 +82,24 @@ class AristaRoutePath(_AristaBase): timestamp: int = int(datetime.utcnow().timestamp()) next_hop: str route_type: AristaRouteType - route_detail: Optional[AristaRouteDetail] + route_detail: t.Optional[AristaRouteDetail] class AristaRouteEntry(_AristaBase): """Validation model for Arista bgpRouteEntries.""" total_paths: int = 0 - bgp_advertised_peer_groups: Dict = {} + bgp_advertised_peer_groups: t.Dict = {} mask_length: int - bgp_route_paths: List[AristaRoutePath] = [] + bgp_route_paths: t.List[AristaRoutePath] = [] -class AristaRoute(_AristaBase): +class AristaBGPTable(_AristaBase): """Validation model for Arista bgpRouteEntries data.""" router_id: str vrf: str - bgp_route_entries: Dict[str, AristaRouteEntry] + bgp_route_entries: t.Dict[str, AristaRouteEntry] # The raw value is really a string, but `int` will convert it. asn: int @@ -109,21 +110,19 @@ def _get_route_age(timestamp: int) -> int: return now_timestamp - timestamp @staticmethod - def _get_as_path(as_path: str) -> List[str]: + def _get_as_path(as_path: str) -> t.List[str]: if as_path == "": return [] return [int(p) for p in as_path.split() if p.isdecimal()] - def serialize(self): + def bgp_table(self: "AristaBGPTable") -> "BGPRouteTable": """Convert the Arista-formatted fields to standard parsed data model.""" routes = [] count = 0 for prefix, entries in self.bgp_route_entries.items(): - count += entries.total_paths for route in entries.bgp_route_paths: - as_path = self._get_as_path(route.as_path_entry.as_path) rpki_state = RPKI_STATE_MAP.get(route.route_type.origin_validity, 3) @@ -157,9 +156,12 @@ def serialize(self): } ) - serialized = ParsedRoutes( - vrf=self.vrf, count=count, routes=routes, winning_weight=WINNING_WEIGHT, + serialized = BGPRouteTable( + vrf=self.vrf, + count=count, + routes=routes, + winning_weight=WINNING_WEIGHT, ) - log.debug("Serialized Arista response: {}", serialized) + log.bind(platform="arista_eos", response=repr(serialized)).debug("Serialized response") return serialized diff --git a/hyperglass/models/parsing/frr.py b/hyperglass/models/parsing/frr.py index e9bdd2174..dacd82ce0 100644 --- a/hyperglass/models/parsing/frr.py +++ b/hyperglass/models/parsing/frr.py @@ -1,20 +1,20 @@ """Data Models for Parsing FRRouting JSON Response.""" # Standard Library -from typing import List +import typing as t from datetime import datetime # Third Party -from pydantic import StrictInt, StrictStr, StrictBool, constr, root_validator +from pydantic import ConfigDict, model_validator # Project from hyperglass.log import log +from hyperglass.models.data import BGPRouteTable # Local from ..main import HyperglassModel -from .serialized import ParsedRoutes -FRRPeerType = constr(regex=r"(internal|external)") +FRRPeerType = t.Literal["internal", "external", "confed-internal", "confed-external"] def _alias_generator(field): @@ -23,64 +23,64 @@ def _alias_generator(field): class _FRRBase(HyperglassModel): - class Config: - alias_generator = _alias_generator - extra = "ignore" + model_config = ConfigDict(alias_generator=_alias_generator, extra="ignore") class FRRNextHop(_FRRBase): """FRR Next Hop Model.""" - ip: StrictStr - afi: StrictStr - metric: StrictInt - accessible: StrictBool - used: StrictBool + ip: str + afi: str + metric: int + accessible: bool + used: bool class FRRPeer(_FRRBase): """FRR Peer Model.""" - peer_id: StrictStr - router_id: StrictStr + peer_id: str + router_id: str type: FRRPeerType class FRRPath(_FRRBase): """FRR Path Model.""" - aspath: List[StrictInt] - aggregator_as: StrictInt - aggregator_id: StrictStr - med: StrictInt = 0 - localpref: StrictInt - weight: StrictInt - valid: StrictBool - last_update: StrictInt - bestpath: StrictBool - community: List[StrictStr] - nexthops: List[FRRNextHop] + aspath: t.List[int] + aggregator_as: int = 0 + aggregator_id: str = "" + loc_prf: int = 100 # 100 is the default value for local preference + metric: int = 0 + med: int = 0 + weight: int = 0 + valid: bool + last_update: int + bestpath: bool + community: t.List[str] + nexthops: t.List[FRRNextHop] peer: FRRPeer - @root_validator(pre=True) + @model_validator(mode="before") def validate_path(cls, values): """Extract meaningful data from FRR response.""" new = values.copy() new["aspath"] = values["aspath"]["segments"][0]["list"] - new["community"] = values["community"]["list"] + community = values.get("community", {"list": []}) + new["community"] = community["list"] new["lastUpdate"] = values["lastUpdate"]["epoch"] bestpath = values.get("bestpath", {}) new["bestpath"] = bestpath.get("overall", False) return new -class FRRRoute(_FRRBase): +class FRRBGPTable(_FRRBase): """FRR Route Model.""" - prefix: StrictStr - paths: List[FRRPath] = [] + prefix: str + paths: t.List[FRRPath] = [] - def serialize(self): + def bgp_table(self): """Convert the FRR-specific fields to standard parsed data model.""" # TODO: somehow, get the actual VRF @@ -98,7 +98,7 @@ def serialize(self): "age": age, "weight": route.weight, "med": route.med, - "local_preference": route.localpref, + "local_preference": route.loc_prf, "as_path": route.aspath, "communities": route.community, "next_hop": route.nexthops[0].ip, @@ -106,13 +106,17 @@ def serialize(self): "source_rid": route.aggregator_id, "peer_rid": route.peer.peer_id, # TODO: somehow, get the actual RPKI state + # This depends on whether or not the RPKI module is enabled in FRR "rpki_state": 3, } ) - serialized = ParsedRoutes( - vrf=vrf, count=len(routes), routes=routes, winning_weight="high", + serialized = BGPRouteTable( + vrf=vrf, + count=len(routes), + routes=routes, + winning_weight="high", ) - log.info("Serialized FRR response: {}", serialized) + log.bind(platform="frr", response=repr(serialized)).debug("Serialized response") return serialized diff --git a/hyperglass/models/parsing/juniper.py b/hyperglass/models/parsing/juniper.py index 514ffc0fc..f4f999c95 100644 --- a/hyperglass/models/parsing/juniper.py +++ b/hyperglass/models/parsing/juniper.py @@ -1,17 +1,18 @@ """Data Models for Parsing Juniper XML Response.""" # Standard Library -from typing import Dict, List +import typing as t # Third Party -from pydantic import StrictInt, StrictStr, StrictBool, validator, root_validator +from pydantic import ConfigDict, field_validator, model_validator # Project from hyperglass.log import log +from hyperglass.util import deep_convert_keys +from hyperglass.models.data.bgp_route import BGPRouteTable # Local from ..main import HyperglassModel -from .serialized import ParsedRoutes RPKI_STATE_MAP = { "invalid": 0, @@ -21,35 +22,39 @@ } -def _alias_generator(field): - return field.replace("_", "-") +class JuniperBase(HyperglassModel, extra="ignore"): + """Base Juniper model.""" + def __init__(self, **kwargs: t.Any) -> None: + """Convert all `-` keys to `_`. -class _JuniperBase(HyperglassModel): - class Config: - alias_generator = _alias_generator - extra = "ignore" + Default camelCase alias generator will still be used. + """ + rebuilt = deep_convert_keys(kwargs, lambda k: k.replace("-", "_")) + super().__init__(**rebuilt) -class JuniperRouteTableEntry(_JuniperBase): +class JuniperRouteTableEntry(JuniperBase): """Parse Juniper rt-entry data.""" - active_tag: StrictBool + model_config = ConfigDict(validate_assignment=False) + + active_tag: bool preference: int - age: StrictInt + age: int local_preference: int metric: int = 0 - as_path: List[StrictInt] = [] - validation_state: StrictInt = 3 - next_hop: StrictStr - peer_rid: StrictStr + as_path: t.List[int] = [] + validation_state: int = 3 + next_hop: str + peer_rid: str peer_as: int source_as: int - source_rid: StrictStr - communities: List[StrictStr] = None + source_rid: str + communities: t.List[str] = None - @root_validator(pre=True) - def validate_optional_flags(cls, values): + @model_validator(mode="before") + def validate_optional_flags(cls, values: t.Dict[str, t.Any]): """Flatten & rename keys prior to validation.""" next_hops = [] nh = None @@ -59,11 +64,11 @@ def validate_optional_flags(cls, values): nh = values.pop("nh") # Handle Juniper's 'Indirect' Next Hop Type - if "protocol-nh" in values: - nh = values.pop("protocol-nh") + if "protocol_nh" in values: + nh = values.pop("protocol_nh") # Force the next hops to be a list - if isinstance(nh, Dict): + if isinstance(nh, t.Dict): nh = [nh] if nh is not None: @@ -72,32 +77,30 @@ def validate_optional_flags(cls, values): # Extract the 'to:' value from the next-hop selected_next_hop = "" for hop in next_hops: - if "selected-next-hop" in hop: + if "selected_next_hop" in hop: selected_next_hop = hop.get("to", "") break - elif hop.get("to") is not None: + if hop.get("to") is not None: selected_next_hop = hop["to"] break - values["next-hop"] = selected_next_hop + values["next_hop"] = selected_next_hop - _path_attr = values.get("bgp-path-attributes", {}) - _path_attr_agg = _path_attr.get("attr-aggregator", {}).get("attr-value", {}) - values["as-path"] = _path_attr.get("attr-as-path-effective", {}).get( - "attr-value", "" - ) - values["source-as"] = _path_attr_agg.get("aggr-as-number", 0) - values["source-rid"] = _path_attr_agg.get("aggr-router-id", "") - values["peer-rid"] = values["peer-id"] + _path_attr = values.get("bgp_path_attributes", {}) + _path_attr_agg = _path_attr.get("attr_aggregator", {}).get("attr_value", {}) + values["as_path"] = _path_attr.get("attr_as_path_effective", {}).get("attr_value", "") + values["source_as"] = _path_attr_agg.get("aggr_as_number", 0) + values["source_rid"] = _path_attr_agg.get("aggr_router_id", "") + values["peer_rid"] = values.get("peer_id", "") return values - @validator("validation_state", pre=True, always=True) + @field_validator("validation_state", mode="before") def validate_rpki_state(cls, value): """Convert string RPKI state to standard integer mapping.""" return RPKI_STATE_MAP.get(value, 3) - @validator("active_tag", pre=True, always=True) + @field_validator("active_tag", mode="before") def validate_active_tag(cls, value): """Convert active-tag from string/null to boolean.""" if value == "*": @@ -106,25 +109,25 @@ def validate_active_tag(cls, value): value = False return value - @validator("age", pre=True, always=True) + @field_validator("age", mode="before") def validate_age(cls, value): """Get age as seconds.""" if not isinstance(value, dict): try: value = int(value) - except ValueError: - raise ValueError(f"Age field is in an unexpected format. Got: {value}") + except ValueError as err: + raise ValueError(f"Age field is in an unexpected format. Got: {value}") from err else: value = value.get("@junos:seconds", 0) return int(value) - @validator("as_path", pre=True, always=True) + @field_validator("as_path", mode="before") def validate_as_path(cls, value): """Remove origin flags from AS_PATH.""" disallowed = ("E", "I", "?") return [int(a) for a in value.split() if a not in disallowed] - @validator("communities", pre=True, always=True) + @field_validator("communities", mode="before") def validate_communities(cls, value): """Flatten community list.""" if value is not None: @@ -134,32 +137,32 @@ def validate_communities(cls, value): return flat -class JuniperRouteTable(_JuniperBase): +class JuniperRouteTable(JuniperBase): """Validation model for Juniper rt data.""" - rt_destination: StrictStr + rt_destination: str rt_prefix_length: int rt_entry_count: int rt_announced_count: int - rt_entry: List[JuniperRouteTableEntry] + rt_entry: t.List[JuniperRouteTableEntry] - @validator("rt_entry_count", pre=True, always=True) + @field_validator("rt_entry_count", mode="before") def validate_entry_count(cls, value): """Flatten & convert entry-count to integer.""" return int(value.get("#text")) -class JuniperRoute(_JuniperBase): +class JuniperBGPTable(JuniperBase): """Validation model for route-table data.""" - table_name: StrictStr + table_name: str destination_count: int total_route_count: int active_route_count: int hidden_route_count: int - rt: List[JuniperRouteTable] + rt: t.List[JuniperRouteTable] - def serialize(self): + def bgp_table(self: "JuniperBGPTable") -> "BGPRouteTable": """Convert the Juniper-specific fields to standard parsed data model.""" vrf_parts = self.table_name.split(".") if len(vrf_parts) == 2: @@ -171,9 +174,7 @@ def serialize(self): count = 0 for table in self.rt: count += table.rt_entry_count - prefix = "/".join( - str(i) for i in (table.rt_destination, table.rt_prefix_length) - ) + prefix = "/".join(str(i) for i in (table.rt_destination, table.rt_prefix_length)) for route in table.rt_entry: routes.append( { @@ -193,9 +194,6 @@ def serialize(self): } ) - serialized = ParsedRoutes( - vrf=vrf, count=count, routes=routes, winning_weight="low", - ) - - log.debug("Serialized Juniper response: {}", serialized) + serialized = BGPRouteTable(vrf=vrf, count=count, routes=routes, winning_weight="low") + log.bind(platform="juniper", response=repr(serialized)).debug("Serialized response") return serialized diff --git a/hyperglass/models/parsing/serialized.py b/hyperglass/models/parsing/serialized.py deleted file mode 100644 index c4c0bca98..000000000 --- a/hyperglass/models/parsing/serialized.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Device-Agnostic Parsed Response Data Model.""" - -# Standard Library -import re -from typing import List -from ipaddress import ip_network - -# Third Party -from pydantic import StrictInt, StrictStr, StrictBool, constr, validator - -# Project -from hyperglass.configuration import params -from hyperglass.external.rpki import rpki_state - -# Local -from ..main import HyperglassModel - -WinningWeight = constr(regex=r"(low|high)") - - -class ParsedRouteEntry(HyperglassModel): - """Per-Route Response Model.""" - - prefix: StrictStr - active: StrictBool - age: StrictInt - weight: StrictInt - med: StrictInt - local_preference: StrictInt - as_path: List[StrictInt] - communities: List[StrictStr] - next_hop: StrictStr - source_as: StrictInt - source_rid: StrictStr - peer_rid: StrictStr - rpki_state: StrictInt - - @validator("communities") - def validate_communities(cls, value): - """Filter returned communities against configured policy. - - Actions: - permit: only permit matches - deny: only deny matches - """ - - def _permit(comm): - """Only allow matching patterns.""" - valid = False - for pattern in params.structured.communities.items: - if re.match(pattern, comm): - valid = True - break - return valid - - def _deny(comm): - """Allow any except matching patterns.""" - valid = True - for pattern in params.structured.communities.items: - if re.match(pattern, comm): - valid = False - break - return valid - - func_map = {"permit": _permit, "deny": _deny} - func = func_map[params.structured.communities.mode] - - return [c for c in value if func(c)] - - @validator("rpki_state") - def validate_rpki_state(cls, value, values): - """If external RPKI validation is enabled, get validation state.""" - - if params.structured.rpki.mode == "router": - # If router validation is enabled, return the value as-is. - return value - - elif params.structured.rpki.mode == "external": - # If external validation is enabled, validate the prefix - # & asn with Cloudflare's RPKI API. - as_path = values["as_path"] - - if len(as_path) == 0: - # If the AS_PATH length is 0, i.e. for an internal route, - # return RPKI Unknown state. - return 3 - else: - # Get last ASN in path - asn = as_path[-1] - - try: - net = ip_network(values["prefix"]) - except ValueError: - return 3 - - # Only do external RPKI lookups for global prefixes. - if net.is_global: - return rpki_state(prefix=values["prefix"], asn=asn) - else: - return value - - -class ParsedRoutes(HyperglassModel): - """Parsed Response Model.""" - - vrf: StrictStr - count: StrictInt = 0 - routes: List[ParsedRouteEntry] - winning_weight: WinningWeight diff --git a/hyperglass/models/system.py b/hyperglass/models/system.py new file mode 100644 index 000000000..049452631 --- /dev/null +++ b/hyperglass/models/system.py @@ -0,0 +1,176 @@ +"""hyperglass System Settings model.""" + +# Standard Library +import typing as t +from pathlib import Path +from ipaddress import ip_address + +# Third Party +from pydantic import ( + FilePath, + RedisDsn, + SecretStr, + DirectoryPath, + IPvAnyAddress, + ValidationInfo, + field_validator, +) +from pydantic_settings import BaseSettings, SettingsConfigDict + +# Project +from hyperglass.util import at_least, cpu_count + +if t.TYPE_CHECKING: + # Third Party + from rich.console import Console, RenderResult, ConsoleOptions + +ListenHost = t.Union[None, IPvAnyAddress, t.Literal["localhost"]] + +_default_app_path = Path("/etc/hyperglass") + + +class HyperglassSettings(BaseSettings): + """hyperglass system settings, required to start hyperglass.""" + + model_config = SettingsConfigDict(env_prefix="hyperglass_") + + config_file_names: t.ClassVar[t.Tuple[str, ...]] = ("config", "devices", "directives") + default_app_path: t.ClassVar[Path] = _default_app_path + original_app_path: Path = _default_app_path + + debug: bool = False + dev_mode: bool = False + disable_ui: bool = False + app_path: DirectoryPath = _default_app_path + redis_host: str = "localhost" + redis_password: t.Optional[SecretStr] = None + redis_db: int = 1 + redis_dsn: RedisDsn = None + host: IPvAnyAddress = None + port: int = 8001 + ca_cert: t.Optional[FilePath] = None + container: bool = False + + def __init__(self, **kwargs) -> None: + """Create hyperglass Settings instance.""" + super().__init__(**kwargs) + if self.container: + self.app_path = self.default_app_path + + def __rich_console__(self, console: "Console", options: "ConsoleOptions") -> "RenderResult": + """Render a Rich table representation of hyperglass settings.""" + # Third Party + from rich.panel import Panel + from rich.style import Style + from rich.table import Table, box + from rich.pretty import Pretty + + table = Table(box=box.MINIMAL, border_style="subtle") + table.add_column("Environment Variable", style=Style(color="#118ab2", bold=True)) + table.add_column("Value") + params = sorted( + ( + "debug", + "dev_mode", + "app_path", + "redis_host", + "redis_db", + "redis_dsn", + "host", + "port", + ) + ) + for attr in params: + table.add_row(f"hyperglass_{attr}".upper(), Pretty(getattr(self, attr))) + + yield Panel.fit(table, title="hyperglass settings", border_style="subtle") + + @field_validator("host", mode="before") + def validate_host( + cls: "HyperglassSettings", value: t.Any, info: ValidationInfo + ) -> IPvAnyAddress: + """Set default host based on debug mode.""" + + if value is None: + if info.data.get("debug") is False: + return ip_address("::1") + if info.data.get("debug") is True: + return ip_address("::") + + if isinstance(value, str): + if value != "localhost": + try: + return ip_address(value) + except ValueError as err: + raise ValueError(str(value)) from err + + elif value == "localhost": + return ip_address("::1") + + raise ValueError(str(value)) + + @field_validator("redis_dsn", mode="before") + def validate_redis_dsn(cls, value: t.Any, info: ValidationInfo) -> RedisDsn: + """Construct a Redis DSN if none is provided.""" + if value is None: + host = info.data.get("redis_host") + db = info.data.get("redis_db") + dsn = "redis://{}/{!s}".format(host, db) + password = info.data.get("redis_password") + if password is not None: + dsn = "redis://:{}@{}/{!s}".format(password.get_secret_value(), host, db) + return dsn + return value + + def bind(self: "HyperglassSettings") -> str: + """Format a listen_address. Wraps IPv6 address in brackets.""" + if self.host.version == 6: + return f"[{self.host!s}]:{self.port!s}" + return f"{self.host!s}:{self.port!s}" + + @property + def log_level(self: "HyperglassSettings") -> str: + """Get log level as string, inferred from debug mode.""" + if self.debug: + return "DEBUG" + return "WARNING" + + @property + def workers(self: "HyperglassSettings") -> int: + """Get worker count, inferred from debug mode.""" + if self.debug: + return 1 + return cpu_count(2) + + @property + def redis(self: "HyperglassSettings") -> t.Dict[str, t.Union[None, int, str]]: + """Get redis parameters as a dict for convenient connection setups.""" + password = None + if self.redis_password is not None: + password = self.redis_password.get_secret_value() + + return { + "db": self.redis_db, + "host": self.redis_host, + "password": password, + } + + @property + def redis_connection_pool(self: "HyperglassSettings") -> t.Dict[str, t.Any]: + """Get Redis ConnectionPool keyword arguments.""" + return {"url": str(self.redis_dsn), "max_connections": at_least(8, cpu_count(2))} + + @property + def dev_url(self: "HyperglassSettings") -> str: + """Get the hyperglass URL for when dev_mode is enabled.""" + return f"http://localhost:{self.port!s}/" + + @property + def prod_url(self: "HyperglassSettings") -> str: + """Get the UI-facing hyperglass URL/path.""" + return "/api/" + + @property + def static_path(self: "HyperglassSettings") -> Path: + """Get static asset path.""" + return Path(self.app_path / "static") diff --git a/hyperglass/models/tests/__init__.py b/hyperglass/models/tests/__init__.py new file mode 100644 index 000000000..bb9fb6694 --- /dev/null +++ b/hyperglass/models/tests/__init__.py @@ -0,0 +1 @@ +"""Model tests.""" diff --git a/hyperglass/models/tests/test_multi_model.py b/hyperglass/models/tests/test_multi_model.py new file mode 100644 index 000000000..43d80830f --- /dev/null +++ b/hyperglass/models/tests/test_multi_model.py @@ -0,0 +1,48 @@ +"""Test HyperglassMultiModel.""" + +# Third Party +from pydantic import BaseModel + +# Local +from ..main import MultiModel + + +class Item(BaseModel): + """Test item.""" + + id: str + name: str + + +class Items(MultiModel, model=Item, unique_by="id"): + """Multi Model Test.""" + + +ITEMS_1 = [ + {"id": "item1", "name": "Item One"}, + Item(id="item2", name="Item Two"), + {"id": "item3", "name": "Item Three"}, +] + +ITEMS_2 = [ + Item(id="item4", name="Item Four"), + {"id": "item5", "name": "Item Five"}, +] + +ITEMS_3 = [ + {"id": "item1", "name": "Item New One"}, + {"id": "item6", "name": "Item Six"}, +] + + +def test_multi_model(): + model = Items(*ITEMS_1) + assert model.count == 3 + assert len([o for o in model]) == model.count # noqa: C416 (Iteration testing) + assert model["item1"].name == "Item One" + model.add(*ITEMS_2) + assert model.count == 5 + assert model[3].name == "Item Four" + model.add(*ITEMS_3, unique_by="id") + assert model.count == 6 + assert model["item1"].name == "Item New One" diff --git a/hyperglass/models/tests/test_util.py b/hyperglass/models/tests/test_util.py new file mode 100644 index 000000000..f990d579e --- /dev/null +++ b/hyperglass/models/tests/test_util.py @@ -0,0 +1,30 @@ +"""Test model utilities.""" + +# Third Party +import pytest + +# Local +from ..util import check_legacy_fields + + +@pytest.mark.dependency() +def test_check_legacy_fields(): + test1 = {"name": "Device A", "nos": "juniper"} + test1_expected = {"name": "Device A", "platform": "juniper"} + test2 = {"name": "Device B", "platform": "juniper"} + test3 = {"name": "Device C"} + test4 = {"name": "Device D", "network": "this is wrong"} + + assert set(check_legacy_fields(model="Device", data=test1).keys()) == set( + test1_expected.keys() + ), "legacy field not replaced" + + assert set(check_legacy_fields(model="Device", data=test2).keys()) == set(test2.keys()), ( + "new field not left unmodified" + ) + + with pytest.raises(ValueError): + check_legacy_fields(model="Device", data=test3) + + with pytest.raises(ValueError): + check_legacy_fields(model="Device", data=test4) diff --git a/hyperglass/models/ui.py b/hyperglass/models/ui.py new file mode 100644 index 000000000..f8a304c94 --- /dev/null +++ b/hyperglass/models/ui.py @@ -0,0 +1,64 @@ +"""UI Configuration models.""" + +# Standard Library +import typing as t + +# Local +from .main import HyperglassModel +from .config.web import WebPublic +from .config.cache import Cache +from .config.params import ParamsPublic +from .config.messages import Messages + +Alignment = t.Union[t.Literal["left"], t.Literal["center"], t.Literal["right"], None] +StructuredDataField = t.Tuple[str, str, Alignment] + + +class UIDirective(HyperglassModel): + """UI: Directive.""" + + id: str + name: str + field_type: t.Union[str, None] + groups: t.List[str] + description: str + info: t.Optional[str] = None + options: t.Optional[t.List[t.Dict[str, t.Any]]] = None + + +class UILocation(HyperglassModel): + """UI: Location (Device).""" + + id: str + name: str + group: t.Optional[str] = None + avatar: t.Optional[str] = None + description: t.Optional[str] = None + directives: t.List[UIDirective] = [] + + +class UIDevices(HyperglassModel): + """UI: Devices.""" + + group: t.Optional[str] = None + locations: t.List[UILocation] = [] + + +class UIContent(HyperglassModel): + """UI: Content.""" + + credit: str + greeting: str + + +class UIParameters(ParamsPublic, HyperglassModel): + """UI Configuration Parameters.""" + + cache: Cache + web: WebPublic + messages: Messages + version: str + devices: t.List[UIDevices] = [] + parsed_data_fields: t.Tuple[StructuredDataField, ...] + content: UIContent + developer_mode: bool diff --git a/hyperglass/models/util.py b/hyperglass/models/util.py new file mode 100644 index 000000000..75f432c44 --- /dev/null +++ b/hyperglass/models/util.py @@ -0,0 +1,67 @@ +"""Model utilities.""" + +# Standard Library +import typing as t + +# Third Party +from pydantic import BaseModel + +# Project +from hyperglass.log import log + + +class LegacyField(BaseModel): + """Define legacy fields on a per-model basis. + + When `overwrite` is `True`, the old key is replaced with the new + key. This will generally only occur when the value type is the same, + and the key name has only changed names for clarity or cosmetic + purposes. + + When `overwrite` is `False` and the old key is found, an error is + raised. This generally occurs when the overall function of the old + and new keys has remained the same, but the value type has changed, + requiring the user to make changes to the config file. + + When `required` is `True` and neither the old or new keys are found, + an error is raised. When `required` is false and neither keys are + found, nothing happens. + """ + + old: str + new: str + overwrite: bool = False + required: bool = True + + +LEGACY_FIELDS: t.Dict[str, t.Tuple[LegacyField, ...]] = { + "Device": ( + LegacyField(old="nos", new="platform", overwrite=True), + LegacyField(old="network", new="group", overwrite=False, required=False), + ), + "Proxy": (LegacyField(old="nos", new="platform", overwrite=True),), +} + + +def check_legacy_fields(*, model: str, data: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + """Check for legacy fields prior to model initialization.""" + if model in LEGACY_FIELDS: + for field in LEGACY_FIELDS[model]: + legacy_value = data.pop(field.old, None) + new_value = data.get(field.new) + if legacy_value is not None and new_value is None: + if field.overwrite: + log.bind(old_field=f"{model}.{field.old}", new_field=field.new).warning( + "Deprecated field" + ) + data[field.new] = legacy_value + else: + raise ValueError( + ( + "The {!r} field has been replaced with the {!r} field. " + "Please consult the documentation and/or changelog to determine the appropriate migration path." + ).format(f"{model}.{field.old}", field.new) + ) + elif legacy_value is None and new_value is None and field.required: + raise ValueError(f"'{field.new}' is missing") + return data diff --git a/hyperglass/models/webhook.py b/hyperglass/models/webhook.py index a61e27380..2c831935e 100644 --- a/hyperglass/models/webhook.py +++ b/hyperglass/models/webhook.py @@ -1,82 +1,78 @@ """Data models used throughout hyperglass.""" # Standard Library -from typing import Optional +import typing as t from datetime import datetime # Third Party -from pydantic import StrictStr, root_validator +from pydantic import ConfigDict, model_validator # Project from hyperglass.log import log # Local -from .main import HyperglassModel, HyperglassModelExtra +from .main import HyperglassModel _WEBHOOK_TITLE = "hyperglass received a valid query with the following data" _ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png" +def to_snake_case(value: str) -> str: + """Convert string to snake case.""" + return value.replace("_", "-") + + class WebhookHeaders(HyperglassModel): """Webhook data model.""" - user_agent: Optional[StrictStr] - referer: Optional[StrictStr] - accept_encoding: Optional[StrictStr] - accept_language: Optional[StrictStr] - x_real_ip: Optional[StrictStr] - x_forwarded_for: Optional[StrictStr] - - class Config: - """Pydantic model config.""" - - fields = { - "user_agent": "user-agent", - "accept_encoding": "accept-encoding", - "accept_language": "accept-language", - "x_real_ip": "x-real-ip", - "x_forwarded_for": "x-forwarded-for", - } + model_config = ConfigDict(alias_generator=to_snake_case) + + user_agent: t.Optional[str] = None + referer: t.Optional[str] = None + accept_encoding: t.Optional[str] = None + accept_language: t.Optional[str] = None + x_real_ip: t.Optional[str] = None + x_forwarded_for: t.Optional[str] = None -class WebhookNetwork(HyperglassModelExtra): +class WebhookNetwork(HyperglassModel): """Webhook data model.""" - prefix: StrictStr = "Unknown" - asn: StrictStr = "Unknown" - org: StrictStr = "Unknown" - country: StrictStr = "Unknown" + model_config = ConfigDict(extra="allow") + + prefix: str = "Unknown" + asn: str = "Unknown" + org: str = "Unknown" + country: str = "Unknown" class Webhook(HyperglassModel): """Webhook data model.""" - query_location: StrictStr - query_type: StrictStr - query_vrf: StrictStr - query_target: StrictStr + query_location: str + query_type: str + query_target: t.Union[t.List[str], str] headers: WebhookHeaders - source: StrictStr = "Unknown" + source: str = "Unknown" network: WebhookNetwork timestamp: datetime - @root_validator(pre=True) - def validate_webhook(cls, values): + @model_validator(mode="before") + def validate_webhook(cls, model: "Webhook") -> "Webhook": """Reset network attributes if the source is localhost.""" - if values.get("source") in ("127.0.0.1", "::1"): - values["network"] = {} - return values + if model.source in ("127.0.0.1", "::1"): + model.network = {} + return model - def msteams(self): + def msteams(self) -> t.Dict[str, t.Any]: """Format the webhook data as a Microsoft Teams card.""" - def code(value): + def code(value: t.Any) -> str: """Wrap argument in backticks for markdown inline code formatting.""" return f"`{str(value)}`" header_data = [ - {"name": k, "value": code(v)} - for k, v in self.headers.dict(by_alias=True).items() + {"name": k, "value": code(v)} for k, v in self.headers.model_dump(by_alias=True).items() ] time_fmt = self.timestamp.strftime("%Y %m %d %H:%M:%S") payload = { @@ -93,7 +89,6 @@ def code(value): {"name": "Query Location", "value": self.query_location}, {"name": "Query Target", "value": code(self.query_target)}, {"name": "Query Type", "value": self.query_type}, - {"name": "Query VRF", "value": self.query_vrf}, ], }, {"markdown": True, "text": "**Source Information**"}, @@ -113,11 +108,11 @@ def code(value): {"markdown": True, "facts": header_data}, ], } - log.debug("Created MS Teams webhook: {}", str(payload)) + log.bind(type="MS Teams", payload=str(payload)).debug("Created webhook") return payload - def slack(self): + def slack(self) -> t.Dict[str, t.Any]: """Format the webhook data as a Slack message.""" def make_field(key, value, code=False): @@ -126,44 +121,25 @@ def make_field(key, value, code=False): return f"*{key}*\n{value}" header_data = [] - for k, v in self.headers.dict(by_alias=True).items(): + for k, v in self.headers.model_dump(by_alias=True).items(): field = make_field(k, v, code=True) header_data.append(field) query_data = [ - { - "type": "mrkdwn", - "text": make_field("Query Location", self.query_location), - }, - { - "type": "mrkdwn", - "text": make_field("Query Target", self.query_target, code=True), - }, + {"type": "mrkdwn", "text": make_field("Query Location", self.query_location)}, + {"type": "mrkdwn", "text": make_field("Query Target", self.query_target, code=True)}, {"type": "mrkdwn", "text": make_field("Query Type", self.query_type)}, - {"type": "mrkdwn", "text": make_field("Query VRF", self.query_vrf)}, ] source_data = [ - { - "type": "mrkdwn", - "text": make_field("Source IP", self.source, code=True), - }, + {"type": "mrkdwn", "text": make_field("Source IP", self.source, code=True)}, { "type": "mrkdwn", "text": make_field("Source Prefix", self.network.prefix, code=True), }, - { - "type": "mrkdwn", - "text": make_field("Source ASN", self.network.asn, code=True), - }, - { - "type": "mrkdwn", - "text": make_field("Source Country", self.network.country), - }, - { - "type": "mrkdwn", - "text": make_field("Source Organization", self.network.org), - }, + {"type": "mrkdwn", "text": make_field("Source ASN", self.network.asn, code=True)}, + {"type": "mrkdwn", "text": make_field("Source Country", self.network.country)}, + {"type": "mrkdwn", "text": make_field("Source Organization", self.network.org)}, ] time_fmt = self.timestamp.strftime("%Y %m %d %H:%M:%S") @@ -171,22 +147,16 @@ def make_field(key, value, code=False): payload = { "text": _WEBHOOK_TITLE, "blocks": [ - { - "type": "section", - "text": {"type": "mrkdwn", "text": f"*{time_fmt} UTC*"}, - }, + {"type": "section", "text": {"type": "mrkdwn", "text": f"*{time_fmt} UTC*"}}, {"type": "section", "fields": query_data}, {"type": "divider"}, {"type": "section", "fields": source_data}, {"type": "divider"}, { "type": "section", - "text": { - "type": "mrkdwn", - "text": "*Headers*\n" + "\n".join(header_data), - }, + "text": {"type": "mrkdwn", "text": "*Headers*\n" + "\n".join(header_data)}, }, ], } - log.debug("Created Slack webhook: {}", str(payload)) + log.bind(type="Slack", payload=str(payload)).debug("Created webhook") return payload diff --git a/hyperglass/parsing/__init__.py b/hyperglass/parsing/__init__.py deleted file mode 100644 index a3270f045..000000000 --- a/hyperglass/parsing/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Parsing utilities for command output.""" diff --git a/hyperglass/parsing/arista.py b/hyperglass/parsing/arista.py deleted file mode 100644 index e1eaabed2..000000000 --- a/hyperglass/parsing/arista.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Parse Arista JSON Response to Structured Data.""" - -# Standard Library -import json -from typing import Dict, Sequence - -# Third Party -from pydantic import ValidationError - -# Project -from hyperglass.log import log -from hyperglass.exceptions import ParsingError -from hyperglass.models.parsing.arista_eos import AristaRoute - - -def parse_arista(output: Sequence[str]) -> Dict: # noqa: C901 - """Parse a Arista BGP JSON response.""" - data = {} - - for i, response in enumerate(output): - - try: - data: Dict = json.loads(response) - - log.debug("Pre-parsed data: {}", data) - - vrf = list(data["vrfs"].keys())[0] - routes = data["vrfs"][vrf] - - log.debug("Pre-validated data: {}", routes) - - validated = AristaRoute(**routes) - serialized = validated.serialize().export_dict() - - if i == 0: - data.update(serialized) - else: - data["routes"].extend(serialized["routes"]) - - except json.JSONDecodeError as err: - log.critical("Error decoding JSON: {}", str(err)) - raise ParsingError("Error parsing response data") - - except KeyError as err: - log.critical("'{}' was not found in the response", str(err)) - raise ParsingError("Error parsing response data") - - except IndexError as err: - log.critical(str(err)) - raise ParsingError("Error parsing response data") - - except ValidationError as err: - log.critical(str(err)) - raise ParsingError(err.errors()) - - log.debug("Serialzed: {}", data) - return data diff --git a/hyperglass/parsing/common.py b/hyperglass/parsing/common.py deleted file mode 100644 index 252f47baa..000000000 --- a/hyperglass/parsing/common.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Command parsers applied to all unstructured output.""" - - -def remove_command(commands: str, output: str) -> str: - """Remove anything before the command if found in output.""" - _output = output.strip().split("\n") - - for command in commands: - for line in _output: - if command in line: - idx = _output.index(line) + 1 - _output = _output[idx:] - - return "\n".join(_output) - - -parsers = (remove_command,) diff --git a/hyperglass/parsing/juniper.py b/hyperglass/parsing/juniper.py deleted file mode 100644 index 3f2ad9bc2..000000000 --- a/hyperglass/parsing/juniper.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Parse Juniper XML Response to Structured Data.""" - -# Standard Library -import re -from typing import Dict, List, Sequence, Generator - -# Third Party -import xmltodict -from pydantic import ValidationError - -# Project -from hyperglass.log import log -from hyperglass.exceptions import ParsingError -from hyperglass.models.parsing.juniper import JuniperRoute - -REMOVE_PATTERNS = ( - # The XML response can a CLI banner appended to the end of the XML - # string. For example: - # ``` - # <rpc-reply> - # ... - # <cli> - # <banner>{master}</banner> - # </cli> - # </rpc-reply> - # - # {master} noqa: E800 - # ``` - # - # This pattern will remove anything inside braces, including the braces. - r"\{.+\}", -) - - -def clean_xml_output(output: str) -> str: - """Remove Juniper-specific patterns from output.""" - - def scrub(lines: List[str]) -> Generator[str, None, None]: - """Clean & remove each pattern from each line.""" - for pattern in REMOVE_PATTERNS: - for line in lines: - # Remove the pattern & strip extra newlines - scrubbed = re.sub(pattern, "", line.strip()) - # Only return non-empty and non-newline lines - if scrubbed and scrubbed != "\n": - yield scrubbed - - lines = scrub(output.splitlines()) - - return "\n".join(lines) - - -def parse_juniper(output: Sequence) -> Dict: # noqa: C901 - """Parse a Juniper BGP XML response.""" - data = {} - - for i, response in enumerate(output): - cleaned = clean_xml_output(response) - - try: - parsed = xmltodict.parse( - cleaned, force_list=("rt", "rt-entry", "community") - ) - - log.debug("Initially Parsed Response: \n{}", parsed) - - if "rpc-reply" in parsed.keys(): - if "xnm:error" in parsed["rpc-reply"]: - if "message" in parsed["rpc-reply"]["xnm:error"]: - err = parsed["rpc-reply"]["xnm:error"]["message"] - raise ParsingError('Error from device: "{}"', err) - - parsed_base = parsed["rpc-reply"]["route-information"] - elif "route-information" in parsed.keys(): - parsed_base = parsed["route-information"] - - if "route-table" not in parsed_base: - return data - - if "rt" not in parsed_base["route-table"]: - return data - - parsed = parsed_base["route-table"] - - validated = JuniperRoute(**parsed) - serialized = validated.serialize().export_dict() - - if i == 0: - data.update(serialized) - else: - data["routes"].extend(serialized["routes"]) - - except xmltodict.expat.ExpatError as err: - log.critical(str(err)) - raise ParsingError("Error parsing response data") from err - - except KeyError as err: - log.critical("{} was not found in the response", str(err)) - raise ParsingError("Error parsing response data") - - except ValidationError as err: - log.critical(str(err)) - raise ParsingError(err.errors()) - - return data diff --git a/hyperglass/parsing/linux.py b/hyperglass/parsing/linux.py deleted file mode 100644 index 2aba43027..000000000 --- a/hyperglass/parsing/linux.py +++ /dev/null @@ -1,81 +0,0 @@ -# flake8: noqa -# WORK IN PROGRESS - -"""Linux-style parsers for ping & traceroute.""" - -# Standard Library -import re - -# Project -from hyperglass.exceptions import ParsingError - - -def _process_numbers(numbers): - """Convert string to float or int.""" - for num in numbers: - num = float(num) - if num.is_integer(): - num = int(num) - yield num - - -def parse_linux_ping(output): - """Parse standard Linux-style ping output to structured data. - - Example: - 64 bytes from 1.1.1.1: icmp_seq=0 ttl=59 time=1.151 ms - 64 bytes from 1.1.1.1: icmp_seq=1 ttl=59 time=1.180 ms - 64 bytes from 1.1.1.1: icmp_seq=2 ttl=59 time=1.170 ms - 64 bytes from 1.1.1.1: icmp_seq=3 ttl=59 time=1.338 ms - 64 bytes from 1.1.1.1: icmp_seq=4 ttl=59 time=4.913 ms - - --- 1.1.1.1 ping statistics --- - 5 packets transmitted, 5 packets received, 0% packet loss - round-trip min/avg/max/stddev = 1.151/1.950/4.913/1.483 ms - """ - try: - # Extract target host - host = re.findall(r"^PING (.+) \(.+\): \d+ data bytes", output)[0] - - # Separate echo replies from summary info - replies, _stats = re.split(r"--- .+ ---", output) - replies = [l for l in replies.splitlines()[1:] if l] - - reply_stats = [] - for line in replies: - # Extract the numerical values from each echo reply line - bytes_seq_ttl_rtt = re.findall( - r"(\d+) bytes.+ icmp_seq=(\d+) ttl=(\d+) time=(\d+\.\d+).*", line - )[0] - - _bytes, seq, ttl, rtt = _process_numbers(bytes_seq_ttl_rtt) - - reply_stats.append( - {"bytes": _bytes, "sequence": seq, "ttl": ttl, "rtt": rtt} - ) - - stats = [l for l in _stats.splitlines() if l] - - # Extract the top summary line numbers & process - tx_rx_loss = re.findall(r"(\d+) .+, (\d+) .+, (\d+)\%.+", stats[0])[0] - tx, rx, loss = _process_numbers(tx_rx_loss) - - # Extract the bottom summary line numbers & process - rt = stats[1].split(" = ")[1] - min_max_avg = rt.split("/")[:-1] - _min, _max, _avg = _process_numbers(min_max_avg) - - return { - "host": host, - "transmitted": tx, - "received": rx, - "loss_percent": loss, - "min_rtt": _min, - "max_rtt": _max, - "avg_rtt": _avg, - "replies": reply_stats, - } - - except (KeyError, ValueError) as err: - # KeyError for empty findalls, ValueError for regex errors - raise ParsingError("Error parsing ping response: {e}", e=str(err)) diff --git a/hyperglass/parsing/mikrotik.py b/hyperglass/parsing/mikrotik.py deleted file mode 100644 index 422862c8c..000000000 --- a/hyperglass/parsing/mikrotik.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Mikrotik Output Parsing Functions.""" - -# Standard Library -import re - -END_COLUMNS = ("DISTANCE", "STATUS") - - -def parse_mikrotik(output: str): - """Parse Mikrotik output to remove garbage.""" - if output.split()[-1] in END_COLUMNS: - # Mikrotik shows the columns with no rows if there is no data. - # Rather than send back an empty table, send back an empty - # response which is handled with a warning message. - output = "" - else: - remove_lines = () - all_lines = output.splitlines() - # Starting index for rows (after the column row). - start = 1 - # Extract the column row. - column_line = " ".join(all_lines[0].split()) - - for i, line in enumerate(all_lines[1:]): - # Remove all the newline characters (which differ line to - # line) for comparison purposes. - normalized = " ".join(line.split()) - - # Remove ansii characters that aren't caught by Netmiko. - normalized = re.sub(r"\\x1b\[\S{2}\s", "", normalized) - - if column_line in normalized: - # Mikrotik often re-inserts the column row in the output, - # effectively 'starting over'. In that case, re-assign - # the column row and starting index to that point. - column_line = re.sub(r"\[\S{2}\s", "", line) - start = i + 2 - - if "[Q quit|D dump|C-z pause]" in normalized: - # Remove Mikrotik's unhelpful helpers from the output. - remove_lines += (i + 1,) - - # Combine the column row and the data rows from the starting - # index onward. - lines = [column_line, *all_lines[start:]] - - # Remove any lines marked for removal and re-join with a single - # newline character. - lines = [l for i, l in enumerate(lines) if i not in remove_lines] - output = "\n".join(lines) - - return output diff --git a/hyperglass/parsing/nos.py b/hyperglass/parsing/nos.py deleted file mode 100644 index eefed691b..000000000 --- a/hyperglass/parsing/nos.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Map NOS and Commands to Parsing Functions.""" - -# Local -from .arista import parse_arista -from .juniper import parse_juniper -from .mikrotik import parse_mikrotik - -structured_parsers = { - "juniper": { - "bgp_route": parse_juniper, - "bgp_aspath": parse_juniper, - "bgp_community": parse_juniper, - }, - "arista_eos": { - "bgp_route": parse_arista, - "bgp_aspath": parse_arista, - "bgp_community": parse_arista, - }, -} - -scrape_parsers = { - "mikrotik_routeros": { - "bgp_route": parse_mikrotik, - "bgp_aspath": parse_mikrotik, - "bgp_community": parse_mikrotik, - "ping": parse_mikrotik, - "traceroute": parse_mikrotik, - }, - "mikrotik_switchos": { - "bgp_route": parse_mikrotik, - "bgp_aspath": parse_mikrotik, - "bgp_community": parse_mikrotik, - "ping": parse_mikrotik, - "traceroute": parse_mikrotik, - }, -} diff --git a/hyperglass/parsing/test_arista.py b/hyperglass/parsing/test_arista.py deleted file mode 100644 index 218fec31d..000000000 --- a/hyperglass/parsing/test_arista.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Test Arista JSON Parsing.""" - -# Standard Library -import sys -import json -from pathlib import Path - -# Project -from hyperglass.log import log - -# Local -from .arista import parse_arista - -SAMPLE_FILE = Path(__file__).parent.parent / "models" / "parsing" / "arista_route.json" - -if __name__ == "__main__": - if len(sys.argv) == 2: - sample = sys.argv[1] - else: - with SAMPLE_FILE.open("r") as file: - sample = file.read() - - parsed = parse_arista([sample]) - log.info(json.dumps(parsed, indent=2)) - sys.exit(0) diff --git a/hyperglass/parsing/test_juniper.py b/hyperglass/parsing/test_juniper.py deleted file mode 100644 index 10d2fe989..000000000 --- a/hyperglass/parsing/test_juniper.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Test Juniper XML Parsing.""" - -# Standard Library -import sys -import json -from pathlib import Path - -# Project -from hyperglass.log import log - -# Local -from .juniper import parse_juniper - -SAMPLE_FILES = ( - Path(__file__).parent.parent / "models" / "parsing" / "juniper_route_direct.xml", - Path(__file__).parent.parent / "models" / "parsing" / "juniper_route_indirect.xml", - Path(__file__).parent.parent / "models" / "parsing" / "juniper_route_aspath.xml", -) - - -@log.catch -def run(): - """Run tests.""" - samples = () - if len(sys.argv) == 2: - samples += (sys.argv[1],) - else: - for sample_file in SAMPLE_FILES: - with sample_file.open("r") as file: - samples += (file.read(),) - - for sample in samples: - parsed = parse_juniper([sample]) - log.info(json.dumps(parsed, indent=2)) - sys.exit(0) - - -if __name__ == "__main__": - run() diff --git a/hyperglass/plugins/__init__.py b/hyperglass/plugins/__init__.py new file mode 100644 index 000000000..52933a104 --- /dev/null +++ b/hyperglass/plugins/__init__.py @@ -0,0 +1,18 @@ +"""hyperglass Plugins.""" + +# Local +from .main import register_plugin, init_builtin_plugins +from ._input import InputPlugin, InputPluginValidationReturn +from ._output import OutputType, OutputPlugin +from ._manager import InputPluginManager, OutputPluginManager + +__all__ = ( + "init_builtin_plugins", + "InputPlugin", + "InputPluginManager", + "InputPluginValidationReturn", + "OutputPlugin", + "OutputPluginManager", + "OutputType", + "register_plugin", +) diff --git a/hyperglass/plugins/_base.py b/hyperglass/plugins/_base.py new file mode 100644 index 000000000..ea4e2e13f --- /dev/null +++ b/hyperglass/plugins/_base.py @@ -0,0 +1,119 @@ +"""Base Plugin Definition.""" + +# Standard Library +import typing as t +from abc import ABC +from inspect import Signature + +# Third Party +from pydantic import BaseModel, PrivateAttr + +# Project +from hyperglass.log import log as _logger + +if t.TYPE_CHECKING: + # Third Party + from loguru import Logger + +PluginType = t.Union[t.Literal["output"], t.Literal["input"]] +SupportedMethod = t.TypeVar("SupportedMethod") + + +class HyperglassPlugin(BaseModel, ABC): + """Plugin to interact with device command output.""" + + _hyperglass_builtin: bool = PrivateAttr(False) + _type: t.ClassVar[str] + name: str + common: bool = False + ref: t.Optional[str] = None + log: t.ClassVar["Logger"] = _logger + + @property + def _signature(self) -> Signature: + """Get this instance's class signature.""" + return self.__class__.__signature__ + + def __eq__(self, other: "HyperglassPlugin") -> bool: + """Other plugin is equal to this plugin.""" + if hasattr(other, "_signature"): + return other and self._signature == other._signature + return False + + def __ne__(self, other: "HyperglassPlugin") -> bool: + """Other plugin is not equal to this plugin.""" + return not self.__eq__(other) + + def __hash__(self) -> int: + """Create a hashed representation of this plugin's name.""" + return hash(self._signature) + + def __str__(self) -> str: + """Represent plugin by its name.""" + return self.name + + @classmethod + def __init_subclass__(cls, **kwargs: t.Any) -> None: + """Initialize plugin object.""" + name = kwargs.pop("name", None) or cls.__name__ + cls.name = name + super().__init_subclass__() + + def __init__(self, **kwargs: t.Any) -> None: + """Initialize plugin instance.""" + name = kwargs.pop("name", None) or self.__class__.__name__ + super().__init__(name=name, **kwargs) + + def __rich_console__(self, *_, **__): + """Create a rich representation of this plugin for the hyperglass CLI.""" + + # Third Party + from rich.text import Text + from rich.panel import Panel + from rich.table import Table + from rich.pretty import Pretty + + table = Table.grid(padding=(0, 1), expand=False) + table.add_column(justify="right") + + data = {"builtin": True if self._hyperglass_builtin else False} + data.update( + { + attr: getattr(self, attr) + for attr in ("name", "common", "directives", "platforms") + if hasattr(self, attr) + } + ) + data = {k: data[k] for k in sorted(data.keys())} + for key, value in data.items(): + table.add_row( + Text.assemble((key, "inspect.attr"), (" =", "inspect.equals")), Pretty(value) + ) + + yield Panel( + table, + expand=False, + title=f"[bold magenta]{self.name}", + title_align="left", + subtitle=f"[bold cornflower_blue]{self._type.capitalize()} Plugin", + subtitle_align="right", + padding=(1, 3), + ) + + +class DirectivePlugin(BaseModel): + """Plugin associated with directives. + + Should always be subclassed with `HyperglassPlugin`. + """ + + directives: t.Sequence[str] = () + + +class PlatformPlugin(BaseModel): + """Plugin associated with specific device platform. + + Should always be subclassed with `HyperglassPlugin`. + """ + + platforms: t.Sequence[str] = () diff --git a/hyperglass/plugins/_builtin/__init__.py b/hyperglass/plugins/_builtin/__init__.py new file mode 100644 index 000000000..5e87f5b11 --- /dev/null +++ b/hyperglass/plugins/_builtin/__init__.py @@ -0,0 +1,18 @@ +"""Built-in hyperglass plugins.""" + +# Local +from .bgp_route_frr import BGPRoutePluginFrr +from .remove_command import RemoveCommand +from .bgp_route_arista import BGPRoutePluginArista +from .bgp_route_huawei import BGPRoutePluginHuawei +from .bgp_route_juniper import BGPRoutePluginJuniper +from .mikrotik_garbage_output import MikrotikGarbageOutput + +__all__ = ( + "BGPRoutePluginArista", + "BGPRoutePluginFrr", + "BGPRoutePluginJuniper", + "BGPRoutePluginHuawei", + "MikrotikGarbageOutput", + "RemoveCommand", +) diff --git a/hyperglass/plugins/_builtin/bgp_community.py b/hyperglass/plugins/_builtin/bgp_community.py new file mode 100644 index 000000000..5573d9e35 --- /dev/null +++ b/hyperglass/plugins/_builtin/bgp_community.py @@ -0,0 +1,112 @@ +"""Remove anything before the command if found in output.""" + +# Standard Library +import typing as t +from ipaddress import ip_address + +# Third Party +from pydantic import PrivateAttr + +# Project +from hyperglass.state.hooks import use_state + +# Local +from .._input import InputPlugin + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.api.query import Query + + # Local + from .._input import InputPluginValidationReturn + +_32BIT = 0xFFFFFFFF +_16BIT = 0xFFFF +EXTENDED_TYPES = ("target", "origin") + + +def check_decimal(value: str, size: int) -> bool: + """Verify the value is a 32 bit number.""" + try: + return abs(int(value)) <= size + except Exception: + return False + + +def check_string(value: str) -> bool: + """Verify part of a community is an IPv4 address, per RFC4360.""" + try: + addr = ip_address(value) + return addr.version == 4 + except ValueError: + return False + + +def validate_decimal(value: str) -> bool: + """Verify a community is a 32 bit decimal number.""" + return check_decimal(value, _32BIT) + + +def validate_new_format(value: str) -> bool: + """Verify a community matches "new" format, standard or extended.""" + if ":" in value: + parts = [p for p in value.split(":") if p] + if len(parts) == 3: + if parts[0].lower() not in EXTENDED_TYPES: + # Handle extended community format with `target:` or `origin:` prefix. + return False + # Remove type from parts list after it's been validated. + parts = parts[1:] + if len(parts) != 2: + # Only allow two sections in new format, e.g. 65000:1 + return False + + one, two = parts + + if all((check_decimal(one, _16BIT), check_decimal(two, _16BIT))): + # Handle standard format, e.g. `65000:1` + return True + if all((check_decimal(one, _16BIT), check_decimal(two, _32BIT))): + # Handle extended format, e.g. `65000:4294967295` + return True + if all((check_string(one), check_decimal(two, _16BIT))): + # Handle IP address format, e.g. `192.0.2.1:65000` + return True + + return False + + +def validate_large_community(value: str) -> bool: + """Verify a community matches "large" format. E.g., `65000:65001:65002`.""" + if ":" in value: + parts = [p for p in value.split(":") if p] + if len(parts) != 3: + return False + for part in parts: + if not check_decimal(part, _32BIT): + # Each member must be a 32 bit number. + return False + return True + return False + + +class ValidateBGPCommunity(InputPlugin): + """Validate a BGP community string.""" + + _hyperglass_builtin: bool = PrivateAttr(True) + + def validate(self, query: "Query") -> "InputPluginValidationReturn": + """Ensure an input query target is a valid BGP community.""" + + params = use_state("params") + + if not isinstance(query.query_target, str): + return None + + for validator in (validate_decimal, validate_new_format, validate_large_community): + result = validator(query.query_target) + if result is True: + return True + + self.failure_reason = params.messages.invalid_input + return False diff --git a/hyperglass/plugins/_builtin/bgp_route_arista.py b/hyperglass/plugins/_builtin/bgp_route_arista.py new file mode 100644 index 000000000..b65310a72 --- /dev/null +++ b/hyperglass/plugins/_builtin/bgp_route_arista.py @@ -0,0 +1,92 @@ +"""Parse Arista JSON Response to Structured Data.""" + +# Standard Library +import json +import typing as t + +# Third Party +from pydantic import PrivateAttr, ValidationError + +# Project +from hyperglass.log import log +from hyperglass.exceptions.private import ParsingError +from hyperglass.models.parsing.arista_eos import AristaBGPTable + +# Local +from .._output import OutputPlugin + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.data import OutputDataModel + from hyperglass.models.api.query import Query + + # Local + from .._output import OutputType + + +def parse_arista(output: t.Sequence[str]) -> "OutputDataModel": + """Parse a Arista BGP JSON response.""" + result = None + + _log = log.bind(plugin=BGPRoutePluginArista.__name__) + + for response in output: + try: + parsed: t.Dict = json.loads(response) + + _log.debug("Pre-parsed data", data=parsed) + + vrf = list(parsed["vrfs"].keys())[0] + routes = parsed["vrfs"][vrf] + + validated = AristaBGPTable(**routes) + bgp_table = validated.bgp_table() + + if result is None: + result = bgp_table + else: + result += bgp_table + + except json.JSONDecodeError as err: + _log.bind(error=str(err)).critical("Failed to decode JSON") + raise ParsingError("Error parsing response data") from err + + except KeyError as err: + _log.bind(key=str(err)).critical("Missing required key in response") + raise ParsingError("Error parsing response data") from err + + except IndexError as err: + _log.critical(err) + raise ParsingError("Error parsing response data") from err + + except ValidationError as err: + _log.critical(err) + raise ParsingError(err.errors()) from err + + return result + + +class BGPRoutePluginArista(OutputPlugin): + """Coerce a Arista route table in JSON format to a standard BGP Table structure.""" + + _hyperglass_builtin: bool = PrivateAttr(True) + platforms: t.Sequence[str] = ("arista_eos",) + directives: t.Sequence[str] = ( + "__hyperglass_arista_eos_bgp_route_table__", + "__hyperglass_arista_eos_bgp_aspath_table__", + "__hyperglass_arista_eos_bgp_community_table__", + ) + + def process(self, *, output: "OutputType", query: "Query") -> "OutputType": + """Parse Arista response if data is a string (and is therefore unparsed).""" + should_process = all( + ( + isinstance(output, (list, tuple)), + query.device.platform in self.platforms, + query.device.structured_output is True, + query.device.has_directives(*self.directives), + ) + ) + if should_process: + return parse_arista(output) + return output diff --git a/hyperglass/plugins/_builtin/bgp_route_frr.py b/hyperglass/plugins/_builtin/bgp_route_frr.py new file mode 100644 index 000000000..7130b2057 --- /dev/null +++ b/hyperglass/plugins/_builtin/bgp_route_frr.py @@ -0,0 +1,85 @@ +"""Parse FRR JSON Response to Structured Data.""" + +# Standard Library +import json +import typing as t + +# Third Party +from pydantic import PrivateAttr, ValidationError + +# Project +from hyperglass.log import log +from hyperglass.exceptions.private import ParsingError +from hyperglass.models.parsing.frr import FRRBGPTable + +# Local +from .._output import OutputPlugin + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.data import OutputDataModel + from hyperglass.models.api.query import Query + + # Local + from .._output import OutputType + + +def parse_frr(output: t.Sequence[str]) -> "OutputDataModel": + """Parse a FRR BGP JSON response.""" + result = None + + _log = log.bind(plugin=BGPRoutePluginFrr.__name__) + + for response in output: + try: + parsed: t.Dict = json.loads(response) + + _log.debug("Pre-parsed data", data=parsed) + + validated = FRRBGPTable(**parsed) + bgp_table = validated.bgp_table() + + if result is None: + result = bgp_table + else: + result += bgp_table + + except json.JSONDecodeError as err: + _log.bind(error=str(err)).critical("Failed to decode JSON") + raise ParsingError("Error parsing response data") from err + + except KeyError as err: + _log.bind(key=str(err)).critical("Missing required key in response") + raise ParsingError("Error parsing response data") from err + + except IndexError as err: + _log.critical(err) + raise ParsingError("Error parsing response data") from err + + except ValidationError as err: + _log.critical(err) + raise ParsingError(err.errors()) from err + + return result + + +class BGPRoutePluginFrr(OutputPlugin): + """Coerce a FRR route table in JSON format to a standard BGP Table structure.""" + + _hyperglass_builtin: bool = PrivateAttr(True) + platforms: t.Sequence[str] = ("frr",) + directives: t.Sequence[str] = ("__hyperglass_frr_bgp_route_table__",) + + def process(self, *, output: "OutputType", query: "Query") -> "OutputType": + """Parse FRR response if data is a string (and is therefore unparsed).""" + should_process = all( + ( + isinstance(output, (list, tuple)), + query.device.platform in self.platforms, + query.device.structured_output is True, + query.device.has_directives(*self.directives), + ) + ) + if should_process: + return parse_frr(output) + return output diff --git a/hyperglass/plugins/_builtin/bgp_route_huawei.py b/hyperglass/plugins/_builtin/bgp_route_huawei.py new file mode 100644 index 000000000..172d9068f --- /dev/null +++ b/hyperglass/plugins/_builtin/bgp_route_huawei.py @@ -0,0 +1,47 @@ +# Standard Library +import typing as t +from ipaddress import ip_network + +# Third Party +from pydantic import PrivateAttr + +# Local +from .._input import InputPlugin + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.api.query import Query + +InputPluginTransformReturn = t.Union[t.Sequence[str], str] + + +class BGPRoutePluginHuawei(InputPlugin): + _hyperglass_builtin: bool = PrivateAttr(True) + platforms: t.Sequence[str] = ( + "huawei", + "huawei_vrpv8", + ) + directives: t.Sequence[str] = ("__hyperglass_huawei_bgp_route__",) + """ + Huawei BGP Route Input Plugin + + This plugin transforms a query target into a network address and prefix length + ex.: 192.0.2.0/24 -> 192.0.2.0 24 + ex.: 2001:db8::/32 -> 2001:db8:: 32 + """ + + def transform(self, query: "Query") -> InputPluginTransformReturn: + target = query.query_target + + if not target or not isinstance(target, list) or len(target) == 0: + return None + + target = target[0].strip() + + # Check for the / in the query target + if target.find("/") == -1: + return target + + target_network = ip_network(target) + + return f"{target_network.network_address!s} {target_network.prefixlen!s}" diff --git a/hyperglass/plugins/_builtin/bgp_route_juniper.py b/hyperglass/plugins/_builtin/bgp_route_juniper.py new file mode 100644 index 000000000..eb5d38e99 --- /dev/null +++ b/hyperglass/plugins/_builtin/bgp_route_juniper.py @@ -0,0 +1,143 @@ +"""Coerce a Juniper route table in XML format to a standard BGP Table structure.""" + +# Standard Library +import re +from typing import TYPE_CHECKING, List, Sequence, Generator + +# Third Party +import xmltodict # type: ignore +from pydantic import PrivateAttr, ValidationError + +# Project +from hyperglass.log import log +from hyperglass.exceptions.private import ParsingError +from hyperglass.models.parsing.juniper import JuniperBGPTable + +# Local +from .._output import OutputPlugin + +if TYPE_CHECKING: + # Standard Library + from collections import OrderedDict + + # Project + from hyperglass.models.data import OutputDataModel + from hyperglass.models.api.query import Query + + # Local + from .._output import OutputType + + +REMOVE_PATTERNS = ( + # The XML response can a CLI banner appended to the end of the XML + # string. For example: + # ``` + # <rpc-reply> + # ... + # <cli> + # <banner>{master}</banner> + # </cli> + # </rpc-reply> + # + # {master} noqa: E800 + # ``` + # + # This pattern will remove anything inside braces, including the braces. + r"\{.+\}", +) + + +def clean_xml_output(output: str) -> str: + """Remove Juniper-specific patterns from output.""" + + def scrub(lines: List[str]) -> Generator[str, None, None]: + """Clean & remove each pattern from each line.""" + for pattern in REMOVE_PATTERNS: + for line in lines: + # Remove the pattern & strip extra newlines + scrubbed = re.sub(pattern, "", line.strip()) + # Only return non-empty and non-newline lines + if scrubbed and scrubbed != "\n": + yield scrubbed + + lines = scrub(output.splitlines()) + + return "\n".join(lines) + + +def parse_juniper(output: Sequence[str]) -> "OutputDataModel": # noqa: C901 + """Parse a Juniper BGP XML response.""" + result = None + + _log = log.bind(plugin=BGPRoutePluginJuniper.__name__) + for response in output: + cleaned = clean_xml_output(response) + + try: + parsed: "OrderedDict" = xmltodict.parse( + cleaned, force_list=("rt", "rt-entry", "community") + ) + if "rpc-reply" in parsed.keys(): + if "xnm:error" in parsed["rpc-reply"]: + if "message" in parsed["rpc-reply"]["xnm:error"]: + err = parsed["rpc-reply"]["xnm:error"]["message"] + raise ParsingError('Error from device: "{}"', err) + + parsed_base = parsed["rpc-reply"]["route-information"] + elif "route-information" in parsed.keys(): + parsed_base = parsed["route-information"] + + if "route-table" not in parsed_base: + return result + + if "rt" not in parsed_base["route-table"]: + return result + + parsed = parsed_base["route-table"] + validated = JuniperBGPTable(**parsed) + bgp_table = validated.bgp_table() + + if result is None: + result = bgp_table + else: + result += bgp_table + + except xmltodict.expat.ExpatError as err: + _log.bind(error=str(err)).critical("Failed to decode XML") + raise ParsingError("Error parsing response data") from err + + except KeyError as err: + _log.bind(key=str(err)).critical("Missing required key in response") + raise ParsingError("{key} was not found in the response", key=str(err)) from err + + except ValidationError as err: + _log.critical(err) + raise ParsingError(err) from err + + return result + + +class BGPRoutePluginJuniper(OutputPlugin): + """Coerce a Juniper route table in XML format to a standard BGP Table structure.""" + + _hyperglass_builtin: bool = PrivateAttr(True) + platforms: Sequence[str] = ("juniper",) + directives: Sequence[str] = ( + "__hyperglass_juniper_bgp_route_table__", + "__hyperglass_juniper_bgp_aspath_table__", + "__hyperglass_juniper_bgp_community_table__", + ) + + def process(self, *, output: "OutputType", query: "Query") -> "OutputType": + """Parse Juniper response if data is a string (and is therefore unparsed).""" + should_process = all( + ( + isinstance(output, (list, tuple)), + query.device.platform in self.platforms, + query.device.structured_output is True, + query.device.has_directives(*self.directives), + ) + ) + if should_process: + return parse_juniper(output) + return output diff --git a/hyperglass/plugins/_builtin/mikrotik_garbage_output.py b/hyperglass/plugins/_builtin/mikrotik_garbage_output.py new file mode 100644 index 000000000..8718604c1 --- /dev/null +++ b/hyperglass/plugins/_builtin/mikrotik_garbage_output.py @@ -0,0 +1,82 @@ +"""Remove anything before the command if found in output.""" + +# Standard Library +import re +import typing as t + +# Third Party +from pydantic import PrivateAttr + +# Project +from hyperglass.types import Series + +# Local +from .._output import OutputType, OutputPlugin + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.api.query import Query + + +class MikrotikGarbageOutput(OutputPlugin): + """Parse Mikrotik output to remove garbage.""" + + _hyperglass_builtin: bool = PrivateAttr(True) + platforms: t.Sequence[str] = ("mikrotik_routeros", "mikrotik_switchos") + directives: t.Sequence[str] = ( + "__hyperglass_mikrotik_bgp_aspath__", + "__hyperglass_mikrotik_bgp_community__", + "__hyperglass_mikrotik_bgp_route__", + "__hyperglass_mikrotik_ping__", + "__hyperglass_mikrotik_traceroute__", + ) + + def process(self, *, output: OutputType, query: "Query") -> Series[str]: + """Parse Mikrotik output to remove garbage.""" + + result = () + + for each_output in output: + if len(each_output) != 0: + if each_output.split()[-1] in ("DISTANCE", "STATUS"): + # Mikrotik shows the columns with no rows if there is no data. + # Rather than send back an empty table, send back an empty + # response which is handled with a warning message. + each_output = "" + else: + remove_lines = () + all_lines = each_output.splitlines() + # Starting index for rows (after the column row). + start = 1 + # Extract the column row. + column_line = " ".join(all_lines[0].split()) + + for i, line in enumerate(all_lines[1:]): + # Remove all the newline characters (which differ line to + # line) for comparison purposes. + normalized = " ".join(line.split()) + + # Remove ansii characters that aren't caught by Netmiko. + normalized = re.sub(r"\\x1b\[\S{2}\s", "", normalized) + + if column_line in normalized: + # Mikrotik often re-inserts the column row in the output, + # effectively 'starting over'. In that case, re-assign + # the column row and starting index to that point. + column_line = re.sub(r"\[\S{2}\s", "", line) + start = i + 2 + + if "[Q quit|D dump|C-z pause]" in normalized: + # Remove Mikrotik's unhelpful helpers from the output. + remove_lines += (i + 1,) + + # Combine the column row and the data rows from the starting + # index onward. + lines = [column_line, *all_lines[start:]] + + # Remove any lines marked for removal and re-join with a single + # newline character. + lines = [line for idx, line in enumerate(lines) if idx not in remove_lines] + result += ("\n".join(lines),) + + return result diff --git a/hyperglass/plugins/_builtin/remove_command.py b/hyperglass/plugins/_builtin/remove_command.py new file mode 100644 index 000000000..bc301cad3 --- /dev/null +++ b/hyperglass/plugins/_builtin/remove_command.py @@ -0,0 +1,42 @@ +"""Remove anything before the command if found in output.""" + +# Standard Library +from typing import TYPE_CHECKING, Sequence + +# Third Party +from pydantic import PrivateAttr + +# Project +from hyperglass.util.typing import is_series + +# Local +from .._output import OutputType, OutputPlugin + +if TYPE_CHECKING: + # Project + from hyperglass.models.api.query import Query + + +class RemoveCommand(OutputPlugin): + """Remove anything before the command if found in output.""" + + _hyperglass_builtin: bool = PrivateAttr(True) + + def process(self, *, output: OutputType, query: "Query") -> Sequence[str]: + """Remove anything before the command if found in output.""" + + def _remove_command(output_in: str) -> str: + output_out = output_in.strip().split("\n") + + for command in query.device.directive_commands: + for line in output_out: + if command in line: + idx = output_out.index(line) + 1 + output_out = output_out[idx:] + + return "\n".join(output_out) + + if is_series(output): + return tuple(_remove_command(o) for o in output) + + return output diff --git a/hyperglass/plugins/_input.py b/hyperglass/plugins/_input.py new file mode 100644 index 000000000..e2316bf27 --- /dev/null +++ b/hyperglass/plugins/_input.py @@ -0,0 +1,30 @@ +"""Input validation plugins.""" + +# Standard Library +import typing as t + +# Local +from ._base import DirectivePlugin, HyperglassPlugin + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.api.query import Query + + +InputPluginValidationReturn = t.Union[None, bool] +InputPluginTransformReturn = t.Union[t.Sequence[str], str] + + +class InputPlugin(HyperglassPlugin, DirectivePlugin): + """Plugin to validate user input prior to running commands.""" + + _type = "input" + failure_reason: t.Optional[str] = None + + def validate(self, query: "Query") -> InputPluginValidationReturn: + """Validate input from hyperglass UI/API.""" + return None + + def transform(self, query: "Query") -> InputPluginTransformReturn: + """Transform query target prior to running commands.""" + return query.query_target diff --git a/hyperglass/plugins/_manager.py b/hyperglass/plugins/_manager.py new file mode 100644 index 000000000..c0194ac05 --- /dev/null +++ b/hyperglass/plugins/_manager.py @@ -0,0 +1,193 @@ +"""Plugin manager definition.""" + +# Standard Library +import typing as t +from inspect import isclass + +# Project +from hyperglass.log import log +from hyperglass.state import use_state +from hyperglass.exceptions.private import PluginError, InputValidationError + +# Local +from ._base import PluginType, HyperglassPlugin +from ._input import InputPlugin, InputPluginTransformReturn, InputPluginValidationReturn +from ._output import OutputType, OutputPlugin + +if t.TYPE_CHECKING: + # Project + from hyperglass.state import HyperglassState + from hyperglass.models.api.query import Query + +PluginT = t.TypeVar("PluginT", bound=HyperglassPlugin) + + +class PluginManager(t.Generic[PluginT]): + """Manage all plugins.""" + + _type: PluginType + _state: "HyperglassState" + _index: int = 0 + _cache_key: str + + def __init__(self: "PluginManager") -> None: + """Initialize plugin manager.""" + self._state = use_state() + self._cache_key = f"hyperglass.plugins.{self._type}" + + def __init_subclass__(cls: "PluginManager", **kwargs: PluginType) -> None: + """Set this plugin manager's type on subclass initialization.""" + _type = kwargs.get("type", None) or cls._type + if _type is None: + raise PluginError("Plugin '{}' is missing a 'type', keyword argument", repr(cls)) + cls._type = _type + return super().__init_subclass__() + + def __iter__(self: "PluginManager") -> "PluginManager": + """Plugin manager iterator.""" + return self + + def __next__(self: "PluginManager") -> PluginT: + """Plugin manager iteration.""" + if self._index <= len(self.plugins()): + result = self.plugins()[self._index - 1] + self._index += 1 + return result + self._index = 0 + raise StopIteration + + def plugins(self: "PluginManager", *, builtins: bool = True) -> t.List[PluginT]: + """Get all plugins, with built-in plugins last.""" + plugins = self._state.plugins(self._type) + + if builtins is False: + plugins = [p for p in plugins if p._hyperglass_builtin is False] + + # Sort plugins by their name attribute, which is the name of the class by default. + sorted_by_name = sorted(plugins, key=lambda p: str(p)) + + # Sort with built-in plugins last. + return sorted( + sorted_by_name, + key=lambda p: -1 if p._hyperglass_builtin else 1, + reverse=True, + ) + + @property + def name(self: PluginT) -> str: + """Get this plugin manager's name.""" + return self.__class__.__name__ + + def methods(self: "PluginManager", name: str) -> t.Generator[t.Callable, None, None]: + """Get methods of all registered plugins matching `name`.""" + for plugin in self.plugins(): + if hasattr(plugin, name): + method = getattr(plugin, name) + if callable(method): + yield method + + def execute(self, *args, **kwargs) -> None: + """Gather all plugins and execute in order.""" + raise NotImplementedError(f"Plugin Manager '{self.name}' is missing an 'execute()' method.") + + def reset(self: "PluginManager") -> None: + """Remove all plugins.""" + self._index = 0 + self._state.reset_plugins(self._type) + + def unregister(self: "PluginManager", plugin: PluginT) -> None: + """Remove a plugin from currently active plugins.""" + if isclass(plugin): + if issubclass(plugin, HyperglassPlugin): + self._state.remove_plugin(self._type, plugin) + + return + raise PluginError("Plugin '{}' is not a valid hyperglass plugin", repr(plugin)) + + def register(self: "PluginManager", plugin: PluginT, *args: t.Any, **kwargs: t.Any) -> None: + """Add a plugin to currently active plugins.""" + # Create a set of plugins so duplicate plugins are not mistakenly added. + try: + if issubclass(plugin, HyperglassPlugin): + instance = plugin(*args, **kwargs) + self._state.add_plugin(self._type, instance) + _log = log.bind(type=self._type, name=instance.name) + if instance._hyperglass_builtin is True: + _log.debug("Registered built-in plugin") + else: + _log.info("Registered plugin") + return + except TypeError: + raise PluginError( # noqa: B904 + "Plugin '{p}' has not defined a required method. " + "Please consult the hyperglass documentation.", + p=repr(plugin), + ) + raise PluginError("Plugin '{p}' is not a valid hyperglass plugin", p=repr(plugin)) + + +class InputPluginManager(PluginManager[InputPlugin], type="input"): + """Manage Input Validation Plugins.""" + + def _gather_plugins( + self: "InputPluginManager", query: "Query" + ) -> t.Generator[InputPlugin, None, None]: + for plugin in self.plugins(builtins=True): + if plugin.directives and query.directive.id in plugin.directives: + yield plugin + if plugin.ref in query.directive.plugins: + yield plugin + if plugin.common is True: + yield plugin + + def validate(self: "InputPluginManager", query: "Query") -> InputPluginValidationReturn: + """Execute all input validation plugins. + + If any plugin returns `False`, execution is halted. + """ + result = None + for plugin in self._gather_plugins(query): + result = plugin.validate(query) + result_test = "valid" if result is True else "invalid" if result is False else "none" + log.bind(name=plugin.name, result=result_test).debug("Input Plugin Validation") + if result is False: + raise InputValidationError( + error="No matched validation rules", target=query.query_target + ) + if result is True: + return result + return result + + def transform(self: "InputPluginManager", *, query: "Query") -> InputPluginTransformReturn: + """Execute all input transformation plugins.""" + result = query.query_target + for plugin in self._gather_plugins(query): + result = plugin.transform(query=query.summary()) + log.bind(name=plugin.name, result=repr(result)).debug("Input Plugin Transform") + return result + + +class OutputPluginManager(PluginManager[OutputPlugin], type="output"): + """Manage Output Processing Plugins.""" + + def execute(self: "OutputPluginManager", *, output: OutputType, query: "Query") -> OutputType: + """Execute all output parsing plugins. + + The result of each plugin is passed to the next plugin. + """ + result = output + directives = ( + plugin + for plugin in self.plugins() + if query.directive.id in plugin.directives and query.device.platform in plugin.platforms + ) + common = (plugin for plugin in self.plugins() if plugin.common is True) + for plugin in (*directives, *common): + log.bind(plugin=plugin.name, value=result).debug("Output Plugin Starting Value") + result = plugin.process(output=result, query=query) + log.bind(plugin=plugin.name, value=result).debug("Output Plugin Ending Value") + + if result is False: + return result + # Pass the result of each plugin to the next plugin. + return result diff --git a/hyperglass/plugins/_output.py b/hyperglass/plugins/_output.py new file mode 100644 index 000000000..8f14dd453 --- /dev/null +++ b/hyperglass/plugins/_output.py @@ -0,0 +1,29 @@ +"""Device output plugins.""" + +# Standard Library +from typing import TYPE_CHECKING, Union + +# Project +from hyperglass.log import log +from hyperglass.types import Series + +# Local +from ._base import PlatformPlugin, DirectivePlugin, HyperglassPlugin + +if TYPE_CHECKING: + # Project + from hyperglass.models.data import OutputDataModel + from hyperglass.models.api.query import Query + +OutputType = Union["OutputDataModel", Series[str]] + + +class OutputPlugin(HyperglassPlugin, DirectivePlugin, PlatformPlugin): + """Plugin to interact with device command output.""" + + _type = "output" + + def process(self, *, output: OutputType, query: "Query") -> OutputType: + """Process or manipulate output from a device.""" + log.warning("Output plugin has not implemented a 'process()' method", plugin=self.name) + return output diff --git a/hyperglass/plugins/external/.gitignore b/hyperglass/plugins/external/.gitignore new file mode 100644 index 000000000..9beee4260 --- /dev/null +++ b/hyperglass/plugins/external/.gitignore @@ -0,0 +1,4 @@ +# Ignore all files as they're imported at runtime. +* +!__init__.py +!.gitignore diff --git a/hyperglass/plugins/external/__init__.py b/hyperglass/plugins/external/__init__.py new file mode 100644 index 000000000..78c58ebb0 --- /dev/null +++ b/hyperglass/plugins/external/__init__.py @@ -0,0 +1 @@ +"""Container for external plugins. External plugins are copied here on registration.""" diff --git a/hyperglass/plugins/main.py b/hyperglass/plugins/main.py new file mode 100644 index 000000000..abc83fb6e --- /dev/null +++ b/hyperglass/plugins/main.py @@ -0,0 +1,69 @@ +"""Register all plugins.""" + +# Standard Library +import sys +import shutil +import typing as t +from inspect import isclass, getmembers +from pathlib import Path +from importlib.util import module_from_spec, spec_from_file_location + +# Local +from . import _builtin +from ._input import InputPlugin +from ._output import OutputPlugin +from ._manager import InputPluginManager, OutputPluginManager + + +def _is_class(module: t.Any, obj: object) -> bool: + if isclass(obj): + # Get the object's containing module name. + obj_module_name: str = getattr(obj, "__module__", "") + # Get the module's name. + module_name: str = getattr(module, "__name__", None) + # Only validate objects that are members of the module. + return module_name in obj_module_name + return False + + +def _register_from_module(module: t.Any, **kwargs: t.Any) -> t.Tuple[str, ...]: + """Register defined classes from the module.""" + failures = () + defs = getmembers(module, lambda o: _is_class(module, o)) + sys.modules[module.__name__] = module + for name, plugin in defs: + if issubclass(plugin, OutputPlugin): + manager = OutputPluginManager() + elif issubclass(plugin, InputPlugin): + manager = InputPluginManager() + else: + failures += (name,) + continue + manager.register(plugin, **kwargs) + return failures + + +def _module_from_file(file: Path) -> t.Any: + """Import a plugin module from its file Path object.""" + plugins_dir = Path(__file__).parent / "external" + dst = plugins_dir / f"imported_{file.name}" + shutil.copy2(file, dst) + name = f"imported_{file.name.split('.')[0]}" + spec = spec_from_file_location(f"hyperglass.plugins.external.{name}", dst) + module = module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def init_builtin_plugins() -> None: + """Initialize all built-in plugins.""" + _register_from_module(_builtin) + + +def register_plugin(plugin_file: Path, **kwargs) -> t.Tuple[str, ...]: + """Register an external plugin by file path.""" + if plugin_file.exists(): + module = _module_from_file(plugin_file) + results = _register_from_module(module, ref=plugin_file.stem, **kwargs) + return results + raise FileNotFoundError(str(plugin_file)) diff --git a/hyperglass/plugins/tests/__init__.py b/hyperglass/plugins/tests/__init__.py new file mode 100644 index 000000000..30d059790 --- /dev/null +++ b/hyperglass/plugins/tests/__init__.py @@ -0,0 +1 @@ +"""Plugin tests.""" diff --git a/hyperglass/plugins/tests/_fixtures.py b/hyperglass/plugins/tests/_fixtures.py new file mode 100644 index 000000000..f054a3c17 --- /dev/null +++ b/hyperglass/plugins/tests/_fixtures.py @@ -0,0 +1,7 @@ +# Project +from hyperglass.models.config.devices import Device + + +class MockDevice(Device): + def has_directives(self, *_: str) -> bool: + return True diff --git a/hyperglass/plugins/tests/test_bgp_community.py b/hyperglass/plugins/tests/test_bgp_community.py new file mode 100644 index 000000000..3541244aa --- /dev/null +++ b/hyperglass/plugins/tests/test_bgp_community.py @@ -0,0 +1,66 @@ +"""Test BGP Community validation.""" + +# Standard Library +import typing as t + +# Third Party +import pytest + +# Project +from hyperglass.state import use_state +from hyperglass.models.config.params import Params + +# Local +from .._builtin.bgp_community import ValidateBGPCommunity + +if t.TYPE_CHECKING: + # Project + from hyperglass.state import HyperglassState + + +CHECKS = ( + ("32768", True), + ("65000:1", True), + ("65000:4294967296", False), + ("4294967295:65000", False), + ("192.0.2.1:65000", True), + ("65000:192.0.2.1", False), + ("target:65000:1", True), + ("origin:65001:1", True), + ("wrong:65000:1", False), + ("65000:65001:65002", True), + ("4294967295:4294967294:4294967293", True), + ("65000:4294967295:1", True), + ("65000:192.0.2.1:1", False), + ("gibberish", False), + ("192.0.2.1", False), + (True, None), + (type("FakeClass", (), {}), None), +) + + +@pytest.fixture +def params(): + return {} + + +@pytest.fixture +def state(*, params: t.Dict[str, t.Any]) -> t.Generator["HyperglassState", None, None]: + """Test fixture to initialize Redis store.""" + _state = use_state() + _params = Params(**params) + + with _state.cache.pipeline() as pipeline: + pipeline.set("params", _params) + + yield _state + _state.clear() + + +def test_bgp_community(state): + plugin = ValidateBGPCommunity() + + for value, expected in CHECKS: + query = type("Query", (), {"query_target": value}) + result = plugin.validate(query) + assert result == expected, f"Invalid value {value!r}" diff --git a/hyperglass/plugins/tests/test_bgp_route_arista.py b/hyperglass/plugins/tests/test_bgp_route_arista.py new file mode 100644 index 000000000..dbb02bc90 --- /dev/null +++ b/hyperglass/plugins/tests/test_bgp_route_arista.py @@ -0,0 +1,55 @@ +"""Arista BGP Route Parsing Tests.""" + +# flake8: noqa +# Standard Library +from pathlib import Path + +# Third Party +import pytest + +# Project +from hyperglass.models.config.devices import Device +from hyperglass.models.data.bgp_route import BGPRouteTable + +# Local +from ._fixtures import MockDevice +from .._builtin.bgp_route_arista import BGPRoutePluginArista + +DEPENDS_KWARGS = { + "depends": [ + "hyperglass/models/tests/test_util.py::test_check_legacy_fields", + "hyperglass/external/tests/test_rpki.py::test_rpki", + ], + "scope": "session", +} + +SAMPLE = Path(__file__).parent.parent.parent.parent / ".samples" / "arista_route.json" + + +def _tester(sample: str): + plugin = BGPRoutePluginArista() + + device = MockDevice( + name="Test Device", + address="127.0.0.1", + group="Test Network", + credential={"username": "", "password": ""}, + platform="arista", + structured_output=True, + directives=["__hyperglass_arista_eos_bgp_route_table__"], + attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"}, + ) + + query = type("Query", (), {"device": device}) + + result = plugin.process(output=(sample,), query=query) + assert isinstance(result, BGPRouteTable), "Invalid parsed result" + assert hasattr(result, "count"), "BGP Table missing count" + assert result.count > 0, "BGP Table count is 0" + + +@pytest.mark.dependency(**DEPENDS_KWARGS) +def test_arista_route_sample(): + with SAMPLE.open("r") as file: + sample = file.read() + return _tester(sample) diff --git a/hyperglass/plugins/tests/test_bgp_route_frr.py b/hyperglass/plugins/tests/test_bgp_route_frr.py new file mode 100644 index 000000000..bd042b238 --- /dev/null +++ b/hyperglass/plugins/tests/test_bgp_route_frr.py @@ -0,0 +1,55 @@ +"""FRR BGP Route Parsing Tests.""" + +# flake8: noqa +# Standard Library +from pathlib import Path + +# Third Party +import pytest + +# Project +from hyperglass.models.config.devices import Device +from hyperglass.models.data.bgp_route import BGPRouteTable + +# Local +from ._fixtures import MockDevice +from .._builtin.bgp_route_frr import BGPRoutePluginFrr + +DEPENDS_KWARGS = { + "depends": [ + "hyperglass/models/tests/test_util.py::test_check_legacy_fields", + "hyperglass/external/tests/test_rpki.py::test_rpki", + ], + "scope": "session", +} + +SAMPLE = Path(__file__).parent.parent.parent.parent / ".samples" / "frr_bgp_route.json" + + +def _tester(sample: str): + plugin = BGPRoutePluginFrr() + + device = MockDevice( + name="Test Device", + address="127.0.0.1", + group="Test Network", + credential={"username": "", "password": ""}, + platform="frr", + structured_output=True, + directives=["__hyperglass_frr_bgp_route_table__"], + attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"}, + ) + + query = type("Query", (), {"device": device}) + + result = plugin.process(output=(sample,), query=query) + assert isinstance(result, BGPRouteTable), "Invalid parsed result" + assert hasattr(result, "count"), "BGP Table missing count" + assert result.count > 0, "BGP Table count is 0" + + +@pytest.mark.dependency(**DEPENDS_KWARGS) +def test_frr_route_sample(): + with SAMPLE.open("r") as file: + sample = file.read() + return _tester(sample) diff --git a/hyperglass/plugins/tests/test_bgp_route_juniper.py b/hyperglass/plugins/tests/test_bgp_route_juniper.py new file mode 100644 index 000000000..7a022740a --- /dev/null +++ b/hyperglass/plugins/tests/test_bgp_route_juniper.py @@ -0,0 +1,70 @@ +"""Juniper BGP Route Parsing Tests.""" + +# flake8: noqa +# Standard Library +from pathlib import Path + +# Third Party +import pytest + +# Project +from hyperglass.models.data.bgp_route import BGPRouteTable + +# Local +from ._fixtures import MockDevice +from .._builtin.bgp_route_juniper import BGPRoutePluginJuniper + +DEPENDS_KWARGS = { + "depends": [ + "hyperglass/models/tests/test_util.py::test_check_legacy_fields", + "hyperglass/external/tests/test_rpki.py::test_rpki", + ], + "scope": "session", +} + +DIRECT = Path(__file__).parent.parent.parent.parent / ".samples" / "juniper_route_direct.xml" +INDIRECT = Path(__file__).parent.parent.parent.parent / ".samples" / "juniper_route_indirect.xml" +AS_PATH = Path(__file__).parent.parent.parent.parent / ".samples" / "juniper_route_aspath.xml" + + +def _tester(sample: str): + plugin = BGPRoutePluginJuniper() + + device = MockDevice( + name="Test Device", + address="127.0.0.1", + group="Test Network", + credential={"username": "", "password": ""}, + platform="juniper", + structured_output=True, + directives=[], + attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"}, + ) + + query = type("Query", (), {"device": device}) + + result = plugin.process(output=(sample,), query=query) + assert isinstance(result, BGPRouteTable), "Invalid parsed result" + assert hasattr(result, "count"), "BGP Table missing count" + assert result.count > 0, "BGP Table count is 0" + + +@pytest.mark.dependency(**DEPENDS_KWARGS) +def test_juniper_bgp_route_direct(): + with DIRECT.open("r") as file: + sample = file.read() + return _tester(sample) + + +@pytest.mark.dependency(**DEPENDS_KWARGS) +def test_juniper_bgp_route_indirect(): + with INDIRECT.open("r") as file: + sample = file.read() + return _tester(sample) + + +@pytest.mark.dependency(**DEPENDS_KWARGS) +def test_juniper_bgp_route_aspath(): + with AS_PATH.open("r") as file: + sample = file.read() + return _tester(sample) diff --git a/hyperglass/settings.py b/hyperglass/settings.py new file mode 100644 index 000000000..b8dac1913 --- /dev/null +++ b/hyperglass/settings.py @@ -0,0 +1,19 @@ +"""Access hyperglass global system settings.""" + +# Standard Library +import typing as t + +if t.TYPE_CHECKING: + # Local + from .models.system import HyperglassSettings + + +def _system_settings() -> "HyperglassSettings": + """Get system settings from local environment.""" + # Local + from .models.system import HyperglassSettings + + return HyperglassSettings() + + +Settings = _system_settings() diff --git a/hyperglass/state/__init__.py b/hyperglass/state/__init__.py new file mode 100644 index 000000000..dd8cc765a --- /dev/null +++ b/hyperglass/state/__init__.py @@ -0,0 +1,10 @@ +"""hyperglass global state management.""" + +# Local +from .hooks import use_state +from .store import HyperglassState + +__all__ = ( + "use_state", + "HyperglassState", +) diff --git a/hyperglass/state/hooks.py b/hyperglass/state/hooks.py new file mode 100644 index 000000000..107655329 --- /dev/null +++ b/hyperglass/state/hooks.py @@ -0,0 +1,77 @@ +"""Hooks for accessing hyperglass global state.""" + +# Standard Library +import typing as t +from functools import lru_cache + +# Project +from hyperglass.exceptions.private import StateError + +# Local +from .store import HyperglassState +from ..settings import Settings + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.ui import UIParameters + from hyperglass.models.directive import Directives + from hyperglass.models.config.params import Params + from hyperglass.models.config.devices import Devices + + # Local + from .redis import RedisManager + + +@lru_cache +def _use_state(attr: t.Optional[str] = None) -> "HyperglassState": + """Get hyperglass state by property. + + Implemented separately due to typing issues related to lru_cache described here: + https://github.com/python/mypy/issues/8356 + https://github.com/python/mypy/issues/9112 + """ + if attr is None: + return HyperglassState(settings=Settings) + if attr in ("cache", "redis"): + return HyperglassState(settings=Settings).cache + if attr in HyperglassState.properties(): + return getattr(HyperglassState(settings=Settings), attr) + raise StateError("'{attr}' does not exist on HyperglassState", attr=attr) + + +@t.overload +def use_state(attr: t.Literal["params"]) -> "Params": + """Access hyperglass configuration parameters from global state.""" + + +@t.overload +def use_state(attr: t.Literal["devices"]) -> "Devices": + """Access hyperglass devices from global state.""" + + +@t.overload +def use_state(attr: t.Literal["ui_params"]) -> "UIParameters": + """Access hyperglass UI parameters from global state.""" + + +@t.overload +def use_state(attr: t.Literal["cache", "redis"]) -> "RedisManager": + """Directly access hyperglass Redis cache manager.""" + + +@t.overload +def use_state(attr: t.Literal["directives"]) -> "Directives": + """Access all hyperglass directives.""" + + +@t.overload +def use_state(attr=None) -> "HyperglassState": + """Access entire global state. + + This overload needs to be defined last since it's a catchall. + """ + + +def use_state(attr: t.Optional[str] = None) -> "HyperglassState": + """Access global hyperglass state.""" + return _use_state(attr) diff --git a/hyperglass/state/manager.py b/hyperglass/state/manager.py new file mode 100644 index 000000000..c143b100b --- /dev/null +++ b/hyperglass/state/manager.py @@ -0,0 +1,53 @@ +"""hyperglass global state.""" + +# Standard Library +import typing as t + +# Third Party +from redis import Redis, ConnectionPool + +# Project +from hyperglass.util import repr_from_attrs + +# Local +from .redis import RedisManager + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.system import HyperglassSettings + + +class StateManager: + """Global State Manager. + + Maintains configuration objects in Redis cache and accesses them as needed. + """ + + settings: "HyperglassSettings" + redis: RedisManager + _namespace: str = "hyperglass.state" + + def __init__(self, *, settings: "HyperglassSettings") -> None: + """Set up Redis connection and add configuration objects.""" + + self.settings = settings + connection_pool = ConnectionPool.from_url(**self.settings.redis_connection_pool) + redis = Redis(connection_pool=connection_pool) + self.redis = RedisManager(instance=redis, namespace=self._namespace) + + def __repr__(self) -> str: + """Represent state manager by name and namespace.""" + return repr_from_attrs(self, ("redis", "namespace")) + + def __str__(self) -> str: + """Represent state manager by __repr__.""" + return repr(self) + + @classmethod + def properties(cls: "StateManager") -> t.Tuple[str, ...]: + """Get all read-only properties of the state manager.""" + return tuple( + attr + for attr in dir(cls) + if not attr.startswith("_") and "fget" in dir(getattr(cls, attr)) + ) diff --git a/hyperglass/state/redis.py b/hyperglass/state/redis.py new file mode 100644 index 000000000..33fc0ae55 --- /dev/null +++ b/hyperglass/state/redis.py @@ -0,0 +1,187 @@ +"""Interact with redis for state management.""" + +# Standard Library +import pickle +import typing as t +from types import TracebackType +from typing import overload +from datetime import datetime, timedelta + +# Project +from hyperglass.log import log +from hyperglass.exceptions.private import StateError + +if t.TYPE_CHECKING: + # Third Party + from redis import Redis + from redis.client import Pipeline + + +class RedisManager: + """Convenience wrapper for managing a redis session.""" + + instance: "Redis" + namespace: str + + def __init__(self, instance: "Redis", namespace: str) -> None: + """Set up Redis connection and add configuration objects.""" + self.instance = instance + self.namespace = namespace + + def __repr__(self) -> str: + """Alias repr to Redis instance's repr.""" + return repr(self.instance) + + def __str__(self) -> str: + """String-friendly redis manager.""" + return repr(self) + + def _key_join(self, *keys: str) -> str: + """Format keys with state namespace.""" + key_in_parts = (k for key in keys for k in key.split(".")) + key_parts = list(dict.fromkeys((*self.namespace.split("."), *key_in_parts))) + return ".".join(key_parts) + + def key(self, key: t.Union[str, t.Sequence[str]]) -> str: + """Format keys with state namespace.""" + if isinstance(key, (t.List, t.Tuple, t.Generator)): + return self._key_join(*key) + return self._key_join(key) + + def check(self) -> bool: + """Ensure the redis instance is running and reachable.""" + result = self.instance.ping() + if result is False: + raise RuntimeError( + "Redis instance {!r} is not running or reachable".format(self.instance) + ) + return result + + def delete(self, key: t.Union[str, t.Sequence[str]]) -> None: + """Delete a key and value from the cache.""" + self.instance.delete(self.key(key)) + + def expire( + self, + key: t.Union[str, t.Sequence[str]], + *, + expire_in: t.Optional[t.Union[timedelta, int]] = None, + expire_at: t.Optional[t.Union[datetime, int]] = None, + ) -> None: + """Expire a cache key, either at a time, or in a number of seconds. + + If no at or in time is specified, the key is deleted. + """ + key = self.key(key) + if isinstance(expire_at, (datetime, int)): + self.instance.expireat(key, expire_at) + return + if isinstance(expire_in, (timedelta, int)): + self.instance.expire(key, expire_in) + return + self.instance.delete(key) + + def get( + self, + key: t.Union[str, t.Sequence[str]], + *, + raise_if_none: bool = False, + value_if_none: t.Any = None, + ) -> t.Union[None, t.Any]: + """Get and decode a value from the cache.""" + name = self.key(key) + value: t.Optional[bytes] = self.instance.get(name) + if isinstance(value, bytes): + return pickle.loads(value) # noqa + if raise_if_none is True: + raise StateError("'{key}' ('{name}') does not exist in Redis store", key=key, name=name) + if value_if_none is not None: + return value_if_none + return None + + def set(self, key: t.Union[str, t.Sequence[str]], value: t.Any) -> None: + """Add an object to the cache.""" + name = self.key(key) + self.instance.set(name, pickle.dumps(value)) + + @overload + def get_map(self, key: str, item: str) -> t.Any: + """Get a single value from a Redis hash map (dict).""" + + @overload + def get_map(self, key: str, item=None) -> t.Any: + """Get a single value from a Redis hash map (dict).""" + + def get_map(self, key: str, item: t.Optional[str] = None) -> t.Any: + """Get a Redis hash map or hash map value.""" + name = self.key(key) + if isinstance(item, str): + value = self.instance.hget(name, item) + else: + value = self.instance.hgetall(name) + + if isinstance(value, bytes): + return pickle.loads(value) # noqa + return None + + def set_map_item(self, key: str, item: str, value: t.Any) -> None: + """Add a value to a hash map (dict).""" + name = self.key(key) + self.instance.hset(name, item, pickle.dumps(value)) + + def pipeline(self): + """Enter a Redis Pipeline, but expose all the custom interaction methods.""" + # Copy the base RedisManager and remove the pipeline method (this method). + ctx = type( + "RedisManagerExcludePipeline", + (RedisManager,), + {k: v for k, v in self.__dict__.items() if k != "pipeline"}, + ) + + def nested_pipeline(*_, **__) -> None: + """Ensure pipeline is never called from within pipeline.""" + raise AttributeError("Cannot access pipeline from pipeline") + + class RedisManagerPipeline(ctx): + """Copy of RedisManager, but uses `Redis.pipeline` as the `instance`.""" + + parent: "Redis" + instance: "Pipeline" + pipeline: t.Any = nested_pipeline + + def __init__( + pipeline_self, # noqa: N805 Avoid `self` namespace conflict + *, + parent: "Redis", + instance: "Pipeline", + namespace: str, + ) -> None: + pipeline_self.parent = parent + super().__init__(instance=instance, namespace=namespace) + + def __enter__( + pipeline_self: "RedisManagerPipeline", # noqa: N805 Avoid `self` namespace conflict + ) -> "RedisManagerPipeline": + return pipeline_self + + def __exit__( + pipeline_self: "RedisManagerPipeline", # noqa: N805 Avoid `self` namespace conflict + exc_type: t.Optional[t.Type[BaseException]] = None, + exc_value: t.Optional[BaseException] = None, + _: t.Optional[TracebackType] = None, + ) -> None: + pipeline_self.instance.execute() + if exc_type is not None: + log.bind( + pipeline=repr(pipeline_self), + parent=repr(pipeline_self.parent), + error=exc_value, + ).error( + "Error exiting pipeline", + ) + + return RedisManagerPipeline( + parent=self.instance, + instance=self.instance.pipeline(), + namespace=self.namespace, + ) diff --git a/hyperglass/state/store.py b/hyperglass/state/store.py new file mode 100644 index 000000000..9414adc0c --- /dev/null +++ b/hyperglass/state/store.py @@ -0,0 +1,79 @@ +"""Primary state container.""" + +# Standard Library +import typing as t + +# Local +from .manager import StateManager + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.ui import UIParameters + from hyperglass.plugins._base import HyperglassPlugin + from hyperglass.models.directive import Directive, Directives + from hyperglass.models.config.params import Params + from hyperglass.models.config.devices import Devices + + # Local + from .manager import RedisManager + + +PluginT = t.TypeVar("PluginT", bound="HyperglassPlugin") + + +class HyperglassState(StateManager): + """Primary hyperglass state container.""" + + def add_plugin(self, _type: str, plugin: "HyperglassPlugin") -> None: + """Add a plugin to its list by type.""" + current = self.plugins(_type) + self.redis.set(("plugins", _type), list({*current, plugin})) + + def remove_plugin(self, _type: str, plugin: "HyperglassPlugin") -> None: + """Remove a plugin from its list by type.""" + current = self.plugins(_type) + plugins = {p for p in current if p != plugin} + self.redis.set(("plugins", _type), list(plugins)) + + def reset_plugins(self, _type: str) -> None: + """Remove all plugins of `_type`.""" + self.redis.set(("plugins", _type), []) + + def add_directive(self, *directives: t.Union["Directive", t.Dict[str, t.Any]]) -> None: + """Add a directive.""" + current = self.directives + current.add(*directives, unique_by="id") + self.redis.set("directives", current) + + def clear(self) -> None: + """Delete all cache keys.""" + self.redis.instance.flushdb(asynchronous=True) + + @property + def cache(self) -> "RedisManager": + """Get the redis manager instance.""" + return self.redis + + @property + def params(self) -> "Params": + """Get hyperglass configuration parameters (`hyperglass.yaml`).""" + return self.redis.get("params", raise_if_none=True) + + @property + def devices(self) -> "Devices": + """Get hyperglass devices (`devices.yaml`).""" + return self.redis.get("devices", raise_if_none=True) + + @property + def ui_params(self) -> "UIParameters": + """UI parameters, built from params.""" + return self.redis.get("ui_params", raise_if_none=True) + + @property + def directives(self) -> "Directives": + """All directives.""" + return self.redis.get("directives", raise_if_none=True) + + def plugins(self, _type: str) -> t.List[PluginT]: + """Get plugins by type.""" + return self.redis.get(("plugins", _type), raise_if_none=False, value_if_none=[]) diff --git a/hyperglass/state/tests/__init__.py b/hyperglass/state/tests/__init__.py new file mode 100644 index 000000000..b8e28d438 --- /dev/null +++ b/hyperglass/state/tests/__init__.py @@ -0,0 +1 @@ +"""State tests.""" diff --git a/hyperglass/state/tests/test_hooks.py b/hyperglass/state/tests/test_hooks.py new file mode 100644 index 000000000..e82f1a432 --- /dev/null +++ b/hyperglass/state/tests/test_hooks.py @@ -0,0 +1,102 @@ +"""Test state hooks.""" + +# Standard Library +import typing as t + +# Third Party +import pytest + +if t.TYPE_CHECKING: + from hyperglass.state import HyperglassState + +# Project +from hyperglass.models.ui import UIParameters +from hyperglass.configuration import init_ui_params +from hyperglass.models.directive import Directives +from hyperglass.models.config.params import Params +from hyperglass.models.config.devices import Devices + +# Local +from ..hooks import use_state +from ..store import HyperglassState + +STATE_ATTRS = ( + ("params", Params), + ("devices", Devices), + ("ui_params", UIParameters), + ("directives", Directives), + (None, HyperglassState), +) + + +@pytest.fixture +def params(): + return {} + + +@pytest.fixture +def devices(): + return [ + { + "name": "test1", + "address": "127.0.0.1", + "credential": {"username": "", "password": ""}, + "platform": "juniper", + "attrs": {"source4": "192.0.2.1", "source6": "2001:db8::1"}, + "directives": ["juniper_bgp_route"], + } + ] + + +@pytest.fixture +def directives(): + return [ + { + "juniper_bgp_route": { + "name": "BGP Route", + "field": {"description": "test"}, + } + } + ] + + +@pytest.fixture +def state( + *, + params: t.Dict[str, t.Any], + directives: t.Sequence[t.Dict[str, t.Any]], + devices: t.Sequence[t.Dict[str, t.Any]], +) -> t.Generator["HyperglassState", None, None]: + """Test fixture to initialize Redis store.""" + _state = use_state() + _params = Params(**params) + _directives = Directives.new(*directives) + + with _state.cache.pipeline() as pipeline: + # Write params and directives to the cache first to avoid a race condition where ui_params + # or devices try to access params or directives before they're available. + pipeline.set("params", _params) + pipeline.set("directives", _directives) + + _devices = Devices(*devices) + ui_params = init_ui_params(params=_params, devices=_devices) + + with _state.cache.pipeline() as pipeline: + pipeline.set("devices", _devices) + pipeline.set("ui_params", ui_params) + + yield _state + _state.clear() + + +def test_use_state_caching(state): + first = None + for attr, model in STATE_ATTRS: + for i in range(0, 5): + instance = use_state(attr) + if i == 0: + first = instance + assert isinstance(instance, model), ( + f"{instance!r} is not an instance of '{model.__name__}'" + ) + assert instance == first, f"{instance!r} is not equal to {first!r}" diff --git a/hyperglass/types.py b/hyperglass/types.py new file mode 100644 index 000000000..6bfd9ca3f --- /dev/null +++ b/hyperglass/types.py @@ -0,0 +1,9 @@ +"""Custom types.""" + +# Standard Library +import typing as _t + +_S = _t.TypeVar("_S") + +Series = _t.Union[_t.MutableSequence[_S], _t.Tuple[_S], _t.Set[_S]] +"""Like `typing.Sequence`, but excludes `str`.""" diff --git a/hyperglass/ui/.eslintignore-delete b/hyperglass/ui/.eslintignore-delete new file mode 100644 index 000000000..e06f250c7 --- /dev/null +++ b/hyperglass/ui/.eslintignore-delete @@ -0,0 +1,5 @@ +node_modules +dist +.next/ +favicon-formats.ts +custom.*[js, html] diff --git a/hyperglass/ui/.eslintrc b/hyperglass/ui/.eslintrc deleted file mode 100644 index bbf8f1eea..000000000 --- a/hyperglass/ui/.eslintrc +++ /dev/null @@ -1,60 +0,0 @@ -{ - "root": true, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "plugin:prettier/recommended", - "prettier/@typescript-eslint" - ], - "parser": "@typescript-eslint/parser", - "env": { - "browser": true, - "es6": true, - "node": true - }, - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "ecmaFeatures": { - "jsx": true, - "arrowFunctions": true - }, - "project": "./tsconfig.json" - }, - "plugins": ["react", "@typescript-eslint", "prettier"], - "settings": { - "react": { - "version": "detect" - }, - "import/resolver": { - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"], - "paths": ["./src"] - } - } - }, - "rules": { - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-unused-vars-experimental": "error", - "no-unused-vars": "off", - "react/jsx-uses-react": "off", - "react/react-in-jsx-scope": "off", - "comma-dangle": ["error", "always-multiline"], - "global-require": "off", - "import/no-dynamic-require": "off", - "import/prefer-default-export": "off", - "@typescript-eslint/no-inferrable-types": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-namespace": "off", - "@typescript-eslint/no-empty-interface": [ - "error", - { - "allowSingleExtends": true - } - ] - } -} diff --git a/hyperglass/ui/.eslintrc-delete.js b/hyperglass/ui/.eslintrc-delete.js new file mode 100644 index 000000000..cd4d95d52 --- /dev/null +++ b/hyperglass/ui/.eslintrc-delete.js @@ -0,0 +1,69 @@ +module.exports = { + root: true, + extends: ['eslint:recommended'], + env: { + es6: true, + node: true, + }, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + arrowFunctions: true, + }, + project: './tsconfig.json', + }, + overrides: [ + { + files: ['**/*.ts', '**/*.tsx'], + parser: '@typescript-eslint/parser', + settings: { + react: { version: 'detect' }, + 'import/resolver': { + typescript: {}, + }, + }, + env: { + browser: true, + node: true, + es6: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + 'plugin:import/typescript', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:prettier/recommended', + ], + rules: { + 'prettier/prettier': ['error', {}, { usePrettierrc: true }], + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars-experimental': 'error', + 'no-unused-vars': 'off', + 'react/jsx-uses-react': 'off', + 'react/react-in-jsx-scope': 'off', + 'comma-dangle': ['error', 'always-multiline'], + 'global-require': 'off', + 'import/no-dynamic-require': 'off', + 'import/prefer-default-export': 'off', + 'import/no-named-as-default-member': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-empty-interface': [ + 'error', + { + allowSingleExtends: true, + }, + ], + }, + }, + ], +}; diff --git a/hyperglass/ui/.gitignore b/hyperglass/ui/.gitignore index 35328e3b7..e5441774d 100644 --- a/hyperglass/ui/.gitignore +++ b/hyperglass/ui/.gitignore @@ -1,4 +1,8 @@ .DS_Store +.env* +hyperglass.json +custom.*[js, html] +*.tsbuildinfo # dev/test files TODO.txt *.tmp* diff --git a/hyperglass/ui/.prettierignore b/hyperglass/ui/.prettierignore index b1e1b1858..fc2cfe785 100644 --- a/hyperglass/ui/.prettierignore +++ b/hyperglass/ui/.prettierignore @@ -5,4 +5,6 @@ yarn.lock package-lock.json .eslintrc tsconfig.json -.next/ \ No newline at end of file +.next/ +favicon-formats.ts +custom.*[js, html] diff --git a/hyperglass/ui/biome.json b/hyperglass/ui/biome.json new file mode 100644 index 000000000..9ba75761b --- /dev/null +++ b/hyperglass/ui/biome.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "extends": ["../../biome.json"] +} diff --git a/hyperglass/ui/components/card/types.ts b/hyperglass/ui/components/card/types.ts deleted file mode 100644 index 739e24e9c..000000000 --- a/hyperglass/ui/components/card/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { FlexProps } from '@chakra-ui/react'; - -export interface TCardBody extends Omit<FlexProps, 'onClick'> { - onClick?: () => boolean; -} - -export interface TCardFooter extends FlexProps {} - -export interface TCardHeader extends FlexProps {} diff --git a/hyperglass/ui/components/countdown/index.ts b/hyperglass/ui/components/countdown/index.ts deleted file mode 100644 index c1ddcb5d5..000000000 --- a/hyperglass/ui/components/countdown/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './countdown'; diff --git a/hyperglass/ui/components/countdown/types.ts b/hyperglass/ui/components/countdown/types.ts deleted file mode 100644 index c056e9d9e..000000000 --- a/hyperglass/ui/components/countdown/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CountdownRenderProps } from 'react-countdown'; - -export interface TRenderer extends CountdownRenderProps { - text: string; -} - -export interface TCountdown { - timeout: number; - text: string; -} diff --git a/hyperglass/ui/components/debugger.tsx b/hyperglass/ui/components/debugger.tsx index dd4b73202..4c2fc28a9 100644 --- a/hyperglass/ui/components/debugger.tsx +++ b/hyperglass/ui/components/debugger.tsx @@ -3,17 +3,23 @@ import { Modal, HStack, Button, - useTheme, ModalBody, ModalHeader, ModalOverlay, ModalContent, - useColorMode, useDisclosure, ModalCloseButton, } from '@chakra-ui/react'; -import { useConfig, useColorValue, useBreakpointValue } from '~/context'; -import { CodeBlock } from '~/components'; +import { useConfig } from '~/context'; +import { CodeBlock, DynamicIcon } from '~/elements'; +import { + useTheme, + useColorMode, + useColorValue, + useBreakpointValue, + // useHyperglassConfig, +} from '~/hooks'; + import type { UseDisclosureReturn } from '@chakra-ui/react'; interface TViewer extends Pick<UseDisclosureReturn, 'isOpen' | 'onClose'> { @@ -21,14 +27,14 @@ interface TViewer extends Pick<UseDisclosureReturn, 'isOpen' | 'onClose'> { children: React.ReactNode; } -const Viewer: React.FC<TViewer> = (props: TViewer) => { +const Viewer = (props: TViewer): JSX.Element => { const { title, isOpen, onClose, children } = props; - const bg = useColorValue('white', 'black'); + const bg = useColorValue('white', 'blackSolid.700'); const color = useColorValue('black', 'white'); return ( <Modal isOpen={isOpen} onClose={onClose} size="full" scrollBehavior="inside"> <ModalOverlay /> - <ModalContent bg={bg} color={color} py={4} borderRadius="md" maxW="90%"> + <ModalContent bg={bg} color={color} py={4} borderRadius="md" maxW="90%" minH="90vh"> <ModalHeader>{title}</ModalHeader> <ModalCloseButton /> <ModalBody> @@ -39,7 +45,7 @@ const Viewer: React.FC<TViewer> = (props: TViewer) => { ); }; -export const Debugger: React.FC = () => { +export const Debugger = (): JSX.Element => { const { isOpen: configOpen, onOpen: onConfigOpen, onClose: configClose } = useDisclosure(); const { isOpen: themeOpen, onOpen: onThemeOpen, onClose: themeClose } = useDisclosure(); const { colorMode } = useColorMode(); @@ -50,6 +56,7 @@ export const Debugger: React.FC = () => { useBreakpointValue({ base: 'SMALL', md: 'MEDIUM', lg: 'LARGE', xl: 'X-LARGE' }) ?? 'UNKNOWN'; const tagSize = useBreakpointValue({ base: 'sm', lg: 'lg' }) ?? 'lg'; const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' }) ?? 'sm'; + // const { refetch } = useHyperglassConfig(); return ( <> <HStack @@ -69,12 +76,30 @@ export const Debugger: React.FC = () => { <Tag size={tagSize} colorScheme="gray"> {colorMode.toUpperCase()} </Tag> - <Button size={btnSize} colorScheme="blue" onClick={onConfigOpen}> + <Button + size={btnSize} + colorScheme="cyan" + onClick={onConfigOpen} + leftIcon={<DynamicIcon icon={{ bs: 'BsBraces' }} />} + > View Config </Button> - <Button size={btnSize} colorScheme="red" onClick={onThemeOpen}> + <Button + size={btnSize} + leftIcon={<DynamicIcon icon={{ io: 'IoIosColorPalette' }} />} + colorScheme="blue" + onClick={onThemeOpen} + > View Theme </Button> + {/* <Button + size={btnSize} + colorScheme="purple" + leftIcon={<DynamicIcon icon={{ hi: 'HiOutlineDownload' }} />} + onClick={() => refetch()} + > + Reload Config + </Button> */} <Tag size={tagSize} colorScheme="teal"> {mediaSize} </Tag> diff --git a/hyperglass/ui/components/help/modal.tsx b/hyperglass/ui/components/directive-info-modal.tsx similarity index 53% rename from hyperglass/ui/components/help/modal.tsx rename to hyperglass/ui/components/directive-info-modal.tsx index faff30a03..9b7b91540 100644 --- a/hyperglass/ui/components/help/modal.tsx +++ b/hyperglass/ui/components/directive-info-modal.tsx @@ -1,4 +1,3 @@ -import dynamic from 'next/dynamic'; import { Modal, ScaleFade, @@ -10,21 +9,25 @@ import { useDisclosure, ModalCloseButton, } from '@chakra-ui/react'; -import { Markdown } from '~/components'; -import { useColorValue } from '~/context'; -import { isQueryContent } from '~/types'; +import { DynamicIcon, Markdown } from '~/elements'; +import { useColorValue } from '~/hooks'; -import type { THelpModal } from './types'; +import type { ModalContentProps } from '@chakra-ui/react'; -const Info = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiInfo)); +interface DirectiveInfoModalProps extends Omit<ModalContentProps, 'title'> { + title: string | null; + item: string | null; + name: string; + visible: boolean; +} -export const HelpModal: React.FC<THelpModal> = (props: THelpModal) => { - const { visible, item, name, ...rest } = props; +export const DirectiveInfoModal = (props: DirectiveInfoModalProps): JSX.Element => { + const { visible, item, name, title, ...rest } = props; const { isOpen, onOpen, onClose } = useDisclosure(); - const bg = useColorValue('whiteSolid.50', 'blackSolid.800'); + const bg = useColorValue('whiteSolid.50', 'blackSolid.700'); const color = useColorValue('black', 'white'); - if (!isQueryContent(item)) { - return null; + if (item === null) { + return <></>; } return ( <> @@ -36,19 +39,19 @@ export const HelpModal: React.FC<THelpModal> = (props: THelpModal) => { minW={3} size="md" variant="link" - icon={<Info />} onClick={onOpen} colorScheme="blue" - aria-label={`${name}_help`} + aria-label={`${title} Details`} + icon={<DynamicIcon icon={{ fa: 'InfoCircle' }} />} /> </ScaleFade> - <Modal isOpen={isOpen} onClose={onClose} size="xl" motionPreset="slideInRight"> + <Modal isOpen={isOpen} onClose={onClose} size="xl" motionPreset="scale"> <ModalOverlay /> <ModalContent bg={bg} color={color} py={4} borderRadius="md" {...rest}> - <ModalHeader>{item.params.title}</ModalHeader> + <ModalHeader>{title}</ModalHeader> <ModalCloseButton /> <ModalBody> - <Markdown content={item.content} /> + <Markdown content={item} /> </ModalBody> </ModalContent> </Modal> diff --git a/hyperglass/ui/components/footer/button.tsx b/hyperglass/ui/components/footer/button.tsx index a67193d1d..354684918 100644 --- a/hyperglass/ui/components/footer/button.tsx +++ b/hyperglass/ui/components/footer/button.tsx @@ -1,16 +1,22 @@ import { useMemo } from 'react'; import { Button, Menu, MenuButton, MenuList } from '@chakra-ui/react'; -import { Markdown } from '~/components'; -import { useColorValue, useBreakpointValue, useConfig } from '~/context'; -import { useOpposingColor, useStrf } from '~/hooks'; +import { useConfig } from '~/context'; +import { Markdown } from '~/elements'; +import { useColorValue, useBreakpointValue, useOpposingColor, useStrf } from '~/hooks'; -import type { IConfig } from '~/types'; -import type { TFooterButton } from './types'; +import type { MenuListProps } from '@chakra-ui/react'; +import type { Config } from '~/types'; + +interface FooterButtonProps extends Omit<MenuListProps, 'title'> { + side: 'left' | 'right'; + title?: MenuListProps['children']; + content: string; +} /** * Filter the configuration object based on values that are strings for formatting. */ -function getConfigFmt(config: IConfig): Record<string, string> { +function getConfigFmt(config: Config): Record<string, string> { const fmt = {} as Record<string, string>; for (const [k, v] of Object.entries(config)) { if (typeof v === 'string') { @@ -20,12 +26,13 @@ function getConfigFmt(config: IConfig): Record<string, string> { return fmt; } -export const FooterButton: React.FC<TFooterButton> = (props: TFooterButton) => { +export const FooterButton = (props: FooterButtonProps): JSX.Element => { const { content, title, side, ...rest } = props; const config = useConfig(); - const fmt = useMemo(() => getConfigFmt(config), []); - const fmtContent = useStrf(content, fmt); + const strF = useStrf(); + const fmt = useMemo(() => getConfigFmt(config), [config]); + const fmtContent = useMemo(() => strF(content, fmt), [fmt, content, strF]); const placement = side === 'left' ? 'top' : side === 'right' ? 'top-end' : undefined; const bg = useColorValue('white', 'gray.900'); @@ -39,6 +46,7 @@ export const FooterButton: React.FC<TFooterButton> = (props: TFooterButton) => { as={Button} size={size} variant="ghost" + lineHeight={0} aria-label={typeof title === 'string' ? title : undefined} > {title} diff --git a/hyperglass/ui/components/footer/color-mode.tsx b/hyperglass/ui/components/footer/color-mode.tsx new file mode 100644 index 000000000..5059b97ba --- /dev/null +++ b/hyperglass/ui/components/footer/color-mode.tsx @@ -0,0 +1,48 @@ +import { forwardRef } from 'react'; +import { Button, Tooltip } from '@chakra-ui/react'; +import { Switch, Case } from 'react-if'; +import { DynamicIcon } from '~/elements'; +import { useOpposingColor, useColorMode, useColorValue, useBreakpointValue } from '~/hooks'; + +import type { ButtonProps } from '@chakra-ui/react'; + +interface ColorModeToggleProps extends Omit<ButtonProps, 'size'> { + size?: string | number; +} + +export const ColorModeToggle = forwardRef<HTMLButtonElement, ColorModeToggleProps>( + (props: ColorModeToggleProps, ref) => { + const { size = '1.5rem', ...rest } = props; + const { colorMode, toggleColorMode } = useColorMode(); + + const bg = useColorValue('primary.500', 'yellow.300'); + const color = useOpposingColor(bg); + const label = useColorValue('Switch to dark mode', 'Switch to light mode'); + const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' }); + + return ( + <Tooltip hasArrow placement="top-end" label={label} bg={bg} color={color}> + <Button + ref={ref} + size={btnSize} + title={label} + variant="ghost" + aria-label={label} + _hover={{ color: bg }} + color="currentColor" + onClick={toggleColorMode} + {...rest} + > + <Switch> + <Case condition={colorMode === 'light'}> + <DynamicIcon icon={{ hi: 'HiMoon' }} boxSize={size} /> + </Case> + <Case condition={colorMode === 'dark'}> + <DynamicIcon icon={{ hi: 'HiSun' }} boxSize={size} /> + </Case> + </Switch> + </Button> + </Tooltip> + ); + }, +); diff --git a/hyperglass/ui/components/footer/colorMode.tsx b/hyperglass/ui/components/footer/colorMode.tsx deleted file mode 100644 index feee0dd89..000000000 --- a/hyperglass/ui/components/footer/colorMode.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { forwardRef } from 'react'; -import dynamic from 'next/dynamic'; -import { Button, Icon, Tooltip } from '@chakra-ui/react'; -import { If } from '~/components'; -import { useColorMode, useColorValue, useBreakpointValue } from '~/context'; -import { useOpposingColor } from '~/hooks'; - -import type { TColorModeToggle } from './types'; - -const Sun = dynamic<MeronexIcon>(() => import('@meronex/icons/hi').then(i => i.HiSun)); -const Moon = dynamic<MeronexIcon>(() => import('@meronex/icons/hi').then(i => i.HiMoon)); - -export const ColorModeToggle = forwardRef<HTMLButtonElement, TColorModeToggle>( - (props: TColorModeToggle, ref) => { - const { size = '1.5rem', ...rest } = props; - const { colorMode, toggleColorMode } = useColorMode(); - - const bg = useColorValue('primary.500', 'yellow.300'); - const color = useOpposingColor(bg); - const label = useColorValue('Switch to dark mode', 'Switch to light mode'); - const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' }); - - return ( - <Tooltip hasArrow placement="top-end" label={label} bg={bg} color={color}> - <Button - ref={ref} - size={btnSize} - title={label} - variant="ghost" - aria-label={label} - _hover={{ color: bg }} - color="currentColor" - onClick={toggleColorMode} - {...rest} - > - <If c={colorMode === 'light'}> - <Icon as={Moon} boxSize={size} /> - </If> - <If c={colorMode === 'dark'}> - <Icon as={Sun} boxSize={size} /> - </If> - </Button> - </Tooltip> - ); - }, -); diff --git a/hyperglass/ui/components/footer/footer.tsx b/hyperglass/ui/components/footer/footer.tsx index 80a28a9ce..17d3fea4a 100644 --- a/hyperglass/ui/components/footer/footer.tsx +++ b/hyperglass/ui/components/footer/footer.tsx @@ -1,21 +1,19 @@ +import { Flex, HStack, useToken } from '@chakra-ui/react'; import { useMemo } from 'react'; -import dynamic from 'next/dynamic'; -import { Flex, Icon, HStack, useToken } from '@chakra-ui/react'; -import { If } from '~/components'; -import { useConfig, useMobile, useColorValue, useBreakpointValue } from '~/context'; -import { useStrf } from '~/hooks'; +import { useConfig } from '~/context'; +import { DynamicIcon } from '~/elements'; +import { useBreakpointValue, useColorValue, useMobile } from '~/hooks'; +import { isLink, isMenu } from '~/types'; import { FooterButton } from './button'; -import { ColorModeToggle } from './colorMode'; +import { ColorModeToggle } from './color-mode'; import { FooterLink } from './link'; -import { isLink, isMenu } from './types'; import type { ButtonProps, LinkProps } from '@chakra-ui/react'; -import type { TLink, TMenu } from '~/types'; +import type { Link, Menu } from '~/types'; -const CodeIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCode)); -const ExtIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/go').then(i => i.GoLinkExternal)); +type MenuItems = (Link | Menu)[]; -function buildItems(links: TLink[], menus: TMenu[]): [(TLink | TMenu)[], (TLink | TMenu)[]] { +function buildItems(links: Link[], menus: Menu[]): [MenuItems, MenuItems] { const leftLinks = links.filter(link => link.side === 'left'); const leftMenus = menus.filter(menu => menu.side === 'left'); const rightLinks = links.filter(link => link.side === 'right'); @@ -26,8 +24,23 @@ function buildItems(links: TLink[], menus: TMenu[]): [(TLink | TMenu)[], (TLink return [left, right]; } -export const Footer: React.FC = () => { - const { web, content, primary_asn } = useConfig(); +const LinkOnSide = (props: { item: ArrayElement<MenuItems>; side: 'left' | 'right' }) => { + const { item, side } = props; + if (isLink(item)) { + const icon: Partial<ButtonProps & LinkProps> = {}; + + if (item.showIcon) { + icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />; + } + return <FooterLink key={item.title} href={item.url} title={item.title} {...icon} />; + } + if (isMenu(item)) { + return <FooterButton key={item.title} side={side} content={item.content} title={item.title} />; + } +}; + +export const Footer = (): JSX.Element => { + const { web, content } = useConfig(); const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100'); const footerColor = useColorValue('black', 'white'); @@ -36,7 +49,7 @@ export const Footer: React.FC = () => { const isMobile = useMobile(); - const [left, right] = useMemo(() => buildItems(web.links, web.menus), []); + const [left, right] = useMemo(() => buildItems(web.links, web.menus), [web.links, web.menus]); return ( <HStack @@ -49,46 +62,26 @@ export const Footer: React.FC = () => { whiteSpace="nowrap" color={footerColor} spacing={{ base: 8, lg: 6 }} - d={{ base: 'inline-block', lg: 'flex' }} + display={{ base: 'inline-block', lg: 'flex' }} overflowY={{ base: 'auto', lg: 'unset' }} justifyContent={{ base: 'center', lg: 'space-between' }} > - {left.map(item => { - if (isLink(item)) { - const url = useStrf(item.url, { primary_asn }) ?? '/'; - const icon: Partial<ButtonProps & LinkProps> = {}; - - if (item.show_icon) { - icon.rightIcon = <ExtIcon />; - } - return <FooterLink key={item.title} href={url} title={item.title} {...icon} />; - } else if (isMenu(item)) { - return ( - <FooterButton key={item.title} side="left" content={item.content} title={item.title} /> - ); - } - })} + {left.map(item => ( + <LinkOnSide key={item.title} item={item} side="left" /> + ))} {!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />} - {right.map(item => { - if (isLink(item)) { - const url = useStrf(item.url, { primary_asn }) ?? '/'; - const icon: Partial<ButtonProps & LinkProps> = {}; - - if (item.show_icon) { - icon.rightIcon = <ExtIcon />; - } - return <FooterLink href={url} title={item.title} {...icon} />; - } else if (isMenu(item)) { - return <FooterButton side="right" content={item.content} title={item.title} />; - } - })} - <If c={web.credit.enable}> + {right.map(item => ( + <LinkOnSide key={item.title} item={item} side="right" /> + ))} + {web.credit.enable && ( <FooterButton + key="credit" side="right" content={content.credit} - title={<Icon as={CodeIcon} boxSize={size} />} + title={<DynamicIcon icon={{ fi: 'FiCode' }} boxSize={size} />} /> - </If> + )} + <ColorModeToggle size={size} /> </HStack> ); diff --git a/hyperglass/ui/components/footer/link.tsx b/hyperglass/ui/components/footer/link.tsx index 83d34c02d..cac171ecc 100644 --- a/hyperglass/ui/components/footer/link.tsx +++ b/hyperglass/ui/components/footer/link.tsx @@ -1,8 +1,11 @@ -import { Button, Link, useBreakpointValue } from '@chakra-ui/react'; +import { Button, Link } from '@chakra-ui/react'; +import { useBreakpointValue } from '~/hooks'; -import type { TFooterLink } from './types'; +import type { ButtonProps, LinkProps } from '@chakra-ui/react'; -export const FooterLink: React.FC<TFooterLink> = (props: TFooterLink) => { +type FooterLinkProps = ButtonProps & LinkProps & { title: string }; + +export const FooterLink = (props: FooterLinkProps): JSX.Element => { const { title } = props; const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' }); return ( diff --git a/hyperglass/ui/components/footer/types.ts b/hyperglass/ui/components/footer/types.ts deleted file mode 100644 index 8f6143d78..000000000 --- a/hyperglass/ui/components/footer/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ButtonProps, LinkProps, MenuListProps } from '@chakra-ui/react'; -import type { TLink, TMenu } from '~/types'; - -type TFooterSide = 'left' | 'right'; - -export interface TFooterButton extends Omit<MenuListProps, 'title'> { - side: TFooterSide; - title?: MenuListProps['children']; - content: string; -} - -export type TFooterLink = ButtonProps & LinkProps & { title: string }; - -export type TFooterItems = 'help' | 'credit' | 'terms'; - -export interface TColorModeToggle extends ButtonProps { - size?: string; -} - -export function isLink(item: TLink | TMenu): item is TLink { - return 'url' in item; -} - -export function isMenu(item: TLink | TMenu): item is TMenu { - return 'content' in item; -} diff --git a/hyperglass/ui/components/form-field.tsx b/hyperglass/ui/components/form-field.tsx new file mode 100644 index 000000000..63f02e0ef --- /dev/null +++ b/hyperglass/ui/components/form-field.tsx @@ -0,0 +1,75 @@ +import { Flex, FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react'; +import { useMemo } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { If, Then } from 'react-if'; +import { useBooleanValue, useColorValue } from '~/hooks'; + +import type { FormControlProps } from '@chakra-ui/react'; +import type { FieldError } from 'react-hook-form'; +import type { FormData } from '~/types'; + +interface FormFieldProps extends FormControlProps { + name: string; + label: string; + hiddenLabels?: boolean; + labelAddOn?: React.ReactNode; + fieldAddOn?: React.ReactNode; +} + +export const FormField = (props: FormFieldProps): JSX.Element => { + const { name, label, children, labelAddOn, fieldAddOn, hiddenLabels = false, ...rest } = props; + const labelColor = useColorValue('blackAlpha.700', 'whiteAlpha.700'); + const errorColor = useColorValue('red.500', 'red.300'); + const opacity = useBooleanValue(hiddenLabels, 0, undefined); + + const { formState } = useFormContext<FormData>(); + + const error = useMemo<FieldError | null>(() => { + if (name in formState.errors) { + console.group(`Error on field '${label}'`); + console.warn(formState.errors[name as keyof FormData]); + console.groupEnd(); + return formState.errors[name as keyof FormData] as FieldError; + } + return null; + }, [formState, label, name]); + + return ( + <FormControl + mx={2} + w="100%" + maxW="100%" + flexDir="column" + alignItems="center" + my={{ base: 2, lg: 4 }} + isInvalid={error !== null} + {...rest} + > + <FormLabel + pr={0} + mb={{ lg: 4 }} + htmlFor={name} + display="flex" + opacity={opacity} + fontWeight="bold" + alignItems="center" + justifyContent="space-between" + color={error !== null ? errorColor : labelColor} + > + {label} + <If condition={typeof labelAddOn !== 'undefined'}> + <Then>{labelAddOn}</Then> + </If> + </FormLabel> + {children} + <If condition={typeof fieldAddOn !== 'undefined'}> + <Then> + <Flex justify="flex-end" pt={3}> + {fieldAddOn} + </Flex> + </Then> + </If> + <FormErrorMessage opacity={opacity}>{error?.message}</FormErrorMessage> + </FormControl> + ); +}; diff --git a/hyperglass/ui/components/form/field.tsx b/hyperglass/ui/components/form/field.tsx deleted file mode 100644 index 5ea6fe32c..000000000 --- a/hyperglass/ui/components/form/field.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Flex, FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react'; -import { useFormContext } from 'react-hook-form'; -import { If } from '~/components'; -import { useColorValue } from '~/context'; -import { useBooleanValue } from '~/hooks'; - -import type { FieldError } from 'react-hook-form'; -import type { TField } from './types'; - -export const FormField: React.FC<TField> = (props: TField) => { - const { name, label, children, labelAddOn, fieldAddOn, hiddenLabels = false, ...rest } = props; - const labelColor = useColorValue('blackAlpha.700', 'whiteAlpha.700'); - const errorColor = useColorValue('red.500', 'red.300'); - const opacity = useBooleanValue(hiddenLabels, 0, undefined); - - const { - formState: { errors }, - } = useFormContext(); - - const error = name in errors && (errors[name] as FieldError); - - if (error !== false) { - console.warn(`Error on field '${label}': ${error.message}`); - } - - return ( - <FormControl - mx={2} - d="flex" - w="100%" - maxW="100%" - flexDir="column" - my={{ base: 2, lg: 4 }} - isInvalid={error !== false} - flex={{ base: '1 0 100%', lg: '1 0 33.33%' }} - {...rest} - > - <FormLabel - pl={1} - pr={0} - htmlFor={name} - display="flex" - opacity={opacity} - alignItems="center" - justifyContent="space-between" - color={error !== false ? errorColor : labelColor} - > - {label} - <If c={typeof labelAddOn !== 'undefined'}>{labelAddOn}</If> - </FormLabel> - {children} - <If c={typeof fieldAddOn !== 'undefined'}> - <Flex justify="flex-end" pt={3}> - {fieldAddOn} - </Flex> - </If> - <FormErrorMessage opacity={opacity}>{error && error.message}</FormErrorMessage> - </FormControl> - ); -}; diff --git a/hyperglass/ui/components/form/index.ts b/hyperglass/ui/components/form/index.ts deleted file mode 100644 index c31571571..000000000 --- a/hyperglass/ui/components/form/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './field'; -export * from './queryLocation'; -export * from './queryTarget'; -export * from './queryType'; -export * from './queryVrf'; -export * from './resolvedTarget'; -export * from './row'; diff --git a/hyperglass/ui/components/form/queryLocation.tsx b/hyperglass/ui/components/form/queryLocation.tsx deleted file mode 100644 index f5dec8b2c..000000000 --- a/hyperglass/ui/components/form/queryLocation.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useMemo } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { Select } from '~/components'; -import { useConfig } from '~/context'; -import { useLGState, useLGMethods } from '~/hooks'; - -import type { TNetwork, TSelectOption } from '~/types'; -import type { TQuerySelectField } from './types'; - -function buildOptions(networks: TNetwork[]) { - return networks - .map(net => { - const label = net.display_name; - const options = net.locations - .map(loc => ({ - label: loc.name, - value: loc._id, - group: net.display_name, - })) - .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); - return { label, options }; - }) - .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); -} - -export const QueryLocation: React.FC<TQuerySelectField> = (props: TQuerySelectField) => { - const { onChange, label } = props; - - const { networks } = useConfig(); - const { - formState: { errors }, - } = useFormContext(); - const { selections } = useLGState(); - const { exportState } = useLGMethods(); - - const options = useMemo(() => buildOptions(networks), [networks.length]); - - function handleChange(e: TSelectOption | TSelectOption[]): void { - if (e === null) { - e = []; - } else if (typeof e === 'string') { - e = [e]; - } - if (Array.isArray(e)) { - const value = e.map(sel => sel!.value); - onChange({ field: 'query_location', value }); - selections.queryLocation.set(e); - } - } - - return ( - <Select - isMulti - size="lg" - options={options} - aria-label={label} - name="query_location" - onChange={handleChange} - closeMenuOnSelect={false} - value={exportState(selections.queryLocation.value)} - isError={typeof errors.query_location !== 'undefined'} - /> - ); -}; diff --git a/hyperglass/ui/components/form/queryTarget.tsx b/hyperglass/ui/components/form/queryTarget.tsx deleted file mode 100644 index f604afa38..000000000 --- a/hyperglass/ui/components/form/queryTarget.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useMemo } from 'react'; -import { Input, Text } from '@chakra-ui/react'; -import { components } from 'react-select'; -import { If, Select } from '~/components'; -import { useConfig, useColorValue } from '~/context'; -import { useLGState } from '~/hooks'; - -import type { OptionProps } from 'react-select'; -import type { TBGPCommunity, TSelectOption } from '~/types'; -import type { TQueryTarget } from './types'; - -function buildOptions(communities: TBGPCommunity[]): TSelectOption[] { - return communities.map(c => ({ - value: c.community, - label: c.display_name, - description: c.description, - })); -} - -const Option = (props: OptionProps<Dict, false>) => { - const { label, data } = props; - return ( - <components.Option {...props}> - <Text as="span">{label}</Text> - <br /> - <Text fontSize="xs" as="span"> - {data.description} - </Text> - </components.Option> - ); -}; - -export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => { - const { name, register, onChange, placeholder } = props; - - const bg = useColorValue('white', 'whiteAlpha.100'); - const color = useColorValue('gray.400', 'whiteAlpha.800'); - const border = useColorValue('gray.100', 'whiteAlpha.50'); - const placeholderColor = useColorValue('gray.600', 'whiteAlpha.700'); - - const { queryType, queryTarget, displayTarget } = useLGState(); - - const { queries } = useConfig(); - - const options = useMemo(() => buildOptions(queries.bgp_community.communities), []); - - function handleInputChange(e: React.ChangeEvent<HTMLInputElement>): void { - displayTarget.set(e.target.value); - onChange({ field: name, value: e.target.value }); - } - - function handleSelectChange(e: TSelectOption | TSelectOption[]): void { - if (!Array.isArray(e) && e !== null) { - onChange({ field: name, value: e.value }); - displayTarget.set(e.value); - } - } - - return ( - <> - <input {...register} hidden readOnly value={queryTarget.value} /> - <If c={queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select'}> - <Select - size="lg" - name={name} - options={options} - innerRef={register} - components={{ Option }} - onChange={handleSelectChange} - /> - </If> - <If c={!(queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select')}> - <Input - bg={bg} - size="lg" - color={color} - borderRadius="md" - borderColor={border} - aria-label={placeholder} - placeholder={placeholder} - value={displayTarget.value} - name="query_target_display" - onChange={handleInputChange} - _placeholder={{ color: placeholderColor }} - /> - </If> - </> - ); -}; diff --git a/hyperglass/ui/components/form/queryType.tsx b/hyperglass/ui/components/form/queryType.tsx deleted file mode 100644 index 4464a70a4..000000000 --- a/hyperglass/ui/components/form/queryType.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useMemo } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { Select } from '~/components'; -import { useConfig } from '~/context'; -import { useLGState, useLGMethods } from '~/hooks'; - -import type { TQuery, TSelectOption } from '~/types'; -import type { TQuerySelectField } from './types'; - -function buildOptions(queryTypes: TQuery[]): TSelectOption[] { - return queryTypes - .filter(q => q.enable === true) - .map(q => ({ value: q.name, label: q.display_name })); -} - -export const QueryType: React.FC<TQuerySelectField> = (props: TQuerySelectField) => { - const { onChange, label } = props; - const { queries } = useConfig(); - const { - formState: { errors }, - } = useFormContext(); - const { selections } = useLGState(); - const { exportState } = useLGMethods(); - - const options = useMemo(() => buildOptions(queries.list), [queries.list.length]); - - function handleChange(e: TSelectOption | TSelectOption[]): void { - if (!Array.isArray(e) && e !== null) { - selections.queryType.set(e); - onChange({ field: 'query_type', value: e.value }); - } else { - selections.queryType.set(null); - } - } - - return ( - <Select - size="lg" - name="query_type" - options={options} - aria-label={label} - onChange={handleChange} - value={exportState(selections.queryType.value)} - isError={typeof errors.query_type !== 'undefined'} - /> - ); -}; diff --git a/hyperglass/ui/components/form/queryVrf.tsx b/hyperglass/ui/components/form/queryVrf.tsx deleted file mode 100644 index 4c7d2edd7..000000000 --- a/hyperglass/ui/components/form/queryVrf.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useMemo } from 'react'; -import { Select } from '~/components'; -import { useLGMethods, useLGState } from '~/hooks'; - -import type { TDeviceVrf, TSelectOption } from '~/types'; -import type { TQueryVrf } from './types'; - -function buildOptions(queryVrfs: TDeviceVrf[]): TSelectOption[] { - return queryVrfs.map(q => ({ value: q._id, label: q.display_name })); -} - -export const QueryVrf: React.FC<TQueryVrf> = (props: TQueryVrf) => { - const { vrfs, onChange, label } = props; - const { selections } = useLGState(); - const { exportState } = useLGMethods(); - - const options = useMemo(() => buildOptions(vrfs), [vrfs.length]); - - function handleChange(e: TSelectOption | TSelectOption[]): void { - if (!Array.isArray(e) && e !== null) { - selections.queryVrf.set(e); - onChange({ field: 'query_vrf', value: e.value }); - } else { - selections.queryVrf.set(null); - } - } - - return ( - <Select - size="lg" - name="query_vrf" - options={options} - aria-label={label} - onChange={handleChange} - value={exportState(selections.queryVrf.value)} - /> - ); -}; diff --git a/hyperglass/ui/components/form/resolvedTarget.tsx b/hyperglass/ui/components/form/resolvedTarget.tsx deleted file mode 100644 index eab1e76dd..000000000 --- a/hyperglass/ui/components/form/resolvedTarget.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useEffect, useMemo } from 'react'; -import dynamic from 'next/dynamic'; -import { Button, chakra, Stack, Text, VStack } from '@chakra-ui/react'; -import { useConfig, useColorValue } from '~/context'; -import { useStrf, useLGState, useDNSQuery } from '~/hooks'; - -const RightArrow = chakra( - dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaArrowCircleRight)), -); - -const LeftArrow = chakra( - dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaArrowCircleLeft)), -); - -import type { DnsOverHttps } from '~/types'; -import type { TResolvedTarget } from './types'; - -function findAnswer(data: DnsOverHttps.Response | undefined): string { - let answer = ''; - if (typeof data !== 'undefined') { - answer = data?.Answer?.filter(answerData => answerData.type === data?.Question[0]?.type)[0] - ?.data; - } - return answer; -} - -export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget) => { - const { setTarget, errorClose } = props; - const { web } = useConfig(); - const { displayTarget, isSubmitting, families, queryTarget } = useLGState(); - - const color = useColorValue('secondary.500', 'secondary.300'); - const errorColor = useColorValue('red.500', 'red.300'); - - const query4 = Array.from(families.value).includes(4); - const query6 = Array.from(families.value).includes(6); - - const tooltip4 = useStrf(web.text.fqdn_tooltip, { protocol: 'IPv4' }); - const tooltip6 = useStrf(web.text.fqdn_tooltip, { protocol: 'IPv6' }); - - const [messageStart, messageEnd] = web.text.fqdn_message.split('{fqdn}'); - const [errorStart, errorEnd] = web.text.fqdn_error.split('{fqdn}'); - - const { data: data4, isLoading: isLoading4, isError: isError4, error: error4 } = useDNSQuery( - displayTarget.value, - 4, - ); - - const { data: data6, isLoading: isLoading6, isError: isError6, error: error6 } = useDNSQuery( - displayTarget.value, - 6, - ); - - isError4 && console.error(error4); - isError6 && console.error(error6); - - const answer4 = useMemo(() => findAnswer(data4), [data4]); - const answer6 = useMemo(() => findAnswer(data6), [data6]); - - function handleOverride(value: string): void { - setTarget({ field: 'query_target', value }); - } - function selectTarget(value: string): void { - queryTarget.set(value); - isSubmitting.set(true); - } - - useEffect(() => { - if (query6 && data6?.Answer) { - handleOverride(findAnswer(data6)); - } else if (query4 && data4?.Answer && !query6 && !data6?.Answer) { - handleOverride(findAnswer(data4)); - } else if (query4 && data4?.Answer) { - handleOverride(findAnswer(data4)); - } - }, [data4, data6]); - - return ( - <VStack w="100%" spacing={4} justify="center"> - {(answer4 || answer6) && ( - <Text fontSize="sm" textAlign="center"> - {messageStart} - <Text as="span" fontSize="sm" fontWeight="bold" color={color}> - {`${displayTarget.value}`.toLowerCase()} - </Text> - {messageEnd} - </Text> - )} - <Stack spacing={2}> - {!isLoading4 && !isError4 && query4 && answer4 && ( - <Button - size="sm" - fontSize="xs" - title={tooltip4} - fontFamily="mono" - colorScheme="primary" - justifyContent="space-between" - onClick={() => selectTarget(answer4)} - rightIcon={<RightArrow boxSize="18px" />} - > - {answer4} - </Button> - )} - {!isLoading6 && !isError6 && query6 && answer6 && ( - <Button - size="sm" - fontSize="xs" - title={tooltip6} - fontFamily="mono" - colorScheme="secondary" - justifyContent="space-between" - onClick={() => selectTarget(answer6)} - rightIcon={<RightArrow boxSize="18px" />} - > - {answer6} - </Button> - )} - {!answer4 && !answer6 && ( - <> - <Text fontSize="sm" textAlign="center" color={errorColor}> - {errorStart} - <Text as="span" fontSize="sm" fontWeight="bold"> - {`${displayTarget.value}`.toLowerCase()} - </Text> - {errorEnd} - </Text> - <Button - colorScheme="red" - variant="outline" - onClick={errorClose} - leftIcon={<LeftArrow />} - > - {web.text.fqdn_error_button} - </Button> - </> - )} - </Stack> - </VStack> - ); -}; diff --git a/hyperglass/ui/components/form/types.ts b/hyperglass/ui/components/form/types.ts deleted file mode 100644 index 634edfd5d..000000000 --- a/hyperglass/ui/components/form/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { FormControlProps } from '@chakra-ui/react'; -import type { UseFormRegister } from 'react-hook-form'; -import type { TDeviceVrf, TBGPCommunity, OnChangeArgs, TFormData } from '~/types'; - -export interface TField extends FormControlProps { - name: string; - label: string; - hiddenLabels?: boolean; - labelAddOn?: React.ReactNode; - fieldAddOn?: React.ReactNode; -} - -export type OnChange = (f: OnChangeArgs) => void; - -export interface TQuerySelectField { - onChange: OnChange; - label: string; -} - -export interface TQueryVrf extends TQuerySelectField { - vrfs: TDeviceVrf[]; -} - -export interface TCommunitySelect { - name: string; - onChange: OnChange; - communities: TBGPCommunity[]; - register: UseFormRegister<TFormData>; -} - -export interface TQueryTarget { - name: string; - placeholder: string; - register: UseFormRegister<TFormData>; - onChange(e: OnChangeArgs): void; -} - -export interface TResolvedTarget { - setTarget(e: OnChangeArgs): void; - errorClose(): void; -} diff --git a/hyperglass/ui/components/greeting.tsx b/hyperglass/ui/components/greeting.tsx new file mode 100644 index 000000000..7ee576aab --- /dev/null +++ b/hyperglass/ui/components/greeting.tsx @@ -0,0 +1,70 @@ +import { useEffect } from 'react'; +import { + Modal, + Button, + ModalBody, + ModalHeader, + ModalFooter, + ModalOverlay, + ModalContent, + ModalCloseButton, +} from '@chakra-ui/react'; +import { If, Then } from 'react-if'; +import { Markdown } from '~/elements'; +import { useConfig } from '~/context'; +import { useGreeting, useColorValue, useOpposingColor } from '~/hooks'; + +import type { ModalContentProps } from '@chakra-ui/react'; + +export const Greeting = (props: ModalContentProps): JSX.Element => { + const { web, content } = useConfig(); + const { isAck, isOpen, open, ack } = useGreeting(); + + const bg = useColorValue('white', 'gray.800'); + const color = useOpposingColor(bg); + + useEffect(() => { + if (!web.greeting.enable && !web.greeting.required) { + ack(true, false); + } + if (!isAck && web.greeting.enable) { + open(); + } + }, [isAck, open, web.greeting.enable, web.greeting.required, ack]); + return ( + <Modal + size="lg" + isCentered + onClose={() => ack(false, web.greeting.required)} + isOpen={isOpen} + motionPreset="slideInBottom" + closeOnEsc={web.greeting.required} + closeOnOverlayClick={web.greeting.required} + > + <ModalOverlay /> + <ModalContent + py={4} + bg={bg} + color={color} + borderRadius="md" + maxW={{ base: '95%', md: '75%' }} + {...props} + > + <ModalHeader>{web.greeting.title}</ModalHeader> + <If condition={!web.greeting.required}> + <Then> + <ModalCloseButton /> + </Then> + </If> + <ModalBody> + <Markdown content={content.greeting} /> + </ModalBody> + <ModalFooter> + <Button colorScheme="primary" onClick={() => ack(true, web.greeting.required)}> + {web.greeting.button} + </Button> + </ModalFooter> + </ModalContent> + </Modal> + ); +}; diff --git a/hyperglass/ui/components/greeting/greeting.tsx b/hyperglass/ui/components/greeting/greeting.tsx deleted file mode 100644 index 165eee8e9..000000000 --- a/hyperglass/ui/components/greeting/greeting.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useEffect } from 'react'; -import { - Modal, - Button, - ModalBody, - ModalHeader, - ModalFooter, - ModalOverlay, - ModalContent, - ModalCloseButton, -} from '@chakra-ui/react'; -import { If, Markdown } from '~/components'; -import { useConfig, useColorValue } from '~/context'; -import { useGreeting, useOpposingColor } from '~/hooks'; - -import type { TGreeting } from './types'; - -export const Greeting: React.FC<TGreeting> = (props: TGreeting) => { - const { web, content } = useConfig(); - const { ack: greetingAck, isOpen, close } = useGreeting(); - - const bg = useColorValue('white', 'gray.800'); - const color = useOpposingColor(bg); - - function handleClose(ack: boolean = false): void { - if (web.greeting.required && !greetingAck.value && !ack) { - greetingAck.set(false); - } else if (web.greeting.required && !greetingAck.value && ack) { - greetingAck.set(true); - close(); - } else if (web.greeting.required && greetingAck.value) { - close(); - } else if (!web.greeting.required) { - greetingAck.set(true); - close(); - } - } - useEffect(() => { - if (!greetingAck.value && web.greeting.enable) { - isOpen.set(true); - } - }, []); - return ( - <Modal - size="lg" - isCentered - onClose={handleClose} - isOpen={isOpen.value} - motionPreset="slideInBottom" - closeOnEsc={web.greeting.required} - closeOnOverlayClick={web.greeting.required} - > - <ModalOverlay /> - <ModalContent - py={4} - bg={bg} - color={color} - borderRadius="md" - maxW={{ base: '95%', md: '75%' }} - {...props} - > - <ModalHeader>{web.greeting.title}</ModalHeader> - <If c={!web.greeting.required}> - <ModalCloseButton /> - </If> - <ModalBody> - <Markdown content={content.greeting} /> - </ModalBody> - <ModalFooter> - <Button colorScheme="primary" onClick={() => handleClose(true)}> - {web.greeting.button} - </Button> - </ModalFooter> - </ModalContent> - </Modal> - ); -}; diff --git a/hyperglass/ui/components/greeting/index.ts b/hyperglass/ui/components/greeting/index.ts deleted file mode 100644 index fe2d50b6e..000000000 --- a/hyperglass/ui/components/greeting/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './greeting'; diff --git a/hyperglass/ui/components/greeting/types.ts b/hyperglass/ui/components/greeting/types.ts deleted file mode 100644 index 1568d2abb..000000000 --- a/hyperglass/ui/components/greeting/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { BoxProps } from '@chakra-ui/react'; - -export interface TGreeting extends BoxProps {} diff --git a/hyperglass/ui/components/header/header.tsx b/hyperglass/ui/components/header/header.tsx index 07aba3ba5..b36470a38 100644 --- a/hyperglass/ui/components/header/header.tsx +++ b/hyperglass/ui/components/header/header.tsx @@ -1,43 +1,34 @@ -import { useRef } from 'react'; import { Flex, ScaleFade } from '@chakra-ui/react'; -import { AnimatedDiv } from '~/components'; -import { useBreakpointValue } from '~/context'; -import { useBooleanValue, useLGState } from '~/hooks'; +import { motionChakra } from '~/elements'; +import { useBooleanValue, useBreakpointValue, useFormInteractive } from '~/hooks'; import { Title } from './title'; -import type { THeader } from './types'; +const Wrapper = motionChakra('header', { + baseStyle: { display: 'flex', px: 4, pt: 6, minH: 16, w: 'full', flex: '0 1 auto' }, +}); -export const Header: React.FC<THeader> = (props: THeader) => { - const { resetForm, ...rest } = props; - - const { isSubmitting } = useLGState(); - - const titleRef = useRef({} as HTMLDivElement); +export const Header = (): JSX.Element => { + const formInteractive = useFormInteractive(); const titleWidth = useBooleanValue( - isSubmitting.value, + formInteractive, { base: '75%', lg: '50%' }, { base: '75%', lg: '75%' }, ); - const justify = useBreakpointValue({ base: 'flex-start', lg: 'center' }); - return ( - <Flex px={4} pt={6} minH={16} zIndex={4} as="header" width="full" flex="0 1 auto" {...rest}> + <Wrapper layout="position"> <ScaleFade in initialScale={0.5} style={{ width: '100%' }}> - <AnimatedDiv - layout + <Flex height="100%" - display="flex" - ref={titleRef} maxW={titleWidth} // This is here for the logo - justifyContent={justify} - mx={{ base: isSubmitting.value ? 'auto' : 0, lg: 'auto' }} + justifyContent="center" + mx="auto" > <Title /> - </AnimatedDiv> + </Flex> </ScaleFade> - </Flex> + </Wrapper> ); }; diff --git a/hyperglass/ui/components/header/logo.tsx b/hyperglass/ui/components/header/logo.tsx index d26a8bb10..a5aa6ecdf 100644 --- a/hyperglass/ui/components/header/logo.tsx +++ b/hyperglass/ui/components/header/logo.tsx @@ -1,40 +1,33 @@ -import { useMemo, useState } from 'react'; import { Image, Skeleton } from '@chakra-ui/react'; -import { useColorValue, useConfig, useColorMode } from '~/context'; +import { useCallback, useMemo, useState } from 'react'; +import { useConfig } from '~/context'; +import { useColorValue } from '~/hooks'; -import type { TLogo } from './types'; +import type { ImageProps } from '@chakra-ui/react'; /** * Custom hook to handle loading the user's logo, errors loading the logo, and color mode changes. */ function useLogo(): [string, () => void] { const { web } = useConfig(); - const { dark_format, light_format } = web.logo; - const { colorMode } = useColorMode(); + const { darkFormat, lightFormat } = web.logo; - const src = useColorValue(`/images/dark${dark_format}`, `/images/light${light_format}`); + const src = useColorValue(`/images/light${darkFormat}`, `/images/dark${lightFormat}`); // Use the hyperglass logo if the user's logo can't be loaded for whatever reason. - const fallbackSrc = useColorValue( - 'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-dark.svg', - 'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-light.svg', - ); - const [fallback, setSource] = useState<string | null>(null); - /** - * If the user image cannot be loaded, log an error to the console and set the fallback image. - */ - function setFallback() { + // If the user image cannot be loaded, log an error to the console and set the fallback image. + const setFallback = useCallback(() => { console.warn(`Error loading image from '${src}'`); - setSource(fallbackSrc); - } + setSource('https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-light.svg'); + }, [src]); // Only return the fallback image if it's been set. - return useMemo(() => [fallback ?? src, setFallback], [colorMode]); + return useMemo(() => [fallback ?? src, setFallback], [fallback, setFallback, src]); } -export const Logo: React.FC<TLogo> = (props: TLogo) => { +export const Logo = (props: ImageProps): JSX.Element => { const { web } = useConfig(); const { width } = web.logo; @@ -48,7 +41,8 @@ export const Logo: React.FC<TLogo> = (props: TLogo) => { src={source} alt={web.text.title} onError={setFallback} - width={width ?? 'auto'} + maxW={{ base: '100%', md: width }} + width="auto" css={{ userDrag: 'none', userSelect: 'none', diff --git a/hyperglass/ui/components/header/subtitleOnly.tsx b/hyperglass/ui/components/header/subtitle-only.tsx similarity index 69% rename from hyperglass/ui/components/header/subtitleOnly.tsx rename to hyperglass/ui/components/header/subtitle-only.tsx index 4f2d09f18..0f9a0cb0f 100644 --- a/hyperglass/ui/components/header/subtitleOnly.tsx +++ b/hyperglass/ui/components/header/subtitle-only.tsx @@ -1,8 +1,9 @@ import { Heading } from '@chakra-ui/react'; -import { useConfig, useBreakpointValue } from '~/context'; -import { useTitleSize } from './useTitleSize'; +import { useConfig } from '~/context'; +import { useBreakpointValue } from '~/hooks'; +import { useTitleSize } from './use-title-size'; -export const SubtitleOnly: React.FC = () => { +export const SubtitleOnly = (): JSX.Element => { const { web } = useConfig(); const sizeSm = useTitleSize(web.text.subtitle, 'sm'); const fontSize = useBreakpointValue({ base: sizeSm, lg: 'xl' }); diff --git a/hyperglass/ui/components/header/titleOnly.tsx b/hyperglass/ui/components/header/title-only.tsx similarity index 53% rename from hyperglass/ui/components/header/titleOnly.tsx rename to hyperglass/ui/components/header/title-only.tsx index c26399978..4b12d1ac1 100644 --- a/hyperglass/ui/components/header/titleOnly.tsx +++ b/hyperglass/ui/components/header/title-only.tsx @@ -1,13 +1,12 @@ import { Heading } from '@chakra-ui/react'; import { useConfig } from '~/context'; -import { useBooleanValue, useLGState } from '~/hooks'; -import { useTitleSize } from './useTitleSize'; +import { useBooleanValue, useFormInteractive } from '~/hooks'; +import { useTitleSize } from './use-title-size'; -export const TitleOnly: React.FC = () => { +export const TitleOnly = (): JSX.Element => { const { web } = useConfig(); - const { isSubmitting } = useLGState(); - - const margin = useBooleanValue(isSubmitting.value, 0, 2); + const formInteractive = useFormInteractive(); + const margin = useBooleanValue(formInteractive, 0, 2); const sizeSm = useTitleSize(web.text.title, '2xl', []); return ( diff --git a/hyperglass/ui/components/header/title.tsx b/hyperglass/ui/components/header/title.tsx index d06dfeabc..c69b03657 100644 --- a/hyperglass/ui/components/header/title.tsx +++ b/hyperglass/ui/components/header/title.tsx @@ -1,27 +1,33 @@ -import { Flex, Button, VStack } from '@chakra-ui/react'; +import { Button, Flex, VStack } from '@chakra-ui/react'; import { motion } from 'framer-motion'; import { isSafari } from 'react-device-detect'; -import { If } from '~/components'; -import { useConfig, useMobile } from '~/context'; -import { useBooleanValue, useLGState, useLGMethods } from '~/hooks'; -import { SubtitleOnly } from './subtitleOnly'; -import { TitleOnly } from './titleOnly'; +import { Case, Switch } from 'react-if'; +import { useConfig } from '~/context'; +import { useFormInteractive, useFormState, useMobile } from '~/hooks'; import { Logo } from './logo'; +import { SubtitleOnly } from './subtitle-only'; +import { TitleOnly } from './title-only'; -import type { TTitle, TTitleWrapper, TDWrapper, TMWrapper } from './types'; +import type { FlexProps, StackProps } from '@chakra-ui/react'; +import type { MotionProps } from 'framer-motion'; + +type DWrapperProps = Omit<StackProps, 'transition'> & MotionProps; +type MWrapperProps = Omit<StackProps, 'transition'> & MotionProps; +type WrapperProps = Partial<MotionProps & Omit<StackProps, 'transition'>>; const AnimatedVStack = motion(VStack); +const AnimatedFlex = motion(Flex); /** * Title wrapper for mobile devices, breakpoints sm & md. */ -const MWrapper: React.FC<TMWrapper> = (props: TMWrapper) => { - const { isSubmitting } = useLGState(); +const MWrapper = (props: MWrapperProps): JSX.Element => { + const formInteractive = useFormInteractive(); return ( <AnimatedVStack layout spacing={1} - alignItems={isSubmitting.value ? 'center' : 'flex-start'} + alignItems={formInteractive ? 'center' : 'flex-start'} {...props} /> ); @@ -30,16 +36,17 @@ const MWrapper: React.FC<TMWrapper> = (props: TMWrapper) => { /** * Title wrapper for desktop devices, breakpoints lg & xl. */ -const DWrapper: React.FC<TDWrapper> = (props: TDWrapper) => { - const { isSubmitting } = useLGState(); +const DWrapper = (props: DWrapperProps): JSX.Element => { + const formInteractive = useFormInteractive(); return ( <AnimatedVStack spacing={1} initial="main" alignItems="center" - animate={isSubmitting.value ? 'submitting' : 'main'} + animate={formInteractive} transition={{ damping: 15, type: 'spring', stiffness: 100 }} - variants={{ submitting: { scale: 0.5 }, main: { scale: 1 } }} + variants={{ results: { scale: 0.5 }, form: { scale: 1 } }} + maxWidth="75%" {...props} /> ); @@ -49,11 +56,15 @@ const DWrapper: React.FC<TDWrapper> = (props: TDWrapper) => { * Universal wrapper for title sub-components, which will be different depending on the * `title_mode` configuration variable. */ -const TitleWrapper: React.FC<TDWrapper | TMWrapper> = (props: TDWrapper | TMWrapper) => { +const TitleWrapper = (props: DWrapperProps | MWrapperProps): JSX.Element => { const isMobile = useMobile(); return ( <> - {isMobile ? <MWrapper {...(props as TMWrapper)} /> : <DWrapper {...(props as TDWrapper)} />} + {isMobile ? ( + <MWrapper {...(props as MWrapperProps)} /> + ) : ( + <DWrapper {...(props as DWrapperProps)} /> + )} </> ); }; @@ -61,7 +72,7 @@ const TitleWrapper: React.FC<TDWrapper | TMWrapper> = (props: TDWrapper | TMWrap /** * Title sub-component if `title_mode` is set to `text_only`. */ -const TextOnly: React.FC<TTitleWrapper> = (props: TTitleWrapper) => { +const TextOnly = (props: WrapperProps): JSX.Element => { return ( <TitleWrapper {...props}> <TitleOnly /> @@ -73,8 +84,8 @@ const TextOnly: React.FC<TTitleWrapper> = (props: TTitleWrapper) => { /** * Title sub-component if `title_mode` is set to `logo_only`. Renders only the logo. */ -const LogoOnly: React.FC = () => ( - <TitleWrapper> +const LogoOnly = (props: WrapperProps): JSX.Element => ( + <TitleWrapper {...props}> <Logo /> </TitleWrapper> ); @@ -83,8 +94,8 @@ const LogoOnly: React.FC = () => ( * Title sub-component if `title_mode` is set to `logo_subtitle`. Renders the logo with the * subtitle underneath. */ -const LogoSubtitle: React.FC = () => ( - <TitleWrapper> +const LogoSubtitle = (props: WrapperProps): JSX.Element => ( + <TitleWrapper {...props}> <Logo /> <SubtitleOnly /> </TitleWrapper> @@ -93,8 +104,8 @@ const LogoSubtitle: React.FC = () => ( /** * Title sub-component if `title_mode` is set to `all`. Renders the logo, title, and subtitle. */ -const All: React.FC = () => ( - <TitleWrapper> +const All = (props: WrapperProps): JSX.Element => ( + <TitleWrapper {...props}> <Logo /> <TextOnly mt={2} /> </TitleWrapper> @@ -103,27 +114,20 @@ const All: React.FC = () => ( /** * Title component which renders sub-components based on the `title_mode` configuration variable. */ -export const Title: React.FC<TTitle> = (props: TTitle) => { +export const Title = (props: FlexProps): JSX.Element => { const { fontSize, ...rest } = props; const { web } = useConfig(); - const titleMode = web.text.title_mode; - - const { isSubmitting } = useLGState(); - const { resetForm } = useLGMethods(); - - const titleHeight = useBooleanValue(isSubmitting.value, undefined, { md: '20vh' }); + const { titleMode } = web.text; - function handleClick(): void { - isSubmitting.set(false); - resetForm(); - } + const reset = useFormState(s => s.reset); + const formInteractive = useFormInteractive(); return ( - <Flex + <AnimatedFlex px={0} flexWrap="wrap" flexDir="column" - minH={titleHeight} + animate={{ height: formInteractive ? undefined : '20vh' }} justifyContent="center" /* flexBasis This is a fix for Safari specifically. LMGTFY: Safari flex-basis width. Nutshell: Safari @@ -132,7 +136,7 @@ export const Title: React.FC<TTitle> = (props: TTitle) => { div up to the parent's max-width. The fix is to hard-code a flex-basis width. */ flexBasis={{ base: '100%', lg: isSafari ? '33%' : '100%' }} - mt={[null, isSubmitting.value ? null : 'auto']} + mt={{ md: formInteractive ? undefined : 'auto' }} {...rest} > <Button @@ -140,23 +144,25 @@ export const Title: React.FC<TTitle> = (props: TTitle) => { variant="link" flexWrap="wrap" flexDir="column" - onClick={handleClick} + onClick={async () => await reset()} _focus={{ boxShadow: 'none' }} _hover={{ textDecoration: 'none' }} > - <If c={titleMode === 'text_only'}> - <TextOnly /> - </If> - <If c={titleMode === 'logo_only'}> - <LogoOnly /> - </If> - <If c={titleMode === 'logo_subtitle'}> - <LogoSubtitle /> - </If> - <If c={titleMode === 'all'}> - <All /> - </If> + <Switch> + <Case condition={titleMode === 'text_only'}> + <TextOnly width={web.logo.width} /> + </Case> + <Case condition={titleMode === 'logo_only'}> + <LogoOnly width={web.logo.width} /> + </Case> + <Case condition={titleMode === 'logo_subtitle'}> + <LogoSubtitle width={web.logo.width} /> + </Case> + <Case condition={titleMode === 'all'}> + <All width={web.logo.width} /> + </Case> + </Switch> </Button> - </Flex> + </AnimatedFlex> ); }; diff --git a/hyperglass/ui/components/header/types.ts b/hyperglass/ui/components/header/types.ts deleted file mode 100644 index afdcf4cc9..000000000 --- a/hyperglass/ui/components/header/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { FlexProps, HeadingProps, ImageProps, StackProps } from '@chakra-ui/react'; -import type { MotionProps } from 'framer-motion'; - -export interface THeader extends FlexProps { - resetForm(): void; -} - -export type THeaderLayout = { - sm: [JSX.Element, JSX.Element]; - md: [JSX.Element, JSX.Element]; - lg: [JSX.Element, JSX.Element]; - xl: [JSX.Element, JSX.Element]; -}; -export type TDWrapper = Omit<StackProps, 'transition'> & MotionProps; - -export type TMWrapper = Omit<StackProps, 'transition'> & MotionProps; - -export interface TTitle extends FlexProps {} - -export interface TTitleOnly extends HeadingProps {} - -export interface TLogo extends ImageProps {} - -export interface TTitleWrapper extends Partial<MotionProps & Omit<StackProps, 'transition'>> {} - -export interface THeaderCtx { - showSubtitle: boolean; - titleRef: React.MutableRefObject<HTMLHeadingElement>; -} diff --git a/hyperglass/ui/components/header/useTitleSize.ts b/hyperglass/ui/components/header/use-title-size.ts similarity index 90% rename from hyperglass/ui/components/header/useTitleSize.ts rename to hyperglass/ui/components/header/use-title-size.ts index 23d00a717..e6223c906 100644 --- a/hyperglass/ui/components/header/useTitleSize.ts +++ b/hyperglass/ui/components/header/use-title-size.ts @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { useToken } from '@chakra-ui/react'; -import { useMobile } from '~/context'; +import { useMobile } from '~/hooks'; // Mobile: // xs: 32 @@ -55,5 +55,6 @@ export function useTitleSize(title: string, defaultSize: Sizes, deps: unknown[] return useMemo(() => { getSize(title.length); return realSize; - }, [title, isMobile, ...deps]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [title, isMobile, realSize, ...deps]); } diff --git a/hyperglass/ui/components/help/index.ts b/hyperglass/ui/components/help/index.ts deleted file mode 100644 index 133aa7420..000000000 --- a/hyperglass/ui/components/help/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './modal'; diff --git a/hyperglass/ui/components/help/types.ts b/hyperglass/ui/components/help/types.ts deleted file mode 100644 index 2a47dfce3..000000000 --- a/hyperglass/ui/components/help/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ModalContentProps } from '@chakra-ui/react'; -import type { TQueryContent, TQueryFields } from '~/types'; - -export interface THelpModal extends ModalContentProps { - item: TQueryContent | null; - name: TQueryFields; - visible: boolean; -} diff --git a/hyperglass/ui/components/index.ts b/hyperglass/ui/components/index.ts index 17216b1b8..6c0f318c4 100644 --- a/hyperglass/ui/components/index.ts +++ b/hyperglass/ui/components/index.ts @@ -1,22 +1,30 @@ -export * from './card'; -export * from './codeBlock'; -export * from './countdown'; +/** + * The components directory contains React components that handle logic. + * + * Generally, components that call hooks or reference configuration, or API types should be in + * components. + */ + export * from './debugger'; +export * from './directive-info-modal'; export * from './footer'; -export * from './form'; +export * from './form-field'; export * from './greeting'; export * from './header'; -export * from './help'; -export * from './label'; export * from './layout'; -export * from './loading'; -export * from './lookingGlass'; -export * from './markdown'; +export * from './location-card'; +export * from './looking-glass-form'; export * from './meta'; export * from './output'; export * from './path'; +export * from './prompt'; +export * from './query-location'; +export * from './query-target'; +export * from './query-type'; +export * from './reset-button'; +export * from './resolved-target'; export * from './results'; export * from './select'; -export * from './submit'; +export * from './submit-button'; export * from './table'; -export * from './util'; +export * from './user-ip'; diff --git a/hyperglass/ui/components/label/index.ts b/hyperglass/ui/components/label/index.ts deleted file mode 100644 index 301fbded1..000000000 --- a/hyperglass/ui/components/label/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './label'; diff --git a/hyperglass/ui/components/label/types.ts b/hyperglass/ui/components/label/types.ts deleted file mode 100644 index 2069c4c2b..000000000 --- a/hyperglass/ui/components/label/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FlexProps } from '@chakra-ui/react'; - -export interface TLabel extends FlexProps { - value: string; - label: string; - bg: string; - valueColor?: string; - labelColor?: string; -} diff --git a/hyperglass/ui/components/layout.tsx b/hyperglass/ui/components/layout.tsx new file mode 100644 index 000000000..585a73a74 --- /dev/null +++ b/hyperglass/ui/components/layout.tsx @@ -0,0 +1,76 @@ +import { Flex } from '@chakra-ui/react'; +import { useCallback, useRef } from 'react'; +import { isSafari } from 'react-device-detect'; +import { If, Then } from 'react-if'; +import { Debugger, Footer, Greeting, Header, ResetButton } from '~/components'; +import { useConfig } from '~/context'; +import { motionChakra } from '~/elements'; +import { useFormState } from '~/hooks'; + +import type { FlexProps } from '@chakra-ui/react'; + +const Main = motionChakra('main', { + baseStyle: { + px: 4, + py: 0, + w: '100%', + display: 'flex', + flex: '1 1 auto', + flexDir: 'column', + textAlign: 'center', + alignItems: 'center', + justifyContent: 'start', + }, +}); + +export const Layout = (props: FlexProps): JSX.Element => { + const { developerMode } = useConfig(); + const { setStatus, reset } = useFormState( + useCallback(({ setStatus, reset }) => ({ setStatus, reset }), []), + ); + + const containerRef = useRef<HTMLDivElement>({} as HTMLDivElement); + + async function handleReset() { + containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); + setStatus('form'); + reset(); + } + + return ( + <> + <Flex + w="100%" + flex="1 0 auto" + flexDir="column" + id="__hyperglass" + ref={containerRef} + /** minHeight + * This is a Safari-specific fix. Without it, the footer will appear to be "under" the + * viewport. Safari needs `-webkit-fill-available`, but other browsers need `100vh`. + * @see https://allthingssmitty.com/2020/05/11/css-fix-for-100vh-in-mobile-webkit/ + */ + minHeight={isSafari ? '-webkit-fill-available' : '100vh'} + > + <Header /> + <Main + layout + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.3 }} + exit={{ opacity: 0, x: -300 }} + initial={{ opacity: 0, y: 300 }} + > + {props.children} + </Main> + <Footer /> + <If condition={developerMode}> + <Then> + <Debugger /> + </Then> + </If> + <ResetButton developerMode={developerMode} resetForm={handleReset} /> + </Flex> + <Greeting /> + </> + ); +}; diff --git a/hyperglass/ui/components/layout/frame.tsx b/hyperglass/ui/components/layout/frame.tsx deleted file mode 100644 index d84f7f9b7..000000000 --- a/hyperglass/ui/components/layout/frame.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useRef } from 'react'; -import { Flex } from '@chakra-ui/react'; -import { isSafari } from 'react-device-detect'; -import { If, Debugger, Greeting, Footer, Header } from '~/components'; -import { useConfig } from '~/context'; -import { useLGState, useLGMethods } from '~/hooks'; -import { ResetButton } from './resetButton'; - -import type { TFrame } from './types'; - -export const Frame: React.FC<TFrame> = (props: TFrame) => { - const { developer_mode } = useConfig(); - const { isSubmitting } = useLGState(); - const { resetForm } = useLGMethods(); - - const containerRef = useRef<HTMLDivElement>({} as HTMLDivElement); - - function handleReset(): void { - containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); - isSubmitting.set(false); - resetForm(); - } - - return ( - <> - <Flex - w="100%" - flex="1 0 auto" - flexDir="column" - id="__hyperglass" - ref={containerRef} - /** minHeight - * This is a Safari-specific fix. Without it, the footer will appear to be "under" the - * viewport. Safari needs `-webkit-fill-available`, but other browsers need `100vh`. - * @see https://allthingssmitty.com/2020/05/11/css-fix-for-100vh-in-mobile-webkit/ - */ - minHeight={isSafari ? '-webkit-fill-available' : '100vh'} - > - <Header resetForm={handleReset} /> - <Flex - px={4} - py={0} - w="100%" - as="main" - align="center" - flex="1 1 auto" - justify="start" - flexDir="column" - textAlign="center" - {...props} - /> - <Footer /> - <If c={developer_mode}> - <Debugger /> - </If> - <ResetButton developerMode={developer_mode} resetForm={handleReset} /> - </Flex> - <Greeting /> - </> - ); -}; diff --git a/hyperglass/ui/components/layout/index.ts b/hyperglass/ui/components/layout/index.ts deleted file mode 100644 index aee1748fa..000000000 --- a/hyperglass/ui/components/layout/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './frame'; -export * from './layout'; diff --git a/hyperglass/ui/components/layout/layout.tsx b/hyperglass/ui/components/layout/layout.tsx deleted file mode 100644 index 6810f3f26..000000000 --- a/hyperglass/ui/components/layout/layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { AnimatePresence } from 'framer-motion'; -import { LookingGlass, Results } from '~/components'; -import { useLGMethods } from '~/hooks'; -import { Frame } from './frame'; - -export const Layout: React.FC = () => { - const { formReady } = useLGMethods(); - return ( - <Frame> - {formReady() ? ( - <Results /> - ) : ( - <AnimatePresence> - <LookingGlass /> - </AnimatePresence> - )} - </Frame> - ); -}; diff --git a/hyperglass/ui/components/layout/types.ts b/hyperglass/ui/components/layout/types.ts deleted file mode 100644 index bf3ee75fa..000000000 --- a/hyperglass/ui/components/layout/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { FlexProps } from '@chakra-ui/react'; - -export interface TFrame extends FlexProps {} - -export interface TResetButton extends FlexProps { - developerMode: boolean; - resetForm(): void; -} diff --git a/hyperglass/ui/components/loading.tsx b/hyperglass/ui/components/loading.tsx deleted file mode 100644 index 44b32020c..000000000 --- a/hyperglass/ui/components/loading.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Flex, Spinner } from '@chakra-ui/react'; - -import type { LoadableBaseOptions } from 'next/dynamic'; - -export const Loading: LoadableBaseOptions['loading'] = () => ( - <Flex flexDirection="column" minHeight="100vh" w="100%"> - <Flex - px={2} - py={0} - w="100%" - bg="white" - color="black" - flex="1 1 auto" - textAlign="center" - alignItems="center" - justifyContent="center" - flexDirection="column" - css={{ - '@media (prefers-color-scheme: dark)': { backgroundColor: 'black', color: 'white' }, - }} - > - <Spinner color="primary.500" w="6rem" h="6rem" /> - </Flex> - </Flex> -); diff --git a/hyperglass/ui/components/location-card.tsx b/hyperglass/ui/components/location-card.tsx new file mode 100644 index 000000000..6797ac94f --- /dev/null +++ b/hyperglass/ui/components/location-card.tsx @@ -0,0 +1,107 @@ +import { useMemo, useState } from 'react'; +import { Flex, Avatar, chakra } from '@chakra-ui/react'; +import { motionChakra } from '~/elements'; +import { useColorValue, useOpposingColor } from '~/hooks'; + +import type { SingleOption } from '~/types'; +import type { LocationOption } from './query-location'; + +interface LocationCardProps { + option: SingleOption; + defaultChecked: boolean; + onChange(a: 'add' | 'remove', v: SingleOption): void; + hasError: boolean; +} + +const LocationCardWrapper = motionChakra('div', { + baseStyle: { + py: 4, + px: 6, + minW: 'xs', + maxW: 'md', + mx: 'auto', + shadow: 'sm', + rounded: 'lg', + cursor: 'pointer', + borderWidth: '1px', + borderStyle: 'solid', + }, +}); + +export const LocationCard = (props: LocationCardProps): JSX.Element => { + const { option, onChange, defaultChecked, hasError } = props; + const { label } = option; + const [isChecked, setChecked] = useState(defaultChecked); + + function handleChange(value: LocationOption) { + if (isChecked) { + setChecked(false); + onChange('remove', value); + } else { + setChecked(true); + onChange('add', value); + } + } + + const bg = useColorValue('white', 'blackSolid.600'); + const imageBorder = useColorValue('gray.600', 'whiteAlpha.800'); + const fg = useOpposingColor(bg); + const checkedBorder = useColorValue('blue.400', 'blue.300'); + const errorBorder = useColorValue('red.500', 'red.300'); + + const borderColor = useMemo( + () => + hasError && isChecked + ? // Highlight red when there are no overlapping query types for the locations selected. + errorBorder + : isChecked && !hasError + ? // Highlight blue when any location is selected and there is no error. + checkedBorder + : // Otherwise, no border. + 'transparent', + + [hasError, isChecked, checkedBorder, errorBorder], + ); + return ( + <LocationCardWrapper + bg={bg} + key={label} + whileHover={{ scale: 1.05 }} + borderColor={borderColor} + onClick={(e: React.MouseEvent) => { + e.preventDefault(); + handleChange(option); + }} + > + <> + <Flex justifyContent="space-between" alignItems="center"> + <chakra.h2 + color={fg} + fontWeight="bold" + mt={{ base: 2, md: 0 }} + fontSize={{ base: 'lg', md: 'xl' }} + > + {label} + </chakra.h2> + <Avatar + color={fg} + name={label} + boxSize={12} + rounded="full" + borderWidth={1} + bg="whiteAlpha.300" + borderStyle="solid" + borderColor={imageBorder} + src={(option.data?.avatar as string) ?? undefined} + /> + </Flex> + + {option?.data?.description && ( + <chakra.p mt={2} color={fg} opacity={0.6} fontSize="sm"> + {option.data.description as string} + </chakra.p> + )} + </> + </LocationCardWrapper> + ); +}; diff --git a/hyperglass/ui/components/looking-glass-form.tsx b/hyperglass/ui/components/looking-glass-form.tsx new file mode 100644 index 000000000..2da46338a --- /dev/null +++ b/hyperglass/ui/components/looking-glass-form.tsx @@ -0,0 +1,242 @@ +import { Flex, ScaleFade, SlideFade, chakra } from '@chakra-ui/react'; +import { vestResolver } from '@hookform/resolvers/vest'; +import { useCallback, useEffect, useMemo } from 'react'; +import isEqual from 'react-fast-compare'; +import { FormProvider, useForm } from 'react-hook-form'; +import vest, { test, enforce } from 'vest'; +import { + DirectiveInfoModal, + FormField, + QueryLocation, + QueryTarget, + QueryType, + SubmitButton, +} from '~/components'; +import { useConfig } from '~/context'; +import { FormRow } from '~/elements'; +import { useDevice, useFormState, useGreeting, useStrf } from '~/hooks'; +import { Directive, isQueryField, isString } from '~/types'; +import { isFQDN } from '~/util'; + +import type { FormData, OnChangeArgs } from '~/types'; + +export const LookingGlassForm = (): JSX.Element => { + const { web, messages } = useConfig(); + + const greetingReady = useGreeting(s => s.greetingReady); + + const getDevice = useDevice(); + const strF = useStrf(); + const setLoading = useFormState(s => s.setLoading); + const setStatus = useFormState(s => s.setStatus); + const locationChange = useFormState(s => s.locationChange); + const setTarget = useFormState(s => s.setTarget); + const setFormValue = useFormState(s => s.setFormValue); + const { form, filtered, selections } = useFormState( + useCallback(({ form, filtered, selections }) => ({ form, filtered, selections }), []), + isEqual, + ); + + const getDirective = useFormState(useCallback(s => s.getDirective, [])); + const resolvedOpen = useFormState(useCallback(s => s.resolvedOpen, [])); + const resetForm = useFormState(useCallback(s => s.reset, [])); + + const noQueryType = strF(messages.noInput, { field: web.text.queryType }); + const noQueryLoc = strF(messages.noInput, { field: web.text.queryLocation }); + const noQueryTarget = strF(messages.noInput, { field: web.text.queryTarget }); + + const queryTypes = useMemo(() => filtered.types.map(t => t.id), [filtered.types]); + + const formSchema = vest.create((data: FormData = {} as FormData) => { + test('queryLocation', noQueryLoc, () => { + enforce(data.queryLocation).isArrayOf(enforce.isString()).isNotEmpty(); + }); + test('queryTarget', noQueryTarget, () => { + enforce(data.queryTarget).isArrayOf(enforce.isString()).isNotEmpty(); + }); + test('queryType', noQueryType, () => { + enforce(data.queryType).inside(queryTypes); + }); + }); + + const formInstance = useForm<FormData>({ + resolver: vestResolver(formSchema), + defaultValues: { + queryTarget: [], + queryLocation: [], + queryType: '', + }, + }); + + const { handleSubmit, register, setValue, setError, clearErrors } = formInstance; + + const isFqdnQuery = useCallback( + (target: string | string[], fieldType: Directive['fieldType'] | null): boolean => + (typeof target === 'string' || Array.isArray(target)) && + fieldType === 'text' && + isFQDN(target), + [], + ); + + const directive = useMemo<Directive | null>(() => { + const tmp = getDirective(); + if (tmp !== null && tmp.fieldType === null) { + setFormValue('queryTarget', ['null']); + setValue('queryTarget', ['null']); + } + return tmp; + }, [form.queryType, form.queryLocation, getDirective]); + + function submitHandler(): void { + if (process.env.NODE_ENV === 'development') { + console.table({ + 'Query Location': form.queryLocation.toString(), + 'Query Type': form.queryType, + 'Query Target': form.queryTarget, + 'Selected Directive': directive?.name ?? null, + }); + } + + // Before submitting a query, make sure the greeting is acknowledged if required. This should + // be handled before loading the app, but people be sneaky. + if (!greetingReady) { + resetForm(); + location.reload(); + return; + } + + // Determine if queryTarget is an FQDN. + const isFqdn = isFqdnQuery(form.queryTarget, directive?.fieldType ?? null); + + if (greetingReady && !isFqdn) { + setStatus('results'); + return; + } + + if (greetingReady && isFqdn) { + setLoading(true); + resolvedOpen(); + return; + } + console.group('%cSomething went wrong', 'color:red;'); + console.table({ + 'Greeting Required': web.greeting.required, + 'Greeting Ready': greetingReady, + 'Query Target': form.queryTarget, + 'Query Type': form.queryType, + 'Is FQDN': isFqdn, + }); + console.groupEnd(); + } + + const handleLocChange = (locations: string[]) => + locationChange(locations, { setError, clearErrors, getDevice, text: web.text }); + + function handleChange(e: OnChangeArgs): void { + // Signal the field & value to react-hook-form. + if (isQueryField(e.field)) { + setValue(e.field, e.value); + } else { + throw new Error(`Field '${e.field}' is not a valid form field.`); + } + + if (e.field === 'queryLocation' && Array.isArray(e.value)) { + handleLocChange(e.value); + } else if (e.field === 'queryType' && isString(e.value)) { + setValue('queryType', e.value); + setFormValue('queryType', e.value); + if (form.queryTarget.length !== 0) { + // Reset queryTarget as well, so that, for example, selecting BGP Community, and selecting + // a community, then changing the queryType to BGP Route doesn't preserve the selected + // community as the queryTarget. + setFormValue('queryTarget', []); + setTarget({ display: '' }); + } + } else if (e.field === 'queryTarget') { + if (isString(e.value)) { + setFormValue('queryTarget', [e.value]); + setValue('queryTarget', [e.value]); + } + if (Array.isArray(e.value)) { + setFormValue('queryTarget', e.value); + setValue('queryTarget', e.value); + } + } + } + + useEffect(() => { + register('queryLocation', { required: true }); + register('queryType', { required: true }); + }, [register]); + + return ( + <FormProvider {...formInstance}> + <chakra.form + p={0} + my={4} + w="100%" + mx="auto" + textAlign="left" + maxW={{ base: '100%', lg: '75%' }} + onSubmit={handleSubmit(submitHandler)} + > + <FormRow> + <FormField name="queryLocation" label={web.text.queryLocation}> + <QueryLocation onChange={handleChange} label={web.text.queryLocation} /> + </FormField> + </FormRow> + <FormRow> + <SlideFade offsetX={-100} in={filtered.types.length > 0} unmountOnExit> + <FormField + name="queryType" + label={web.text.queryType} + labelAddOn={ + directive !== null && ( + <DirectiveInfoModal + name="queryType" + title={directive.name ?? null} + item={directive.info ?? null} + visible={selections.queryType !== null && directive.info !== null} + /> + ) + } + > + <QueryType onChange={handleChange} label={web.text.queryType} /> + </FormField> + </SlideFade> + <SlideFade + offsetX={100} + in={directive !== null && directive.fieldType !== null} + unmountOnExit + > + {directive !== null && ( + <FormField name="queryTarget" label={web.text.queryTarget}> + <QueryTarget + name="queryTarget" + register={register} + onChange={handleChange} + placeholder={directive.description} + /> + </FormField> + )} + </SlideFade> + </FormRow> + <FormRow mt={0} justifyContent="flex-end"> + <Flex + my={2} + w="100%" + ml="auto" + maxW="100%" + flex="0 0 0" + flexDir="column" + mr={{ base: 0, lg: 2 }} + > + <ScaleFade initialScale={0.5} in={form.queryTarget.length !== 0}> + <SubmitButton /> + </ScaleFade> + </Flex> + </FormRow> + </chakra.form> + </FormProvider> + ); +}; diff --git a/hyperglass/ui/components/lookingGlass.tsx b/hyperglass/ui/components/lookingGlass.tsx deleted file mode 100644 index 6645f0ba5..000000000 --- a/hyperglass/ui/components/lookingGlass.tsx +++ /dev/null @@ -1,318 +0,0 @@ -import { useCallback, useEffect, useMemo } from 'react'; -import { Flex } from '@chakra-ui/react'; -import { FormProvider, useForm } from 'react-hook-form'; -import { intersectionWith } from 'lodash'; -import { vestResolver } from '@hookform/resolvers/vest'; -import vest, { test, enforce } from 'vest'; -import { - If, - FormRow, - QueryVrf, - FormField, - HelpModal, - QueryType, - AnimatedDiv, - QueryTarget, - SubmitButton, - QueryLocation, -} from '~/components'; -import { useConfig } from '~/context'; -import { useStrf, useGreeting, useDevice, useLGState, useLGMethods } from '~/hooks'; -import { isQueryType, isQueryContent, isString, isQueryField } from '~/types'; - -import type { TFormData, TDeviceVrf, OnChangeArgs } from '~/types'; - -/** - * Don't set the global flag on this. - * @see https://stackoverflow.com/questions/24084926/javascript-regexp-cant-use-twice - * - * TLDR: the test() will pass the first time, but not the second. In React Strict Mode & in a dev - * environment, this will mean isFqdn will be true the first time, then false the second time, - * submitting the FQDN to hyperglass the second time. - */ -const fqdnPattern = new RegExp( - /^(?!:\/\/)([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-][a-zA-Z0-9-]+\.[a-zA-Z-]{2,6}?$/im, -); - -function useIsFqdn(target: string, _type: string) { - return useCallback( - (): boolean => ['bgp_route', 'ping', 'traceroute'].includes(_type) && fqdnPattern.test(target), - [target, _type], - ); -} - -export const LookingGlass: React.FC = () => { - const { web, content, messages } = useConfig(); - - const { ack, greetingReady } = useGreeting(); - const getDevice = useDevice(); - - const noQueryType = useStrf(messages.no_input, { field: web.text.query_type }); - const noQueryLoc = useStrf(messages.no_input, { field: web.text.query_location }); - const noQueryTarget = useStrf(messages.no_input, { field: web.text.query_target }); - - const formSchema = vest.create((data: TFormData = {} as TFormData) => { - test('query_location', noQueryLoc, () => { - enforce(data.query_location).isArrayOf(enforce.isString()).isNotEmpty(); - }); - test('query_target', noQueryTarget, () => { - enforce(data.query_target).longerThan(1); - }); - test('query_type', noQueryType, () => { - enforce(data.query_type).anyOf( - enforce.equals('bgp_route'), - enforce.equals('bgp_community'), - enforce.equals('bgp_aspath'), - enforce.equals('ping'), - enforce.equals('traceroute'), - ); - }); - test('query_vrf', 'Query VRF is empty', () => { - enforce(data.query_vrf).isString(); - }); - }); - - const formInstance = useForm<TFormData>({ - resolver: vestResolver(formSchema), - defaultValues: { query_vrf: 'default', query_target: '', query_location: [], query_type: '' }, - }); - - const { handleSubmit, register, setValue, setError, clearErrors } = formInstance; - - const { - queryVrf, - families, - queryType, - availVrfs, - btnLoading, - queryTarget, - isSubmitting, - queryLocation, - displayTarget, - } = useLGState(); - - const { resolvedOpen, resetForm } = useLGMethods(); - - const isFqdnQuery = useIsFqdn(queryTarget.value, queryType.value); - - function submitHandler() { - /** - * Before submitting a query, make sure the greeting is acknowledged if required. This should - * be handled before loading the app, but people be sneaky. - */ - if (!greetingReady()) { - resetForm(); - location.reload(); - } - - // Determine if queryTarget is an FQDN. - const isFqdn = isFqdnQuery(); - - if (greetingReady() && !isFqdn) { - return isSubmitting.set(true); - } - - if (greetingReady() && isFqdn) { - btnLoading.set(true); - return resolvedOpen(); - } else { - console.group('%cSomething went wrong', 'color:red;'); - console.table({ - 'Greeting Required': web.greeting.required, - 'Greeting Ready': greetingReady(), - 'Greeting Acknowledged': ack.value, - 'Query Target': queryTarget.value, - 'Query Type': queryType.value, - 'Is FQDN': isFqdn, - }); - console.groupEnd(); - } - } - - function handleLocChange(locations: string[]): void { - clearErrors('query_location'); - const allVrfs = [] as TDeviceVrf[][]; - const locationNames = [] as string[]; - - queryLocation.set(locations); - - // Create an array of each device's VRFs. - for (const loc of locations) { - const device = getDevice(loc); - locationNames.push(device.name); - allVrfs.push(device.vrfs); - } - - // Use _.intersectionWith to create an array of VRFs common to all selected locations. - const intersecting = intersectionWith( - ...allVrfs, - (a: TDeviceVrf, b: TDeviceVrf) => a._id === b._id, - ); - - availVrfs.set(intersecting); - - // If there is more than one location selected, but there are no intersecting VRFs, show an error. - if (locations.length > 1 && intersecting.length === 0) { - setError('query_location', { - message: `${locationNames.join(', ')} have no VRFs in common.`, - }); - } - // If there is only one intersecting VRF, set it as the form value so the user doesn't have to. - else if (intersecting.length === 1) { - queryVrf.set(intersecting[0]._id); - } - - // Determine which address families are available in the intersecting VRFs. - let ipv4 = 0; - let ipv6 = 0; - - for (const intersection of intersecting) { - if (intersection.ipv4) { - // If IPv4 is enabled in this VRF, count it. - ipv4++; - } - if (intersection.ipv6) { - // If IPv6 is enabled in this VRF, count it. - ipv6++; - } - } - - if (ipv4 !== 0 && ipv4 === ipv6) { - /** - * If ipv4 & ipv6 are equal, this means every VRF has both IPv4 & IPv6 enabled. In that - * case, signal that both A & AAAA records should be queried if the query is an FQDN. - */ - families.set([4, 6]); - } else if (ipv4 > ipv6) { - /** - * If ipv4 is greater than ipv6, this means that IPv6 is not enabled on all VRFs, i.e. there - * are some VRFs with IPv4 enabled but IPv6 disabled. In that case, only query A records. - */ - families.set([4]); - } else if (ipv4 < ipv6) { - /** - * If ipv6 is greater than ipv4, this means that IPv4 is not enabled on all VRFs, i.e. there - * are some VRFs with IPv6 enabled but IPv4 disabled. In that case, only query AAAA records. - */ - families.set([6]); - } else { - /** - * If both ipv4 and ipv6 are 0, then both ipv4 and ipv6 are disabled, and why does that VRF - * even exist? - */ - families.set([]); - } - } - - function handleChange(e: OnChangeArgs): void { - // Signal the field & value to react-hook-form. - if (isQueryField(e.field)) { - setValue(e.field, e.value); - } else { - throw new Error(`Field '${e.field}' is not a valid form field.`); - } - - if (e.field === 'query_location' && Array.isArray(e.value)) { - handleLocChange(e.value); - } else if (e.field === 'query_type' && isQueryType(e.value)) { - queryType.set(e.value); - if (queryTarget.value !== '') { - /** - * Reset queryTarget as well, so that, for example, selecting BGP Community, and selecting - * a community, then changing the queryType to BGP Route doesn't preserve the selected - * community as the queryTarget. - */ - queryTarget.set(''); - displayTarget.set(''); - } - } else if (e.field === 'query_vrf' && isString(e.value)) { - queryVrf.set(e.value); - } else if (e.field === 'query_target' && isString(e.value)) { - queryTarget.set(e.value); - } - } - - /** - * Select the correct help content based on the selected VRF & Query Type. Also remove the icon - * if no locations are set. - */ - const vrfContent = useMemo(() => { - if (queryLocation.value.length === 0) { - return null; - } - if (Object.keys(content.vrf).includes(queryVrf.value) && queryType.value !== '') { - return content.vrf[queryVrf.value][queryType.value]; - } else { - return null; - } - }, [queryVrf.value, queryLocation.value, queryType.value]); - - useEffect(() => { - register('query_location', { required: true }); - register('query_target', { required: true }); - register('query_type', { required: true }); - register('query_vrf'); - }, [register]); - - return ( - <FormProvider {...formInstance}> - <AnimatedDiv - p={0} - my={4} - w="100%" - as="form" - mx="auto" - textAlign="left" - animate={{ opacity: 1, y: 0 }} - transition={{ duration: 0.3 }} - exit={{ opacity: 0, x: -300 }} - initial={{ opacity: 0, y: 300 }} - maxW={{ base: '100%', lg: '75%' }} - onSubmit={handleSubmit(submitHandler)} - > - <FormRow> - <FormField name="query_location" label={web.text.query_location}> - <QueryLocation onChange={handleChange} label={web.text.query_location} /> - </FormField> - <FormField - name="query_type" - label={web.text.query_type} - labelAddOn={ - <HelpModal visible={isQueryContent(vrfContent)} item={vrfContent} name="query_type" /> - } - > - <QueryType onChange={handleChange} label={web.text.query_type} /> - </FormField> - </FormRow> - <FormRow> - <If c={availVrfs.length > 1}> - <FormField label={web.text.query_vrf} name="query_vrf"> - <QueryVrf label={web.text.query_vrf} vrfs={availVrfs.value} onChange={handleChange} /> - </FormField> - </If> - <FormField name="query_target" label={web.text.query_target}> - <QueryTarget - name="query_target" - register={register} - onChange={handleChange} - placeholder={web.text.query_target} - /> - </FormField> - </FormRow> - <FormRow mt={0} justifyContent="flex-end"> - <Flex - my={2} - w="100%" - ml="auto" - maxW="100%" - flex="0 0 0" - flexDir="column" - mr={{ base: 0, lg: 2 }} - > - <SubmitButton handleChange={handleChange} /> - </Flex> - </FormRow> - </AnimatedDiv> - </FormProvider> - ); -}; diff --git a/hyperglass/ui/components/markdown/types.ts b/hyperglass/ui/components/markdown/types.ts deleted file mode 100644 index b5e15aab3..000000000 --- a/hyperglass/ui/components/markdown/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { BoxProps, CheckboxProps, HeadingProps, ListProps } from '@chakra-ui/react'; - -export interface TMarkdown { - content: string; -} - -export interface TCheckbox extends CheckboxProps { - checked: boolean; -} - -export interface TListItem { - checked: boolean; - children?: React.ReactNode; -} - -export interface TList extends ListProps { - ordered: boolean; - children?: React.ReactNode; -} - -export interface THeading extends HeadingProps { - level: 1 | 2 | 3 | 4 | 5 | 6; -} - -export interface TCodeBlock { - value: React.ReactNode; -} - -export interface TTableData extends BoxProps { - isHeader: boolean; -} diff --git a/hyperglass/ui/components/meta.tsx b/hyperglass/ui/components/meta.tsx index 70ab813db..a5c20aef4 100644 --- a/hyperglass/ui/components/meta.tsx +++ b/hyperglass/ui/components/meta.tsx @@ -1,59 +1,38 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import Head from 'next/head'; -import { useTheme } from '@chakra-ui/react'; import { useConfig } from '~/context'; -import { googleFontUrl } from '~/util'; -export const Meta: React.FC = () => { +export const Meta = (): JSX.Element => { const config = useConfig(); - const { fonts } = useTheme(); const [location, setLocation] = useState('/'); const { - site_title: title = 'hyperglass', - site_description: description = 'Network Looking Glass', - site_keywords: keywords = [ - 'hyperglass', - 'looking glass', - 'lg', - 'peer', - 'peering', - 'ipv4', - 'ipv6', - 'transit', - 'community', - 'communities', - 'bgp', - 'routing', - 'network', - 'isp', - ], + siteTitle: title = 'hyperglass', + siteDescription: description = 'Network Looking Glass', } = useConfig(); const siteName = `${title} - ${description}`; - const primaryFont = useMemo(() => googleFontUrl(fonts.body), []); - const monoFont = useMemo(() => googleFontUrl(fonts.mono), []); useEffect(() => { if (typeof window !== 'undefined' && location === '/') { setLocation(window.location.href); } - }, []); + }, [location]); return ( <Head> - <title>{title} - + {title} - - - - + + ); }; diff --git a/hyperglass/ui/components/output/table.tsx b/hyperglass/ui/components/output/bgp-table.tsx similarity index 58% rename from hyperglass/ui/components/output/table.tsx rename to hyperglass/ui/components/output/bgp-table.tsx index a56a6fd8a..670194ee5 100644 --- a/hyperglass/ui/components/output/table.tsx +++ b/hyperglass/ui/components/output/bgp-table.tsx @@ -1,12 +1,14 @@ import { Flex } from '@chakra-ui/react'; -import { useConfig } from '~/context'; import { Table } from '~/components'; +import { useConfig } from '~/context'; import { Cell } from './cell'; -import type { TColumn, TParsedDataField, TCellRender } from '~/types'; -import type { TBGPTable } from './types'; +import type { FlexProps } from '@chakra-ui/react'; +import type { TableColumn, ParsedDataField, CellRenderProps } from '~/types'; + +type BGPTableProps = Swap; -function makeColumns(fields: TParsedDataField[]): TColumn[] { +function makeColumns(fields: ParsedDataField[]): TableColumn[] { return fields.map(pair => { const [header, accessor, align] = pair; @@ -15,7 +17,7 @@ function makeColumns(fields: TParsedDataField[]): TColumn[] { accessor, hidden: false, Header: header, - } as TColumn; + } as TableColumn; if (align === null) { columnConfig.hidden = true; @@ -25,10 +27,10 @@ function makeColumns(fields: TParsedDataField[]): TColumn[] { }); } -export const BGPTable: React.FC = (props: TBGPTable) => { +export const BGPTable = (props: BGPTableProps): JSX.Element => { const { children: data, ...rest } = props; - const { parsed_data_fields } = useConfig(); - const columns = makeColumns(parsed_data_fields); + const { parsedDataFields } = useConfig(); + const columns = makeColumns(parsedDataFields); return ( @@ -38,7 +40,7 @@ export const BGPTable: React.FC = (props: TBGPTable) => { data={data.routes} rowHighlightBg="green" rowHighlightProp="active" - Cell={(d: TCellRender) => } + Cell={(d: CellRenderProps) => } /> ); diff --git a/hyperglass/ui/components/output/cell.tsx b/hyperglass/ui/components/output/cell.tsx index 0bcb6d99a..6bf12b706 100644 --- a/hyperglass/ui/components/output/cell.tsx +++ b/hyperglass/ui/components/output/cell.tsx @@ -1,10 +1,15 @@ import { MonoField, Active, Weight, Age, Communities, RPKIState, ASPath } from './fields'; -import type { TCell } from './types'; +import type { CellRenderProps } from '~/types'; -export const Cell: React.FC = (props: TCell) => { +interface CellProps { + data: CellRenderProps; + rawData: StructuredResponse; +} + +export const Cell = (props: CellProps): JSX.Element => { const { data, rawData } = props; - const cellId = data.column.id as keyof TRoute; + const cellId = data.column.id as keyof Route; const component = { med: , age: , @@ -20,5 +25,5 @@ export const Cell: React.FC = (props: TCell) => { rpki_state: , weight: , }; - return component[cellId] ?? <> ; + return component[cellId] ?? ''; }; diff --git a/hyperglass/ui/components/output/fields.tsx b/hyperglass/ui/components/output/fields.tsx index f372bd97f..a24857a17 100644 --- a/hyperglass/ui/components/output/fields.tsx +++ b/hyperglass/ui/components/output/fields.tsx @@ -1,32 +1,54 @@ -import { forwardRef } from 'react'; -import { Icon, Text, Box, Tooltip, Menu, MenuButton, MenuList, Link } from '@chakra-ui/react'; -import { CgMoreO as More } from '@meronex/icons/cg'; -import { BisError as Warning } from '@meronex/icons/bi'; -import { MdCancel as NotAllowed } from '@meronex/icons/md'; -import { RiHome2Fill as End } from '@meronex/icons/ri'; -import { BsQuestionCircleFill as Question } from '@meronex/icons/bs'; -import { FaCheckCircle as Check, FaChevronRight as ChevronRight } from '@meronex/icons/fa'; +import { Box, Flex, Link, Menu, MenuButton, MenuList, Text, Tooltip } from '@chakra-ui/react'; import dayjs from 'dayjs'; import relativeTimePlugin from 'dayjs/plugin/relativeTime'; import utcPlugin from 'dayjs/plugin/utc'; -import { If } from '~/components'; -import { useConfig, useColorValue } from '~/context'; -import { useOpposingColor } from '~/hooks'; - -import type { - TAge, - TActive, - TWeight, - TASPath, - TMonoField, - TRPKIState, - TCommunities, -} from './types'; +import { forwardRef } from 'react'; +import { Else, If, Then } from 'react-if'; +import { useConfig } from '~/context'; +import { DynamicIcon } from '~/elements'; +import { useColorValue, useOpposingColor } from '~/hooks'; + +import type { TextProps } from '@chakra-ui/react'; + +interface ActiveProps { + isActive: boolean; +} + +interface MonoFieldProps extends TextProps { + v: React.ReactNode; +} + +interface AgeProps extends TextProps { + inSeconds: number; +} + +interface WeightProps extends TextProps { + weight: number; + winningWeight: 'low' | 'high'; +} + +interface ASPathProps { + path: number[]; + active: boolean; +} + +interface CommunitiesProps { + communities: string[]; +} + +interface RPKIStateProps { + state: + | 0 // Invalid + | 1 // Valid + | 2 // Unknown + | 3; // Unverified + active: boolean; +} dayjs.extend(relativeTimePlugin); dayjs.extend(utcPlugin); -export const MonoField: React.FC = (props: TMonoField) => { +export const MonoField = (props: MonoFieldProps): JSX.Element => { const { v, ...rest } = props; return ( @@ -35,22 +57,22 @@ export const MonoField: React.FC = (props: TMonoField) => { ); }; -export const Active: React.FC = (props: TActive) => { +export const Active = (props: ActiveProps): JSX.Element => { const { isActive } = props; const color = useColorValue(['gray.500', 'green.500'], ['whiteAlpha.300', 'blackAlpha.500']); return ( - <> - - - - - - - + + + + + + + + ); }; -export const Age: React.FC = (props: TAge) => { +export const Age = (props: AgeProps): JSX.Element => { const { inSeconds, ...rest } = props; const now = dayjs.utc(); const then = now.subtract(inSeconds, 'second'); @@ -63,7 +85,7 @@ export const Age: React.FC = (props: TAge) => { ); }; -export const Weight: React.FC = (props: TWeight) => { +export const Weight = (props: WeightProps): JSX.Element => { const { weight, winningWeight, ...rest } = props; const fixMeText = winningWeight === 'low' ? 'Lower Weight is Preferred' : 'Higher Weight is Preferred'; @@ -76,7 +98,7 @@ export const Weight: React.FC = (props: TWeight) => { ); }; -export const ASPath: React.FC = (props: TASPath) => { +export const ASPath = (props: ASPathProps): JSX.Element => { const { path, active } = props; const color = useColorValue( // light: inactive, active @@ -86,7 +108,13 @@ export const ASPath: React.FC = (props: TASPath) => { ); if (path.length === 0) { - return ; + return ( + + + + + + ); } const paths = [] as JSX.Element[]; @@ -95,36 +123,45 @@ export const ASPath: React.FC = (props: TASPath) => { const asnStr = String(asn); i !== 0 && paths.push( - , + , ); paths.push( + // biome-ignore lint/suspicious/noArrayIndexKey: index makes sense in this case. {asnStr} , ); }); - return <>{paths}; + return {paths}; }; -export const Communities: React.FC = (props: TCommunities) => { +export const Communities = (props: CommunitiesProps): JSX.Element => { const { communities } = props; const { web } = useConfig(); const bg = useColorValue('white', 'gray.900'); const color = useOpposingColor(bg); return ( - <> - - + + + - + - - + + - + = (props: TCommunities) => { {communities.join('\n')} - - + + ); }; -const _RPKIState: React.ForwardRefRenderFunction = ( - props: TRPKIState, +const _RPKIState: React.ForwardRefRenderFunction = ( + props: RPKIStateProps, ref, ) => { const { state, active } = props; @@ -163,13 +200,19 @@ const _RPKIState: React.ForwardRefRenderFunction = ( ], ); const color = useOpposingColor(bg[+active][state]); - const icon = [NotAllowed, Check, Warning, Question]; + + const icon = [ + { md: 'MdCancel' }, + { fa: 'FaCheckCircle' }, + { bi: 'BiError' }, + { bs: 'BsQuestionCircleFill' }, + ] as Record[]; const text = [ - web.text.rpki_invalid, - web.text.rpki_valid, - web.text.rpki_unknown, - web.text.rpki_unverified, + web.text.rpkiInvalid, + web.text.rpkiValid, + web.text.rpkiUnknown, + web.text.rpkiUnverified, ]; return ( @@ -181,10 +224,10 @@ const _RPKIState: React.ForwardRefRenderFunction = ( color={color} > - + ); }; -export const RPKIState = forwardRef(_RPKIState); +export const RPKIState = forwardRef(_RPKIState); diff --git a/hyperglass/ui/components/output/highlighted.tsx b/hyperglass/ui/components/output/highlighted.tsx new file mode 100644 index 000000000..e5f61c711 --- /dev/null +++ b/hyperglass/ui/components/output/highlighted.tsx @@ -0,0 +1,55 @@ +import { Badge, Tooltip, useStyleConfig } from '@chakra-ui/react'; +import React, { memo } from 'react'; +import isEqual from 'react-fast-compare'; +import replace from 'react-string-replace'; + +import type { TooltipProps } from '@chakra-ui/react'; +import type { Highlight as HighlightConfig } from '~/types'; + +interface HighlightedProps { + patterns: HighlightConfig[]; + children: string; +} + +interface HighlightProps { + label: string | null; + colorScheme: string; + children: React.ReactNode; +} + +const Highlight = (props: HighlightProps): JSX.Element => { + const { colorScheme, label, children } = props; + const { bg, color } = useStyleConfig('Button', { colorScheme }) as TooltipProps; + return ( + + {children} + + ); +}; + +const _Highlighted = (props: HighlightedProps): JSX.Element => { + const { patterns, children } = props; + let result: React.ReactNode[] = []; + let times: number = 0; + + if (patterns.length === 0) { + result = [children]; + } else { + for (const config of patterns) { + let toReplace: string | React.ReactNode[] = children; + if (times !== 0) { + toReplace = result; + } + result = replace(toReplace, new RegExp(`(${config.pattern})`, 'gm'), (m, i) => ( + + {m} + + )); + times++; + } + } + + return <>{result}; +}; + +export const Highlighted = memo(_Highlighted, isEqual); diff --git a/hyperglass/ui/components/output/index.ts b/hyperglass/ui/components/output/index.ts index c22d4997d..6e28bab53 100644 --- a/hyperglass/ui/components/output/index.ts +++ b/hyperglass/ui/components/output/index.ts @@ -1,2 +1,2 @@ -export * from './table'; -export * from './text'; +export * from './bgp-table'; +export * from './text-output'; diff --git a/hyperglass/ui/components/output/text.tsx b/hyperglass/ui/components/output/text-output.tsx similarity index 59% rename from hyperglass/ui/components/output/text.tsx rename to hyperglass/ui/components/output/text-output.tsx index 8fa794cac..1d2c82189 100644 --- a/hyperglass/ui/components/output/text.tsx +++ b/hyperglass/ui/components/output/text-output.tsx @@ -1,9 +1,13 @@ import { Box } from '@chakra-ui/react'; -import { useColorValue } from '~/context'; +import { useConfig } from '~/context'; +import { useColorValue } from '~/hooks'; +import { Highlighted } from './highlighted'; -import type { TTextOutput } from './types'; +import type { BoxProps } from '@chakra-ui/react'; -export const TextOutput: React.FC = (props: TTextOutput) => { +type TextOutputProps = Swap; + +export const TextOutput = (props: TextOutputProps): JSX.Element => { const { children, ...rest } = props; const bg = useColorValue('blackAlpha.100', 'gray.800'); @@ -11,6 +15,10 @@ export const TextOutput: React.FC = (props: TTextOutput) => { const selectionBg = useColorValue('black', 'white'); const selectionColor = useColorValue('white', 'black'); + const { + web: { highlight }, + } = useConfig(); + return ( = (props: TTextOutput) => { }} {...rest} > - {children.split('\\n').join('\n').replace(/\n\n/g, '\n')} + + {children.split('\\n').join('\n').replace(/\n\n/g, '\n')} + ); }; diff --git a/hyperglass/ui/components/output/types.ts b/hyperglass/ui/components/output/types.ts deleted file mode 100644 index a49262567..000000000 --- a/hyperglass/ui/components/output/types.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { BoxProps, FlexProps, TextProps } from '@chakra-ui/react'; -import type { TCellRender } from '~/types'; - -export interface TTextOutput extends Omit { - children: string; -} - -export interface TActive { - isActive: boolean; -} - -export interface TMonoField extends TextProps { - v: React.ReactNode; -} - -export interface TAge extends TextProps { - inSeconds: number; -} - -export interface TWeight extends TextProps { - weight: number; - winningWeight: 'low' | 'high'; -} - -export interface TASPath { - path: number[]; - active: boolean; -} - -export interface TCommunities { - communities: string[]; -} - -export interface TRPKIState { - state: - | 0 // Invalid - | 1 // Valid - | 2 // Unknown - | 3; // Unverified - active: boolean; -} - -export interface TCell { - data: TCellRender; - rawData: TStructuredResponse; -} - -export interface TBGPTable extends Omit { - children: TStructuredResponse; -} diff --git a/hyperglass/ui/components/path/button.tsx b/hyperglass/ui/components/path/button.tsx deleted file mode 100644 index 94723b455..000000000 --- a/hyperglass/ui/components/path/button.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import dynamic from 'next/dynamic'; -import { Button, Icon, Tooltip } from '@chakra-ui/react'; - -import type { TPathButton } from './types'; - -const PathIcon = dynamic(() => - import('@meronex/icons/bi').then(i => i.BisNetworkChart), -); - -export const PathButton: React.FC = (props: TPathButton) => { - const { onOpen } = props; - return ( - - - - ); -}; diff --git a/hyperglass/ui/components/path/chart.tsx b/hyperglass/ui/components/path/chart.tsx index 9f6cff4b6..e5e95f3fc 100644 --- a/hyperglass/ui/components/path/chart.tsx +++ b/hyperglass/ui/components/path/chart.tsx @@ -1,30 +1,59 @@ -import { Box, Flex, SkeletonText, Badge, VStack } from '@chakra-ui/react'; -import ReactFlow from 'react-flow-renderer'; -import { Background, ReactFlowProvider } from 'react-flow-renderer'; -import { Handle, Position } from 'react-flow-renderer'; -import { useConfig, useColorValue, useColorToken } from '~/context'; -import { useASNDetail } from '~/hooks'; +import { Badge, Box, Flex, SkeletonText, VStack } from '@chakra-ui/react'; +import { useMemo } from 'react'; +import ReactFlow, { + Background, + ReactFlowProvider, + Handle, + Position, + isNode, + isEdge, +} from 'reactflow'; +import { useConfig } from '~/context'; +import { useASNDetail, useColorToken, useColorValue } from '~/hooks'; import { Controls } from './controls'; -import { useElements } from './useElements'; +import { useElements } from './use-elements'; -import type { TChart, TNode, TNodeData } from './types'; +import type { NodeProps as ReactFlowNodeProps } from 'reactflow'; -export const Chart: React.FC = (props: TChart) => { +interface ChartProps { + data: StructuredResponse; +} + +interface NodeProps extends Omit { + data: D; +} + +export interface NodeData { + asn: string; + name: string; + hasChildren: boolean; + hasParents?: boolean; +} + +export const Chart = (props: ChartProps): JSX.Element => { const { data } = props; - const { primary_asn, org_name } = useConfig(); + const { primaryAsn, orgName } = useConfig(); const dots = useColorToken('colors', 'blackAlpha.500', 'whiteAlpha.400'); - const elements = useElements({ asn: primary_asn, name: org_name }, data); + const elements = useElements({ asn: primaryAsn, name: orgName }, data); + + const nodes = useMemo(() => elements.filter(isNode), [elements]); + const edges = useMemo(() => elements.filter(isEdge), [elements]); return ( setTimeout(() => inst.fitView(), 0)} + edgesUpdatable={false} + nodesDraggable={false} + nodesConnectable={false} + onInit={inst => setTimeout(() => inst.fitView(), 0)} + proOptions={{ hideAttribution: true }} > @@ -34,20 +63,20 @@ export const Chart: React.FC = (props: TChart) => { ); }; -const ASNode: React.FC> = (props: TNode) => { +const ASNode = (props: NodeProps): JSX.Element => { const { data } = props; const { asn, name, hasChildren, hasParents } = data; const color = useColorValue('black', 'white'); - const bg = useColorValue('white', 'whiteAlpha.100'); + const bg = useColorValue('white', 'whiteAlpha.200'); const { data: asnData, isError, isLoading } = useASNDetail(String(asn)); return ( <> {hasChildren && } - - + + {isLoading ? ( diff --git a/hyperglass/ui/components/path/controls.tsx b/hyperglass/ui/components/path/controls.tsx index 4d6e13d6d..eb0949d03 100644 --- a/hyperglass/ui/components/path/controls.tsx +++ b/hyperglass/ui/components/path/controls.tsx @@ -1,13 +1,9 @@ -import dynamic from 'next/dynamic'; import { ButtonGroup, IconButton } from '@chakra-ui/react'; -import { useZoomPanHelper } from 'react-flow-renderer'; +import { useReactFlow } from 'reactflow'; +import { DynamicIcon } from '~/elements'; -const Plus = dynamic(() => import('@meronex/icons/fi').then(i => i.FiPlus)); -const Minus = dynamic(() => import('@meronex/icons/fi').then(i => i.FiMinus)); -const Square = dynamic(() => import('@meronex/icons/fi').then(i => i.FiSquare)); - -export const Controls: React.FC = () => { - const { fitView, zoomIn, zoomOut } = useZoomPanHelper(); +export const Controls = (): JSX.Element => { + const { fitView, zoomIn, zoomOut } = useReactFlow(); return ( { variant="solid" colorScheme="secondary" > - } onClick={() => zoomIn()} aria-label="Zoom In" /> - } onClick={() => zoomOut()} aria-label="Zoom Out" /> - } onClick={() => fitView()} aria-label="Fit Nodes" /> + } + onClick={() => zoomIn()} + aria-label="Zoom In" + /> + } + onClick={() => zoomOut()} + aria-label="Zoom Out" + /> + } + onClick={() => fitView()} + aria-label="Fit Nodes" + /> ); }; diff --git a/hyperglass/ui/components/path/path-button.tsx b/hyperglass/ui/components/path/path-button.tsx new file mode 100644 index 000000000..4b1d12142 --- /dev/null +++ b/hyperglass/ui/components/path/path-button.tsx @@ -0,0 +1,17 @@ +import { Button, Tooltip } from '@chakra-ui/react'; +import { DynamicIcon } from '~/elements'; + +interface PathButtonProps { + onOpen(): void; +} + +export const PathButton = (props: PathButtonProps): JSX.Element => { + const { onOpen } = props; + return ( + + + + ); +}; diff --git a/hyperglass/ui/components/path/path.tsx b/hyperglass/ui/components/path/path.tsx index 96886e2ec..0fd3d7425 100644 --- a/hyperglass/ui/components/path/path.tsx +++ b/hyperglass/ui/components/path/path.tsx @@ -1,27 +1,29 @@ import { Modal, - Skeleton, ModalBody, + ModalCloseButton, + ModalContent, ModalHeader, ModalOverlay, - ModalContent, + Skeleton, useDisclosure, - ModalCloseButton, } from '@chakra-ui/react'; -import { useColorValue, useBreakpointValue } from '~/context'; -import { useLGState, useLGMethods } from '~/hooks'; -import { PathButton } from './button'; +import 'reactflow/dist/style.css'; +import { useBreakpointValue, useColorValue, useFormState } from '~/hooks'; import { Chart } from './chart'; +import { PathButton } from './path-button'; -import type { TPath } from './types'; +interface PathProps { + device: string; +} -export const Path: React.FC = (props: TPath) => { +export const Path = (props: PathProps): JSX.Element => { const { device } = props; - const { displayTarget } = useLGState(); - const { getResponse } = useLGMethods(); + const displayTarget = useFormState(s => s.target.display); + const getResponse = useFormState(s => s.response); const { isOpen, onClose, onOpen } = useDisclosure(); const response = getResponse(device); - const output = response?.output as TStructuredResponse; + const output = response?.output as StructuredResponse; const bg = useColorValue('light.50', 'dark.900'); const centered = useBreakpointValue({ base: false, lg: true }) ?? true; return ( @@ -35,10 +37,12 @@ export const Path: React.FC = (props: TPath) => { maxH={{ base: '80%', lg: '60%' }} maxW={{ base: '100%', lg: '80%' }} > - {`Path to ${displayTarget.value}`} + {`Path to ${displayTarget}`} - {response !== null ? : } + + + diff --git a/hyperglass/ui/components/path/types.ts b/hyperglass/ui/components/path/types.ts deleted file mode 100644 index dd0f4ba90..000000000 --- a/hyperglass/ui/components/path/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { NodeProps } from 'react-flow-renderer'; - -export interface TChart { - data: TStructuredResponse; -} - -export interface TPath { - device: string; -} - -export interface TNode extends Omit { - data: D; -} - -export interface TNodeData { - asn: string; - name: string; - hasChildren: boolean; - hasParents?: boolean; -} - -export interface BasePath { - asn: string; - name: string; -} - -export interface TPathButton { - onOpen(): void; -} diff --git a/hyperglass/ui/components/path/useElements.ts b/hyperglass/ui/components/path/use-elements.ts similarity index 87% rename from hyperglass/ui/components/path/useElements.ts rename to hyperglass/ui/components/path/use-elements.ts index e0916b490..d7b107b2f 100644 --- a/hyperglass/ui/components/path/useElements.ts +++ b/hyperglass/ui/components/path/use-elements.ts @@ -2,23 +2,33 @@ import dagre from 'dagre'; import { useMemo } from 'react'; import isEqual from 'react-fast-compare'; -import type { FlowElement } from 'react-flow-renderer'; -import type { BasePath } from './types'; +import type { Edge, Node } from 'reactflow'; +import type { NodeData } from './chart'; -const NODE_WIDTH = 200; +interface BasePath { + asn: string; + name: string; +} + +type FlowElement = Node | Edge; + +const NODE_WIDTH = 128; const NODE_HEIGHT = 48; -export function useElements(base: BasePath, data: TStructuredResponse): FlowElement[] { +export function useElements(base: BasePath, data: StructuredResponse): FlowElement[] { return useMemo(() => { return [...buildElements(base, data)]; - }, [data.routes.length]); + }, [base, data]); } /** * Calculate the positions for each AS Path. * @see https://github.com/MrBlenny/react-flow-chart/issues/61 */ -function* buildElements(base: BasePath, data: TStructuredResponse): Generator { +function* buildElements( + base: BasePath, + data: StructuredResponse, +): Generator> { const { routes } = data; // Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths. const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]); @@ -103,7 +113,7 @@ function* buildElements(base: BasePath, data: TStructuredResponse): Generator { + const { trigger, children, ...disclosure } = props; + const bg = useColorValue('white', 'gray.900'); + + return ( + + {trigger} + + + + {children} + + + ); +}; diff --git a/hyperglass/ui/components/prompt/index.tsx b/hyperglass/ui/components/prompt/index.tsx new file mode 100644 index 000000000..1f50f6dc7 --- /dev/null +++ b/hyperglass/ui/components/prompt/index.tsx @@ -0,0 +1,11 @@ +import { useMobile } from '~/hooks'; +import { DesktopPrompt } from './desktop'; +import { MobilePrompt } from './mobile'; + +import type { PromptProps } from './types'; + +export const Prompt = (props: PromptProps): JSX.Element => { + const isMobile = useMobile(); + + return isMobile ? : ; +}; diff --git a/hyperglass/ui/components/prompt/mobile.tsx b/hyperglass/ui/components/prompt/mobile.tsx new file mode 100644 index 000000000..47c0da555 --- /dev/null +++ b/hyperglass/ui/components/prompt/mobile.tsx @@ -0,0 +1,30 @@ +import { Modal, ModalBody, ModalOverlay, ModalContent, ModalCloseButton } from '@chakra-ui/react'; +import { useColorValue } from '~/hooks'; + +import type { PromptProps } from './types'; + +export const MobilePrompt = (props: PromptProps): JSX.Element => { + const { children, trigger, ...disclosure } = props; + const bg = useColorValue('white', 'gray.900'); + return ( + <> + {trigger} + + + + + + {children} + + + + + ); +}; diff --git a/hyperglass/ui/components/prompt/types.ts b/hyperglass/ui/components/prompt/types.ts new file mode 100644 index 000000000..9c32b10d0 --- /dev/null +++ b/hyperglass/ui/components/prompt/types.ts @@ -0,0 +1,10 @@ +import type { UseDisclosureReturn } from '@chakra-ui/react'; + +type PromptPropsBase = React.PropsWithChildren< + Omit, 'isOpen' | 'onClose'> & + Pick +>; + +export interface PromptProps extends PromptPropsBase { + trigger?: JSX.Element; +} diff --git a/hyperglass/ui/components/query-location.tsx b/hyperglass/ui/components/query-location.tsx new file mode 100644 index 000000000..c8f040f88 --- /dev/null +++ b/hyperglass/ui/components/query-location.tsx @@ -0,0 +1,181 @@ +import { Flex, Stack, Wrap, chakra } from '@chakra-ui/react'; +import { useMemo } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { LocationCard, Select } from '~/components'; +import { isMultiValue, isSingleValue } from '~/components/select'; +import { useConfig } from '~/context'; +import { useFormState } from '~/hooks'; + +import type { SelectOnChange } from '~/components/select'; +import type { DeviceGroup, FormData, OnChangeArgs, OptionGroup, SingleOption } from '~/types'; + +/** Location option type alias for future extensions. */ +export type LocationOption = SingleOption; + +interface QueryLocationProps { + onChange: (f: OnChangeArgs) => void; + label: string; +} + +function buildOptions(devices: DeviceGroup[]): OptionGroup[] { + return devices + .map(group => { + const label = group.group; + const options = group.locations + .map( + loc => + ({ + label: loc.name, + value: loc.id, + group: loc.group, + data: { + avatar: loc.avatar, + description: loc.description, + }, + }) as SingleOption, + ) + .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); + return { label: label ?? '', options }; + }) + .sort((a, b) => + (a.label ?? 0) < (b.label ?? 0) ? -1 : (a.label ?? 0) > (b.label ?? 0) ? 1 : 0, + ); +} + +export const QueryLocation = (props: QueryLocationProps): JSX.Element => { + const { onChange, label } = props; + + const { + devices, + web: { locationDisplayMode }, + } = useConfig(); + const { + formState: { errors }, + } = useFormContext(); + const selections = useFormState(s => s.selections); + const setSelection = useFormState(s => s.setSelection); + const { form, filtered } = useFormState(({ form, filtered }) => ({ form, filtered })); + const options = useMemo(() => buildOptions(devices), [devices]); + + const element = useMemo(() => { + if (locationDisplayMode === 'dropdown') { + return 'select'; + } + if (locationDisplayMode === 'gallery') { + return 'cards'; + } + const groups = options.length; + const maxOptionsPerGroup = Math.max(...options.map(opt => opt.options.length)); + const showCards = groups < 5 && maxOptionsPerGroup < 9; + return showCards ? 'cards' : 'select'; + }, [options, locationDisplayMode]); + + const noOverlap = useMemo( + () => form.queryLocation.length > 1 && filtered.types.length === 0, + [form, filtered], + ); + + /** + * Update form and state when a card selections change. + * + * @param action Add or remove the option. + * @param option Full option object. + */ + function handleCardChange(action: 'add' | 'remove', option: SingleOption) { + const exists = selections.queryLocation.map(q => q.value).includes(option.value); + if (action === 'add' && !exists) { + const toAdd = [...form.queryLocation, option.value]; + const newSelections = [...selections.queryLocation, option]; + setSelection('queryLocation', newSelections); + onChange({ field: 'queryLocation', value: toAdd }); + } else if (action === 'remove' && exists) { + const index = selections.queryLocation.findIndex(v => v.value === option.value); + const toRemove = [...form.queryLocation.filter(v => v !== option.value)]; + setSelection( + 'queryLocation', + selections.queryLocation.filter((_, i) => i !== index), + ); + onChange({ field: 'queryLocation', value: toRemove }); + } + } + + /** + * Update form and state when select element values change. + * + * @param options Final value. React-select determines if an option is being added or removed and + * only sends back the final value. + */ + const handleSelectChange: SelectOnChange = (options): void => { + if (isMultiValue(options)) { + onChange({ field: 'queryLocation', value: options.map(o => o.value) }); + setSelection('queryLocation', options); + } else if (isSingleValue(options)) { + onChange({ field: 'queryLocation', value: options.value }); + setSelection('queryLocation', [options]); + } + }; + + if (element === 'cards') { + return ( + <> + {options.length === 1 ? ( + + {options[0].options.map(opt => { + return ( + + ); + })} + + ) : ( + <> + {options.map(group => ( + + + {group.label} + + {group.options.map(opt => { + return ( + + ); + })} + + ))} + + )} + + ); + } + if (element === 'select') { + return ( + + isMulti + options={options} + aria-label={label} + name="queryLocation" + closeMenuOnSelect={true} + onChange={handleSelectChange} + value={selections.queryLocation} + isError={typeof errors.queryLocation !== 'undefined'} + /> + ); + } + return No Locations; +}; diff --git a/hyperglass/ui/components/query-target.tsx b/hyperglass/ui/components/query-target.tsx new file mode 100644 index 000000000..1d452f97a --- /dev/null +++ b/hyperglass/ui/components/query-target.tsx @@ -0,0 +1,113 @@ +import { Input, InputGroup, InputRightElement, Text } from '@chakra-ui/react'; +import { useMemo } from 'react'; +import { components } from 'react-select'; +import { Select } from '~/components'; +import { isSingleValue } from '~/components/select'; +import { useDirective, useFormState } from '~/hooks'; +import { isSelectDirective } from '~/types'; +import { UserIP } from './user-ip'; + +import { type UseFormRegister, useForm } from 'react-hook-form'; +import type { GroupBase, OptionProps } from 'react-select'; +import type { SelectOnChange } from '~/components/select'; +import type { Directive, FormData, OnChangeArgs, SingleOption } from '~/types'; + +type OptionWithDescription = SingleOption<{ description: string | null }>; + +interface QueryTargetProps { + name: string; + placeholder: string; + onChange(e: OnChangeArgs): void; + register: UseFormRegister; +} + +function buildOptions(directive: Nullable): OptionWithDescription[] { + if (directive !== null && isSelectDirective(directive)) { + return directive.options.map(o => ({ + value: o.value, + label: o.name, + data: { description: o.description }, + })); + } + return []; +} + +const Option = (props: OptionProps) => { + const { label, data } = props; + return ( + > {...props}> + {label} +
+ + {data.data?.description} + + + ); +}; + +export const QueryTarget = (props: QueryTargetProps): JSX.Element => { + const { name, register, onChange, placeholder } = props; + + const displayTarget = useFormState(s => s.target.display); + const setTarget = useFormState(s => s.setTarget); + const queryTarget = useFormState(s => s.form.queryTarget); + const directive = useDirective(); + + const options = useMemo(() => buildOptions(directive), [directive]); + const isSelect = useMemo(() => directive !== null && isSelectDirective(directive), [directive]); + + function handleInputChange(e: React.ChangeEvent): void { + setTarget({ display: e.target.value }); + onChange({ field: name, value: [e.target.value] }); + } + + const handleSelectChange: SelectOnChange = e => { + if (isSingleValue(e)) { + onChange({ field: name, value: e.value }); + setTarget({ display: e.value }); + } + }; + + const handleUserIPChange = (target: string): void => { + setTarget({ display: target }); + onChange({ field: name, value: target }); + }; + + return ( + <> + + {isSelect ? ( + + name={name} + options={options} + components={{ Option }} + onChange={handleSelectChange} + /> + ) : ( + + + + + + + )} + + ); +}; diff --git a/hyperglass/ui/components/query-type.tsx b/hyperglass/ui/components/query-type.tsx new file mode 100644 index 000000000..7b9ac73b9 --- /dev/null +++ b/hyperglass/ui/components/query-type.tsx @@ -0,0 +1,185 @@ +import { useMemo } from 'react'; +import create from 'zustand'; +import { Box, Button, HStack, useRadio, useRadioGroup } from '@chakra-ui/react'; +import { useFormContext } from 'react-hook-form'; +import { components } from 'react-select'; +import { Select } from '~/components'; +import { useFormState, useFormSelections } from '~/hooks'; +import { isSingleValue } from '~/components/select'; + +import type { UseRadioProps } from '@chakra-ui/react'; +import type { MenuListProps } from 'react-select'; +import type { SingleOption, OptionGroup, OptionsOrGroup, OnChangeArgs } from '~/types'; +import type { SelectOnChange } from '~/components/select'; + +type QueryTypeOption = SingleOption<{ group?: string }>; + +interface QueryTypeProps { + onChange: (f: OnChangeArgs) => void; + label: string; +} + +type UserFilter = { + selected: string; + setSelected(n: string): void; + filter(candidate: QueryTypeOption, input: string): boolean; +}; + +function sorter>(a: T, b: T): number { + return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; +} + +const useFilter = create((set, get) => ({ + selected: '', + setSelected(newValue: string) { + set(() => ({ selected: newValue })); + }, + filter(candidate, input): boolean { + const { label, data } = candidate; + const group = data?.group ?? null; + + if (input && (label || group)) { + const search = input.toLowerCase(); + if (group) { + return label.toLowerCase().indexOf(search) > -1 || group.toLowerCase().indexOf(search) > -1; + } + return label.toLowerCase().indexOf(search) > -1; + } + const { selected } = get(); + if (selected !== '' && selected === group) { + return true; + } + if (selected === '') { + return true; + } + return false; + }, +})); + +function useOptions() { + const filtered = useFormState(s => s.filtered); + return useMemo((): OptionsOrGroup => { + const groupNames = new Set( + filtered.types.filter(t => t.groups.length > 0).flatMap(t => t.groups), + ); + const optGroups: OptionGroup[] = Array.from(groupNames).map(group => ({ + label: group, + options: filtered.types + .filter(t => t.groups.includes(group)) + .map(t => ({ label: t.name, value: t.id, group })) + .sort(sorter), + })); + + const noGroups: OptionGroup = { + label: '', + options: filtered.types + .filter(t => t.groups.length === 0) + .map(t => ({ label: t.name, value: t.id, group: '' })) + .sort(sorter), + }; + + return [noGroups, ...optGroups].sort(sorter); + }, [filtered.types]); +} + +const GroupFilter = (props: React.PropsWithChildren): JSX.Element => { + const { children, ...rest } = props; + const { + getInputProps, + getCheckboxProps, + getLabelProps, + htmlProps, + state: { isChecked }, + } = useRadio(rest); + const label = getLabelProps(); + const input = getInputProps(); + const checkbox = getCheckboxProps(); + + return ( + + + + + ); +}; + +const MenuList = (props: MenuListProps): JSX.Element => { + const { children, ...rest } = props; + const filtered = useFormState(s => s.filtered); + const selected = useFilter(state => state.selected); + const setSelected = useFilter(state => state.setSelected); + + const { getRadioProps, getRootProps } = useRadioGroup({ + name: 'queryGroup', + value: selected, + }); + + function handleClick(value: string): void { + setSelected(value); + } + return ( + + + handleClick('') })}> + None + + {filtered.groups.map(value => { + return ( + handleClick(value) })} + > + {value} + + ); + })} + + {children} + + ); +}; + +export const QueryType = (props: QueryTypeProps): JSX.Element => { + const { onChange, label } = props; + const { + formState: { errors }, + } = useFormContext(); + const setSelection = useFormState(s => s.setSelection); + const selections = useFormSelections(); + const setFormValue = useFormState(s => s.setFormValue); + const options = useOptions(); + const { filter } = useFilter(); // Intentionally re-render on any changes + + const handleChange: SelectOnChange = e => { + let value = ''; + if (isSingleValue(e)) { + setSelection('queryType', e); + value = e.value; + } else { + setFormValue('queryType', ''); + setSelection('queryType', null); + } + onChange({ field: 'queryType', value }); + }; + + return ( + + name="queryType" + options={options} + aria-label={label} + filterOption={filter} + onChange={handleChange} + components={{ MenuList }} + isError={'queryType' in errors} + value={selections.queryType} + /> + ); +}; diff --git a/hyperglass/ui/components/layout/resetButton.tsx b/hyperglass/ui/components/reset-button.tsx similarity index 63% rename from hyperglass/ui/components/layout/resetButton.tsx rename to hyperglass/ui/components/reset-button.tsx index 145ea6167..c0ea69f62 100644 --- a/hyperglass/ui/components/layout/resetButton.tsx +++ b/hyperglass/ui/components/reset-button.tsx @@ -1,22 +1,23 @@ -import dynamic from 'next/dynamic'; -import { Flex, Icon, IconButton } from '@chakra-ui/react'; +import { Flex, IconButton } from '@chakra-ui/react'; import { AnimatePresence } from 'framer-motion'; -import { AnimatedDiv } from '~/components'; -import { useColorValue } from '~/context'; -import { useLGState, useOpposingColor } from '~/hooks'; +import { AnimatedDiv, DynamicIcon } from '~/elements'; +import { useColorValue, useOpposingColor, useFormState } from '~/hooks'; -import type { TResetButton } from './types'; +import type { FlexProps } from '@chakra-ui/react'; -const LeftArrow = dynamic(() => import('@meronex/icons/fa').then(i => i.FaAngleLeft)); +interface ResetButtonProps extends FlexProps { + developerMode: boolean; + resetForm(): void; +} -export const ResetButton: React.FC = (props: TResetButton) => { +export const ResetButton = (props: ResetButtonProps): JSX.Element => { const { developerMode, resetForm, ...rest } = props; - const { isSubmitting } = useLGState(); + const status = useFormState(s => s.status); const bg = useColorValue('primary.500', 'primary.300'); const color = useOpposingColor(bg); return ( - {isSubmitting.value && ( + {status === 'results' && ( = (props: TResetButton) => { > } + icon={} /> diff --git a/hyperglass/ui/components/resolved-target.tsx b/hyperglass/ui/components/resolved-target.tsx new file mode 100644 index 000000000..9130d6ea1 --- /dev/null +++ b/hyperglass/ui/components/resolved-target.tsx @@ -0,0 +1,138 @@ +import { useMemo } from 'react'; +import { Button, Stack, Text, VStack } from '@chakra-ui/react'; +import { useConfig } from '~/context'; +import { DynamicIcon } from '~/elements'; +import { useStrf, useColorValue, useDNSQuery, useFormState } from '~/hooks'; + +import type { DnsOverHttps } from '~/types'; + +interface ResolvedTargetProps { + errorClose(): void; +} + +function findAnswer(data: DnsOverHttps.Response | undefined): string { + let answer = ''; + if (typeof data !== 'undefined') { + answer = data?.Answer?.filter(answerData => answerData.type === data?.Question[0]?.type)[0] + ?.data; + } + return answer; +} + +export const ResolvedTarget = (props: ResolvedTargetProps): JSX.Element => { + const { errorClose } = props; + const strF = useStrf(); + const { web } = useConfig(); + + const setStatus = useFormState(s => s.setStatus); + const displayTarget = useFormState(s => s.target.display); + const setFormValue = useFormState(s => s.setFormValue); + + const color = useColorValue('secondary.500', 'secondary.300'); + const errorColor = useColorValue('red.500', 'red.300'); + + const tooltip4 = strF(web.text.fqdnTooltip, { protocol: 'IPv4' }); + const tooltip6 = strF(web.text.fqdnTooltip, { protocol: 'IPv6' }); + + const [messageStart, messageEnd] = web.text.fqdnMessage.split('{fqdn}'); + const [errorStart, errorEnd] = web.text.fqdnError.split('{fqdn}'); + + const { + data: data4, + isLoading: isLoading4, + isError: isError4, + error: error4, + } = useDNSQuery(displayTarget, 4); + + const { + data: data6, + isLoading: isLoading6, + isError: isError6, + error: error6, + } = useDNSQuery(displayTarget, 6); + + isError4 && console.error(error4); + isError6 && console.error(error6); + + const answer4 = useMemo(() => findAnswer(data4), [data4]); + const answer6 = useMemo(() => findAnswer(data6), [data6]); + + function selectTarget(value: string): void { + setFormValue('queryTarget', [value]); + setStatus('results'); + } + + const hasAnswer = useMemo( + () => (!isError4 || !isError6) && (answer4 || answer6), + [answer4, answer6, isError4, isError6], + ); + const showA = useMemo(() => !isLoading4 && !isError4 && answer4, [isLoading4, isError4, answer4]); + const showAAAA = useMemo( + () => !isLoading6 && !isError6 && answer6, + [isLoading6, isError6, answer6], + ); + + return ( + + {hasAnswer && ( + + {messageStart} + + {`${displayTarget}`.toLowerCase()} + + {messageEnd} + + )} + + {showA && ( + + )} + {showAAAA && ( + + )} + {!hasAnswer && ( + <> + + {errorStart} + + {`${displayTarget}`.toLowerCase()} + + {errorEnd} + + + + )} + + + ); +}; diff --git a/hyperglass/ui/components/results/copy-button.tsx b/hyperglass/ui/components/results/copy-button.tsx new file mode 100644 index 000000000..fddd87e17 --- /dev/null +++ b/hyperglass/ui/components/results/copy-button.tsx @@ -0,0 +1,28 @@ +import { Button, Tooltip, useClipboard } from '@chakra-ui/react'; +import { DynamicIcon } from '~/elements'; + +import type { ButtonProps } from '@chakra-ui/react'; + +interface CopyButtonProps extends ButtonProps { + copyValue: string; +} + +export const CopyButton = (props: CopyButtonProps): JSX.Element => { + const { copyValue, ...rest } = props; + const { onCopy, hasCopied } = useClipboard(copyValue); + return ( + + + + ); +}; diff --git a/hyperglass/ui/components/results/copyButton.tsx b/hyperglass/ui/components/results/copyButton.tsx deleted file mode 100644 index bc031c36b..000000000 --- a/hyperglass/ui/components/results/copyButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import dynamic from 'next/dynamic'; -import { Button, Icon, Tooltip, useClipboard } from '@chakra-ui/react'; - -const Copy = dynamic(() => import('@meronex/icons/fi').then(i => i.FiCopy)); -const Check = dynamic(() => import('@meronex/icons/fi').then(i => i.FiCheck)); - -import type { TCopyButton } from './types'; - -export const CopyButton: React.FC = (props: TCopyButton) => { - const { copyValue, ...rest } = props; - const { onCopy, hasCopied } = useClipboard(copyValue); - return ( - - - - ); -}; diff --git a/hyperglass/ui/components/results/error.tsx b/hyperglass/ui/components/results/formatted-error.tsx similarity index 60% rename from hyperglass/ui/components/results/error.tsx rename to hyperglass/ui/components/results/formatted-error.tsx index a8292d36a..607015194 100644 --- a/hyperglass/ui/components/results/error.tsx +++ b/hyperglass/ui/components/results/formatted-error.tsx @@ -1,10 +1,13 @@ -import { Text } from '@chakra-ui/react'; +import { chakra } from '@chakra-ui/react'; -import type { TFormattedError } from './types'; +interface FormattedErrorProps { + keywords: string[]; + message: string; +} -type TFormatError = string | JSX.Element; +type FormatError = string | JSX.Element; -function formatError(text: string, values: string[], regex: RegExp): TFormatError[] | TFormatError { +function formatError(text: string, values: string[], regex: RegExp): FormatError[] | FormatError { if (!values.length) { return text; } @@ -19,16 +22,16 @@ function formatError(text: string, values: string[], regex: RegExp): TFormatErro return prev.concat( values.includes(current) ? {current} : current, ); - }, [] as TFormatError[]); + }, [] as FormatError[]); } -export const FormattedError: React.FC = (props: TFormattedError) => { +export const FormattedError = (props: FormattedErrorProps): JSX.Element => { const { keywords, message } = props; const pattern = new RegExp(keywords.map(kw => `(${kw})`).join('|'), 'gi'); const things = formatError(message, keywords, pattern); return ( - + {keywords.length !== 0 ? things : message} - + ); }; diff --git a/hyperglass/ui/components/results/group.tsx b/hyperglass/ui/components/results/group.tsx index 564d2c1d7..2b032d231 100644 --- a/hyperglass/ui/components/results/group.tsx +++ b/hyperglass/ui/components/results/group.tsx @@ -1,15 +1,13 @@ -import { useEffect } from 'react'; import { Accordion } from '@chakra-ui/react'; import { AnimatePresence } from 'framer-motion'; -import { AnimatedDiv } from '~/components'; -import { useDevice, useLGState } from '~/hooks'; +import { useEffect } from 'react'; +import { AnimatedDiv } from '~/elements'; +import { useFormState } from '~/hooks'; import { Result } from './individual'; import { Tags } from './tags'; -export const Results: React.FC = () => { - const { queryLocation, queryTarget, queryType, queryVrf } = useLGState(); - - const getDevice = useDevice(); +export const Results = (): JSX.Element => { + const { queryLocation } = useFormState(s => s.form); // Scroll to the top of the page when results load - primarily for mobile. useEffect(() => { @@ -36,22 +34,11 @@ export const Results: React.FC = () => { animate={{ opacity: 1, y: 0 }} maxW={{ base: '100%', md: '75%' }} > - + - {queryLocation.value && - queryLocation.map((loc, i) => { - const device = getDevice(loc.value); - return ( - - ); + {queryLocation.length > 0 && + queryLocation.map((location, index) => { + return ; })} diff --git a/hyperglass/ui/components/results/guards.ts b/hyperglass/ui/components/results/guards.ts index 5000fe80e..a70b6f0f5 100644 --- a/hyperglass/ui/components/results/guards.ts +++ b/hyperglass/ui/components/results/guards.ts @@ -1,21 +1,22 @@ -/* eslint @typescript-eslint/no-explicit-any: 0 */ -/* eslint @typescript-eslint/explicit-module-boundary-types: 0 */ - +// biome-ignore lint/suspicious/noExplicitAny: type guard export function isStackError(error: any): error is Error { return typeof error !== 'undefined' && error !== null && 'message' in error; } +// biome-ignore lint/suspicious/noExplicitAny: type guard export function isFetchError(error: any): error is Response { return typeof error !== 'undefined' && error !== null && 'statusText' in error; } -export function isLGError(error: any): error is TQueryResponse { +// biome-ignore lint/suspicious/noExplicitAny: type guard +export function isLGError(error: any): error is QueryResponse { return typeof error !== 'undefined' && error !== null && 'output' in error; } /** * Returns true if the response is an LG error, false if not. */ -export function isLGOutputOrError(data: any): data is TQueryResponse { +// biome-ignore lint/suspicious/noExplicitAny: type guard +export function isLGOutputOrError(data: any): data is QueryResponse { return typeof data !== 'undefined' && data !== null && data?.level !== 'success'; } diff --git a/hyperglass/ui/components/results/header.tsx b/hyperglass/ui/components/results/header.tsx index 070dbf24e..f82a62ae4 100644 --- a/hyperglass/ui/components/results/header.tsx +++ b/hyperglass/ui/components/results/header.tsx @@ -1,11 +1,19 @@ +import { AccordionIcon, Box, HStack, Spinner, Text, Tooltip } from '@chakra-ui/react'; import { useMemo } from 'react'; -import { AccordionIcon, Box, Spinner, HStack, Text, Tooltip } from '@chakra-ui/react'; -import { BisError as Warning } from '@meronex/icons/bi'; -import { FaCheckCircle as Check } from '@meronex/icons/fa'; -import { useConfig, useColorValue } from '~/context'; -import { useOpposingColor, useStrf } from '~/hooks'; +import { useConfig } from '~/context'; +import { DynamicIcon } from '~/elements'; +import { useColorValue, useOpposingColor, useStrf } from '~/hooks'; -import type { TResultHeader } from './types'; +import type { ErrorLevels } from '~/types'; + +interface ResultHeaderProps { + title: string; + loading: boolean; + isError?: boolean; + errorMsg: string; + errorLevel: ErrorLevels; + runtime: number; +} const runtimeText = (runtime: number, text: string): string => { let unit = 'seconds'; @@ -15,7 +23,7 @@ const runtimeText = (runtime: number, text: string): string => { return `${text} ${unit}`; }; -export const ResultHeader: React.FC = (props: TResultHeader) => { +export const ResultHeader = (props: ResultHeaderProps): JSX.Element => { const { title, loading, isError, errorMsg, errorLevel, runtime } = props; const status = useColorValue('primary.500', 'primary.300'); @@ -23,8 +31,9 @@ export const ResultHeader: React.FC = (props: TResultHeader) => { const defaultStatus = useColorValue('success.500', 'success.300'); const { web } = useConfig(); - const text = useStrf(web.text.complete_time, { seconds: runtime }, [runtime]); - const label = useMemo(() => runtimeText(runtime, text), [runtime]); + const strF = useStrf(); + const text = strF(web.text.completeTime, { seconds: runtime }); + const label = useMemo(() => runtimeText(runtime, text), [runtime, text]); const color = useOpposingColor(isError ? warning : defaultStatus); @@ -42,8 +51,8 @@ export const ResultHeader: React.FC = (props: TResultHeader) => { {loading ? ( ) : ( - = (props: TResult, ref) => { - const { index, device, queryVrf, queryType, queryTarget, queryLocation } = props; - +const _Result: React.ForwardRefRenderFunction = ( + props: ResultProps, + ref, +) => { + const { index, queryLocation } = props; + const toast = useToast(); const { web, cache, messages } = useConfig(); const { index: indices, setIndex } = useAccordionContext(); + const getDevice = useDevice(); + const device = getDevice(queryLocation); const isMobile = useMobile(); const color = useColorValue('black', 'white'); @@ -50,26 +70,50 @@ const _Result: React.ForwardRefRenderFunction = (props: const scrollbarHover = useColorValue('blackAlpha.400', 'whiteAlpha.400'); const scrollbarBg = useColorValue('blackAlpha.50', 'whiteAlpha.50'); - const { responses } = useLGState(); + const addResponse = useFormState(s => s.addResponse); + const form = useFormState(s => s.form); + const [errorLevel, _setErrorLevel] = useState('error'); - const { data, error, isError, isLoading, refetch, isFetching, isFetchedAfterMount } = useLGQuery({ - queryLocation, - queryTarget, - queryType, - queryVrf, - }); + const setErrorLevel = (level: ResponseLevel): void => { + let e: ErrorLevels = 'error'; + switch (level) { + case 'success': + e = level; + break; + case 'warning' || 'error': + e = 'warning'; + break; + } + _setErrorLevel(e); + }; - const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [ - data, - isLoading, - isFetching, - ]); + const { data, error, isLoading, refetch, isFetchedAfterMount } = useLGQuery( + { queryLocation, queryTarget: form.queryTarget, queryType: form.queryType }, + { + onSuccess(data) { + if (device !== null) { + addResponse(device.id, data); + } + if (isLGOutputOrError(data)) { + console.error(data); + setErrorLevel(data.level); + } + }, + onError(error) { + console.error({ error }); + if (isLGOutputOrError(error)) { + setErrorLevel(error.level); + } + }, + }, + ); - if (typeof data !== 'undefined') { - responses.merge({ [device._id]: data }); - } + const isError = useMemo(() => isLGOutputOrError(data), [data, error]); - const cacheLabel = useStrf(web.text.cache_icon, { time: data?.timestamp }, [data?.timestamp]); + const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [data, isFetchedAfterMount]); + + const strF = useStrf(); + const cacheLabel = strF(web.text.cacheIcon, { time: data?.timestamp }); const errorKeywords = useMemo(() => { let kw = [] as string[]; @@ -83,49 +127,33 @@ const _Result: React.ForwardRefRenderFunction = (props: const errorMsg = useMemo(() => { if (isLGError(error)) { return error.output as string; - } else if (isLGOutputOrError(data)) { + } + if (isLGOutputOrError(data)) { return data.output as string; - } else if (isFetchError(error)) { + } + if (isFetchError(error)) { return startCase(error.statusText); - } else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) { - return messages.request_timeout; - } else if (isStackError(error)) { - return startCase(error.message); - } else { - return messages.general; } - }, [isError, error, data]); - - isError && console.error(error); - - const errorLevel = useMemo(() => { - const statusMap = { - success: 'success', - warning: 'warning', - error: 'warning', - danger: 'error', - } as { [k in TResponseLevel]: 'success' | 'warning' | 'error' }; - - let e: TErrorLevels = 'error'; - - if (isLGError(error)) { - const idx = error.level as TResponseLevel; - e = statusMap[idx]; + if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) { + return messages.requestTimeout; + } + if (isStackError(error)) { + return startCase(error.message); } - return e; - }, [isError, isLoading, data]); + return messages.general; + }, [error, data, messages.general, messages.requestTimeout]); const tableComponent = useMemo(() => { let result = false; - if (typeof queryType.match(/^bgp_\w+$/) !== null && data?.format === 'application/json') { + if (data?.format === 'application/json') { result = true; } return result; - }, [queryType, data?.format]); + }, [data?.format]); let copyValue = data?.output as string; - const formatData = useTableToString(queryTarget, data, [data?.format]); + const formatData = useTableToString(form.queryTarget, data, [data?.format]); if (data?.format === 'application/json') { copyValue = formatData(); @@ -144,12 +172,26 @@ const _Result: React.ForwardRefRenderFunction = (props: setIndex([index]); } } - }, [data, isError]); + }, [data, index, indices, isLoading, isError, setIndex]); + + if (device === null) { + const id = `toast-queryLocation-${index}-${queryLocation}`; + if (!toast.isActive(id)) { + toast({ + id, + title: messages.general, + description: `Configuration for device with ID '${queryLocation}' not found.`, + status: 'error', + isClosable: true, + }); + } + return <>; + } return ( = (props: '&:last-of-type': { borderBottom: 'none' }, }} > - <> - - - - - - {isStructuredOutput(data) && data.level === 'success' && tableComponent && ( - - )} - - - - - + + + + + {isStructuredOutput(data) && data.level === 'success' && tableComponent && ( + + )} + + + + + - - - {!isError && typeof data !== 'undefined' ? ( - <> - {isStructuredOutput(data) && data.level === 'success' && tableComponent ? ( - {data.output} - ) : isStringOutput(data) && data.level === 'success' && !tableComponent ? ( - {data.output} - ) : isStringOutput(data) && data.level !== 'success' ? ( - - - - ) : ( - - - - )} - - ) : ( + '-ms-overflow-style': { display: 'none' }, + }} + > + + + + + {isStructuredOutput(data) && data.level === 'success' && tableComponent ? ( + {data.output} + ) : isStringOutput(data) && data.level === 'success' && !tableComponent ? ( + {data.output} + ) : isStringOutput(data) && data.level !== 'success' ? ( + + + + ) : ( + + + + )} + + - )} - - + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - + + + +
+ ); }; -export const Result = forwardRef(_Result); +export const Result = memo(forwardRef(_Result), isEqual); diff --git a/hyperglass/ui/components/results/requery-button.tsx b/hyperglass/ui/components/results/requery-button.tsx new file mode 100644 index 000000000..7df11cbcd --- /dev/null +++ b/hyperglass/ui/components/results/requery-button.tsx @@ -0,0 +1,37 @@ +import { forwardRef } from 'react'; +import { Button, Tooltip } from '@chakra-ui/react'; +import { DynamicIcon } from '~/elements'; + +import type { ButtonProps } from '@chakra-ui/react'; +import type { UseQueryResult } from '@tanstack/react-query'; + +interface RequeryButtonProps extends ButtonProps { + requery: Get, 'refetch'>; +} + +const _RequeryButton: React.ForwardRefRenderFunction = ( + props: RequeryButtonProps, + ref, +) => { + const { requery, ...rest } = props; + + return ( + + + + ); +}; + +export const RequeryButton = forwardRef(_RequeryButton); diff --git a/hyperglass/ui/components/results/requeryButton.tsx b/hyperglass/ui/components/results/requeryButton.tsx deleted file mode 100644 index 1a691a2f4..000000000 --- a/hyperglass/ui/components/results/requeryButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { forwardRef } from 'react'; -import dynamic from 'next/dynamic'; -import { Button, Icon, Tooltip } from '@chakra-ui/react'; - -import type { TRequeryButton } from './types'; - -const Repeat = dynamic(() => import('@meronex/icons/fi').then(i => i.FiRepeat)); - -const _RequeryButton: React.ForwardRefRenderFunction = ( - props: TRequeryButton, - ref, -) => { - const { requery, ...rest } = props; - - return ( - - - - ); -}; - -export const RequeryButton = forwardRef(_RequeryButton); diff --git a/hyperglass/ui/components/results/tags.tsx b/hyperglass/ui/components/results/tags.tsx index 26de570f3..d03c9a366 100644 --- a/hyperglass/ui/components/results/tags.tsx +++ b/hyperglass/ui/components/results/tags.tsx @@ -1,21 +1,26 @@ +import { useMemo } from 'react'; import { Box, Stack, useToken } from '@chakra-ui/react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Label } from '~/components'; -import { useConfig, useBreakpointValue } from '~/context'; -import { useLGState, useVrf } from '~/hooks'; -import { isQueryType } from '~/types'; +import { useConfig } from '~/context'; +import { Label } from '~/elements'; +import { useFormState, useBreakpointValue } from '~/hooks'; import type { Transition } from 'framer-motion'; const transition = { duration: 0.3, delay: 0.5 } as Transition; -export const Tags: React.FC = () => { - const { queries, web } = useConfig(); - const { queryLocation, queryTarget, queryType, queryVrf } = useLGState(); +export const Tags = (): JSX.Element => { + const { web } = useConfig(); + const form = useFormState(s => s.form); + const getDirective = useFormState(s => s.getDirective); + + const selectedDirective = useMemo(() => { + return getDirective(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [form.queryType, getDirective]); const targetBg = useToken('colors', 'teal.600'); const queryBg = useToken('colors', 'cyan.500'); - const vrfBg = useToken('colors', 'blue.500'); const animateLeft = useBreakpointValue({ base: { opacity: 1, x: 0 }, @@ -24,13 +29,6 @@ export const Tags: React.FC = () => { xl: { opacity: 1, x: 0 }, }); - const animateCenter = useBreakpointValue({ - base: { opacity: 1 }, - md: { opacity: 1 }, - lg: { opacity: 1 }, - xl: { opacity: 1 }, - }); - const animateRight = useBreakpointValue({ base: { opacity: 1, x: 0 }, md: { opacity: 1, x: 0 }, @@ -45,13 +43,6 @@ export const Tags: React.FC = () => { xl: { opacity: 0, x: '-100%' }, }); - const initialCenter = useBreakpointValue({ - base: { opacity: 0 }, - md: { opacity: 0 }, - lg: { opacity: 0 }, - xl: { opacity: 0 }, - }); - const initialRight = useBreakpointValue({ base: { opacity: 0, x: '100%' }, md: { opacity: 0, x: '100%' }, @@ -59,14 +50,6 @@ export const Tags: React.FC = () => { xl: { opacity: 0, x: '100%' }, }); - let queryTypeLabel = ''; - if (isQueryType(queryType.value)) { - queryTypeLabel = queries[queryType.value].display_name; - } - - const getVrf = useVrf(); - const vrf = getVrf(queryVrf.value); - return ( { > - {queryLocation.value && ( + {form.queryLocation.length > 0 && ( <> { > - - diff --git a/hyperglass/ui/components/results/types.ts b/hyperglass/ui/components/results/types.ts deleted file mode 100644 index 10efab525..000000000 --- a/hyperglass/ui/components/results/types.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { State } from '@hookstate/core'; -import type { ButtonProps } from '@chakra-ui/react'; -import type { UseQueryResult } from 'react-query'; -import type { TDevice, TQueryTypes } from '~/types'; - -export interface TResultHeader { - title: string; - loading: boolean; - isError?: boolean; - errorMsg: string; - errorLevel: 'success' | 'warning' | 'error'; - runtime: number; -} - -export interface TFormattedError { - keywords: string[]; - message: string; -} - -export interface TResult { - index: number; - device: TDevice; - queryVrf: string; - queryTarget: string; - queryLocation: string; - queryType: TQueryTypes; -} - -export type TErrorLevels = 'success' | 'warning' | 'error'; - -export interface TCopyButton extends ButtonProps { - copyValue: string; -} - -export interface TRequeryButton extends ButtonProps { - requery: UseQueryResult['refetch']; -} - -export type TUseResults = { - firstOpen: number | null; - locations: { [k: string]: { complete: boolean; open: boolean; index: number } }; -}; - -export type TUseResultsMethods = { - toggle(loc: string): void; - setComplete(loc: string): void; - getOpen(): number[]; -}; - -export type UseResultsReturn = { - results: State; -} & TUseResultsMethods; diff --git a/hyperglass/ui/components/results/useResults.ts b/hyperglass/ui/components/results/useResults.ts deleted file mode 100644 index 63fb6cfaf..000000000 --- a/hyperglass/ui/components/results/useResults.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { useEffect } from 'react'; -import { createState, useState } from '@hookstate/core'; - -import type { Plugin, State, PluginStateControl } from '@hookstate/core'; -import type { TUseResults, TUseResultsMethods, UseResultsReturn } from './types'; - -const MethodsId = Symbol('UseResultsMethods'); - -/** - * Plugin methods. - */ -class MethodsInstance { - /** - * Toggle a location's open/closed state. - */ - public toggle(state: State, loc: string) { - state.locations[loc].open.set(p => !p); - } - /** - * Set a location's completion state. - */ - public setComplete(state: State, loc: string) { - state.locations[loc].merge({ complete: true }); - const thisLoc = state.locations[loc]; - if ( - state.firstOpen.value === null && - state.locations.keys.includes(loc) && - state.firstOpen.value !== thisLoc.index.value - ) { - state.firstOpen.set(thisLoc.index.value); - this.toggle(state, loc); - } - } - /** - * Get the currently open panels. Passed to Chakra UI's index prop for internal state management. - */ - public getOpen(state: State) { - const open = state.locations.keys - .filter(k => state.locations[k].complete.value && state.locations[k].open.value) - .map(k => state.locations[k].index.value); - return open; - } -} - -/** - * hookstate plugin to provide convenience functions & tracking for the useResults hook. - */ -function Methods(inst?: State): Plugin | TUseResultsMethods { - if (inst) { - const [instance] = inst.attach(MethodsId) as [ - MethodsInstance | Error, - PluginStateControl, - ]; - - if (instance instanceof Error) { - throw instance; - } - - return { - toggle: (loc: string) => instance.toggle(inst, loc), - setComplete: (loc: string) => instance.setComplete(inst, loc), - getOpen: () => instance.getOpen(inst), - } as TUseResultsMethods; - } - return { - id: MethodsId, - init: () => { - /* eslint @typescript-eslint/ban-types: 0 */ - return new MethodsInstance() as {}; - }, - } as Plugin; -} -const initialState = { firstOpen: null, locations: {} } as TUseResults; -const resultsState = createState(initialState); - -/** - * Track the state of each result, and whether or not each panel is open. - */ -export function useResults(initial: TUseResults['locations']): UseResultsReturn { - // Initialize the global state before instantiating the hook, only once. - useEffect(() => { - if (resultsState.firstOpen.value === null && resultsState.locations.keys.length === 0) { - resultsState.set({ firstOpen: null, locations: initial }); - } - }, []); - - const results = useState(resultsState); - results.attach(Methods as () => Plugin); - - const methods = Methods(results) as TUseResultsMethods; - - // Reset the state on unmount. - useEffect(() => { - return () => { - results.set(initialState); - }; - }, []); - - return { results, ...methods }; -} diff --git a/hyperglass/ui/components/select/index.ts b/hyperglass/ui/components/select/index.ts index c7396734d..242fc65e9 100644 --- a/hyperglass/ui/components/select/index.ts +++ b/hyperglass/ui/components/select/index.ts @@ -1 +1,3 @@ export * from './select'; +export { isSingleValue, isMultiValue } from './types'; +export type { SelectOnChange } from './types'; diff --git a/hyperglass/ui/components/select/option.tsx b/hyperglass/ui/components/select/option.tsx new file mode 100644 index 000000000..fe9267cd2 --- /dev/null +++ b/hyperglass/ui/components/select/option.tsx @@ -0,0 +1,30 @@ +import { Badge, chakra, HStack } from '@chakra-ui/react'; +import { components } from 'react-select'; + +import type { OptionProps, GroupBase } from 'react-select'; +import type { SingleOption } from '~/types'; + +export const Option = ( + props: OptionProps, +): JSX.Element => { + const { label, data } = props; + const tags = Array.isArray(data.tags) ? (data.tags as string[]) : []; + return ( + > {...props}> + {label} + {tags.length > 0 && ( + + {tags.map(tag => ( + + {tag} + + ))} + + )} + + ); +}; diff --git a/hyperglass/ui/components/select/select.tsx b/hyperglass/ui/components/select/select.tsx index 1f73afcf9..1def4e78d 100644 --- a/hyperglass/ui/components/select/select.tsx +++ b/hyperglass/ui/components/select/select.tsx @@ -1,82 +1,101 @@ -import { createContext, useContext, useMemo } from 'react'; +import { useDisclosure } from '@chakra-ui/react'; +import { createContext, forwardRef, useContext } from 'react'; import ReactSelect from 'react-select'; -import { chakra, useDisclosure } from '@chakra-ui/react'; -import { useColorMode } from '~/context'; +import { useColorMode } from '~/hooks'; +import { Option } from './option'; import { - useRSTheme, - useMenuStyle, - useMenuPortal, - useOptionStyle, + useContainerStyle, useControlStyle, + useIndicatorSeparatorStyle, useMenuListStyle, + useMenuPortal, + useMenuStyle, + useMultiValueLabelStyle, + useMultiValueRemoveStyle, useMultiValueStyle, + useOptionStyle, usePlaceholderStyle, + useRSTheme, useSingleValueStyle, - useMultiValueLabelStyle, - useMultiValueRemoveStyle, - useIndicatorSeparatorStyle, } from './styles'; +import { isSingleValue } from './types'; -import type { TSelectOption } from '~/types'; -import type { TSelectBase, TSelectContext, TReactSelectChakra } from './types'; - -const SelectContext = createContext(Object()); -export const useSelectContext = (): TSelectContext => useContext(SelectContext); +import type { + MultiValue, + OnChangeValue, + Props as ReactSelectProps, + SelectInstance, +} from 'react-select'; +import type { SingleOption } from '~/types'; +import type { SelectContextProps, SelectProps } from './types'; -const ReactSelectChakra = chakra(ReactSelect); +const SelectContext = createContext({} as SelectContextProps); +export const useSelectContext = (): SelectContextProps => useContext(SelectContext); -export const Select: React.FC = (props: TSelectBase) => { - const { options, multi, onSelect, isError = false, ...rest } = props; - const { isOpen, onOpen, onClose } = useDisclosure(); +export const Select = forwardRef( + ( + props: SelectProps, + ref: React.Ref>, + ): JSX.Element => { + const { options, isMulti, onSelect, isError = false, components, ...rest } = props; - const { colorMode } = useColorMode(); + const { isOpen, onOpen, onClose } = useDisclosure(); - const selectContext = useMemo(() => ({ colorMode, isOpen, isError }), [ - colorMode, - isError, - isOpen, - ]); + const { colorMode } = useColorMode(); - const defaultOnChange = (changed: TSelectOption | TSelectOption[]) => { - if (!Array.isArray(changed)) { - changed = [changed]; - } - if (typeof onSelect === 'function') { - onSelect(changed); - } - }; + const defaultOnChange: ReactSelectProps['onChange'] = changed => { + if (isSingleValue(changed)) { + changed = [changed] as unknown as OnChangeValue; + } + if (typeof onSelect === 'function') { + onSelect(changed as MultiValue); + } + }; - const multiValue = useMultiValueStyle({ colorMode }); - const multiValueLabel = useMultiValueLabelStyle({ colorMode }); - const multiValueRemove = useMultiValueRemoveStyle({ colorMode }); - const menuPortal = useMenuPortal(); - const rsTheme = useRSTheme(); + const container = useContainerStyle({ colorMode }); + const menu = useMenuStyle({ colorMode }); + const menuList = useMenuListStyle({ colorMode }); + const control = useControlStyle({ colorMode }); + const option = useOptionStyle({ colorMode }); + const singleValue = useSingleValueStyle({ colorMode }); + const multiValue = useMultiValueStyle({ colorMode }); + const multiValueLabel = useMultiValueLabelStyle({ colorMode }); + const multiValueRemove = useMultiValueRemoveStyle({ colorMode }); + const menuPortal = useMenuPortal(); + const placeholder = usePlaceholderStyle({ colorMode }); + const indicatorSeparator = useIndicatorSeparatorStyle({ colorMode }); + const rsTheme = useRSTheme(); - return ( - - - - ); -}; + return ( + + + onChange={defaultOnChange} + onMenuClose={onClose} + onMenuOpen={onOpen} + isClearable={true} + options={options} + isMulti={isMulti} + theme={rsTheme} + components={{ Option, ...components }} + menuPortalTarget={typeof document !== 'undefined' ? document.body : undefined} + ref={ref} + styles={{ + container, + menu, + option, + control, + menuList, + menuPortal, + multiValue, + singleValue, + placeholder, + multiValueLabel, + multiValueRemove, + indicatorSeparator, + }} + {...rest} + /> + + ); + }, +); diff --git a/hyperglass/ui/components/select/styles.ts b/hyperglass/ui/components/select/styles.ts new file mode 100644 index 000000000..e1e20b2d2 --- /dev/null +++ b/hyperglass/ui/components/select/styles.ts @@ -0,0 +1,245 @@ +import { useToken } from '@chakra-ui/react'; +import { mergeWith } from '@chakra-ui/utils'; +/* eslint-disable react-hooks/exhaustive-deps */ +import { useCallback } from 'react'; +import { + useColorToken, + useColorValue, + useMobile, + useOpposingColor, + useOpposingColorCallback, +} from '~/hooks'; +import { useSelectContext } from './select'; + +import * as ReactSelect from 'react-select'; +import type { SingleOption } from '~/types'; +import type { RSStyleCallbackProps, RSStyleFunction, RSThemeFunction } from './types'; + +export const useContainerStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'container', Opt, IsMulti> => { + return useCallback((base, state) => { + return { width: '100%' }; + }, []); +}; + +export const useControlStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'control', Opt, IsMulti> => { + const { colorMode } = props; + + const { isError } = useSelectContext(); + + const minHeight = useToken('space', 12); + const borderRadius = useToken('radii', 'md'); + const color = useColorToken('colors', 'black', 'whiteAlpha.800'); + const focusBorder = useColorToken('colors', 'blue.500', 'blue.300'); + const invalidBorder = useColorToken('colors', 'red.500', 'red.300'); + // const borderColor = useColorToken('colors', 'gray.200', 'whiteAlpha.300'); + const borderColor = useColorToken('colors', 'gray.100', 'whiteAlpha.50'); + const borderHover = useColorToken('colors', 'gray.300', 'whiteAlpha.400'); + const backgroundColor = useColorToken('colors', 'white', 'blackSolid.800'); + + return useCallback( + (base, state) => { + const { isFocused } = state; + const styles = { + backgroundColor, + borderRadius, + color, + minHeight, + transition: 'all 0.2s', + borderColor: isError ? invalidBorder : isFocused ? focusBorder : borderColor, + boxShadow: isError + ? `0 0 0 1px ${invalidBorder}` + : isFocused + ? `0 0 0 1px ${focusBorder}` + : undefined, + '&:hover': { borderColor: isFocused ? focusBorder : borderHover }, + '&:hover > div > span': { backgroundColor: borderHover }, + '&:focus': { borderColor: isError ? invalidBorder : focusBorder }, + '&.invalid': { borderColor: invalidBorder, boxShadow: `0 0 0 1px ${invalidBorder}` }, + }; + return mergeWith({}, base, styles); + }, + [colorMode, isError], + ); +}; + +export const useMenuStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'menu', Opt, IsMulti> => { + const { colorMode } = props; + + const { isOpen } = useSelectContext(); + + const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700'); + const styles = { backgroundColor, zIndex: 1500 }; + + return useCallback(base => mergeWith({}, base, styles), [colorMode, isOpen]); +}; + +export const useMenuListStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'menuList', Opt, IsMulti> => { + const { colorMode } = props; + + const { isOpen } = useSelectContext(); + + const borderRadius = useToken('radii', 'md'); + const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700'); + const scrollbarTrack = useColorToken('colors', 'blackAlpha.50', 'whiteAlpha.50'); + const scrollbarThumb = useColorToken('colors', 'blackAlpha.300', 'whiteAlpha.300'); + const scrollbarThumbHover = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400'); + const styles = { + borderRadius, + backgroundColor, + '&::-webkit-scrollbar': { width: '5px' }, + '&::-webkit-scrollbar-track': { backgroundColor: scrollbarTrack }, + '&::-webkit-scrollbar-thumb': { backgroundColor: scrollbarThumb }, + '&::-webkit-scrollbar-thumb:hover': { backgroundColor: scrollbarThumbHover }, + '-ms-overflow-style': { display: 'none' }, + }; + + return useCallback(base => mergeWith({}, base, styles), [colorMode, isOpen]); +}; + +export const useOptionStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'option', Opt, IsMulti> => { + const { colorMode } = props; + + const { isOpen } = useSelectContext(); + + const fontSize = useToken('fontSizes', 'lg'); + const disabled = useToken('colors', 'whiteAlpha.400'); + const active = useColorToken('colors', 'primary.600', 'primary.400'); + const focused = useColorToken('colors', 'primary.500', 'primary.300'); + const selected = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400'); + + const activeColor = useOpposingColor(active); + const getColor = useOpposingColorCallback(); + + return useCallback( + (base, state) => { + const { isFocused, isSelected, isDisabled } = state; + + let backgroundColor = 'transparent'; + switch (true) { + case isDisabled: + backgroundColor = disabled; + break; + case isSelected: + backgroundColor = selected; + break; + case isFocused: + backgroundColor = focused; + break; + } + const color = getColor(backgroundColor); + + const styles = { + color: backgroundColor === 'transparent' ? 'currentColor' : color, + '&:active': { backgroundColor: active, color: activeColor }, + '&:focus': { backgroundColor: active, color: activeColor }, + backgroundColor, + fontSize, + }; + + return mergeWith({}, base, styles); + }, + [isOpen, colorMode], + ); +}; + +export const useIndicatorSeparatorStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'indicatorSeparator', Opt, IsMulti> => { + const { colorMode } = props; + const backgroundColor = useColorToken('colors', 'gray.200', 'whiteAlpha.300'); + const styles = { backgroundColor }; + + return useCallback(base => mergeWith({}, base, styles), [colorMode]); +}; + +export const usePlaceholderStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'placeholder', Opt, IsMulti> => { + const { colorMode } = props; + + const color = useColorToken('colors', 'gray.600', 'whiteAlpha.700'); + const fontSize = useToken('fontSizes', 'lg'); + + return useCallback(base => mergeWith({}, base, { color, fontSize }), [colorMode]); +}; + +export const useSingleValueStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'singleValue', Opt, IsMulti> => { + const { colorMode } = props; + + const color = useColorValue('black', 'whiteAlpha.800'); + const fontSize = useToken('fontSizes', 'lg'); + const styles = { color, fontSize }; + + return useCallback(base => mergeWith({}, base, styles), [color, colorMode]); +}; + +export const useMultiValueStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'multiValue', Opt, IsMulti> => { + const { colorMode } = props; + + const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300'); + const color = useOpposingColor(backgroundColor); + const borderRadius = useToken('radii', 'md'); + const styles = { backgroundColor, color, borderRadius }; + + return useCallback(base => mergeWith({}, base, styles), [backgroundColor, colorMode]); +}; + +export const useMultiValueLabelStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'multiValueLabel', Opt, IsMulti> => { + const { colorMode } = props; + + const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300'); + const color = useOpposingColor(backgroundColor); + const styles = { color }; + + return useCallback(base => mergeWith({}, base, styles), [colorMode]); +}; + +export const useMultiValueRemoveStyle = ( + props: RSStyleCallbackProps, +): RSStyleFunction<'multiValueRemove', Opt, IsMulti> => { + const { colorMode } = props; + + const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300'); + const color = useOpposingColor(backgroundColor); + const styles = { + color, + '&:hover': { backgroundColor: 'transparent', color, opacity: 0.8 }, + }; + + return useCallback(base => mergeWith({}, base, styles), [colorMode]); +}; + +export const useRSTheme = (): RSThemeFunction => { + const borderRadius = useToken('radii', 'md') as unknown as number; + + return useCallback((t: ReactSelect.Theme): ReactSelect.Theme => ({ ...t, borderRadius }), []); +}; + +export const useMenuPortal = (): RSStyleFunction< + 'menuPortal', + Opt, + IsMulti +> => { + const isMobile = useMobile(); + const styles = { + zIndex: 1500, + }; + + return useCallback(base => mergeWith({}, base, styles), [isMobile]); +}; diff --git a/hyperglass/ui/components/select/styles.tsx b/hyperglass/ui/components/select/styles.tsx deleted file mode 100644 index 0dfc6e1b3..000000000 --- a/hyperglass/ui/components/select/styles.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { useToken } from '@chakra-ui/react'; -import { mergeWith } from '@chakra-ui/utils'; -import { useOpposingColor } from '~/hooks'; -import { useColorValue, useColorToken, useMobile } from '~/context'; -import { useSelectContext } from './select'; - -import type { - TMenu, - TOption, - TStyles, - TControl, - TRSTheme, - TMultiValue, - TRSThemeCallback, - TRSStyleCallback, -} from './types'; - -export const useControlStyle = (base: TStyles, state: TControl): TStyles => { - const { isFocused } = state; - const { colorMode, isError } = useSelectContext(); - - const minHeight = useToken('space', 12); - const borderRadius = useToken('radii', 'md'); - const color = useColorToken('colors', 'black', 'whiteAlpha.800'); - const focusBorder = useColorToken('colors', 'blue.500', 'blue.300'); - const invalidBorder = useColorToken('colors', 'red.500', 'red.300'); - const borderColor = useColorToken('colors', 'gray.100', 'whiteAlpha.50'); - const borderHover = useColorToken('colors', 'gray.300', 'whiteAlpha.400'); - const backgroundColor = useColorToken('colors', 'white', 'whiteAlpha.100'); - - const styles = { - backgroundColor, - borderRadius, - color, - minHeight, - transition: 'all 0.2s', - borderColor: isError ? invalidBorder : isFocused ? focusBorder : borderColor, - boxShadow: isError - ? `0 0 0 1px ${invalidBorder}` - : isFocused - ? `0 0 0 1px ${focusBorder}` - : undefined, - '&:hover': { borderColor: isFocused ? focusBorder : borderHover }, - '&:hover > div > span': { backgroundColor: borderHover }, - '&:focus': { borderColor: isError ? invalidBorder : focusBorder }, - '&.invalid': { borderColor: invalidBorder, boxShadow: `0 0 0 1px ${invalidBorder}` }, - }; - return useMemo(() => mergeWith({}, base, styles), [colorMode, isFocused, isError]); -}; - -export const useMenuStyle = (base: TStyles, _: TMenu): TStyles => { - const { colorMode, isOpen } = useSelectContext(); - const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700'); - const styles = { backgroundColor }; - return useMemo(() => mergeWith({}, base, styles), [colorMode, isOpen]); -}; - -export const useMenuListStyle = (base: TStyles): TStyles => { - const { colorMode, isOpen } = useSelectContext(); - const borderRadius = useToken('radii', 'md'); - const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700'); - const scrollbarTrack = useColorToken('colors', 'blackAlpha.50', 'whiteAlpha.50'); - const scrollbarThumb = useColorToken('colors', 'blackAlpha.300', 'whiteAlpha.300'); - const scrollbarThumbHover = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400'); - - const styles = { - borderRadius, - backgroundColor, - '&::-webkit-scrollbar': { width: '5px' }, - '&::-webkit-scrollbar-track': { backgroundColor: scrollbarTrack }, - '&::-webkit-scrollbar-thumb': { backgroundColor: scrollbarThumb }, - '&::-webkit-scrollbar-thumb:hover': { backgroundColor: scrollbarThumbHover }, - '-ms-overflow-style': { display: 'none' }, - }; - return useMemo(() => mergeWith({}, base, styles), [colorMode, isOpen]); -}; - -export const useOptionStyle = (base: TStyles, state: TOption): TStyles => { - const { isFocused, isSelected, isDisabled } = state; - const { colorMode, isOpen } = useSelectContext(); - - const fontSize = useToken('fontSizes', 'lg'); - const disabled = useToken('colors', 'whiteAlpha.400'); - const active = useColorToken('colors', 'primary.600', 'primary.400'); - const focused = useColorToken('colors', 'primary.500', 'primary.300'); - const selected = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400'); - - const activeColor = useOpposingColor(active); - - const backgroundColor = useMemo(() => { - let bg = 'transparent'; - switch (true) { - case isDisabled: - bg = disabled; - break; - case isSelected: - bg = selected; - break; - case isFocused: - bg = focused; - break; - } - return bg; - }, [isDisabled, isFocused, isSelected]); - - const color = useOpposingColor(backgroundColor); - - const styles = { - color: backgroundColor === 'transparent' ? 'currentColor' : color, - '&:active': { backgroundColor: active, color: activeColor }, - '&:focus': { backgroundColor: active, color: activeColor }, - backgroundColor, - fontSize, - }; - - return useMemo(() => mergeWith({}, base, styles), [ - isOpen, - colorMode, - isFocused, - isDisabled, - isSelected, - ]); -}; - -export const useIndicatorSeparatorStyle = (base: TStyles): TStyles => { - const { colorMode } = useSelectContext(); - const backgroundColor = useColorToken('colors', 'whiteAlpha.700', 'gray.600'); - const styles = { backgroundColor }; - return useMemo(() => mergeWith({}, base, styles), [colorMode]); -}; - -export const usePlaceholderStyle = (base: TStyles): TStyles => { - const { colorMode } = useSelectContext(); - const color = useColorToken('colors', 'gray.600', 'whiteAlpha.700'); - const fontSize = useToken('fontSizes', 'lg'); - return useMemo(() => mergeWith({}, base, { color, fontSize }), [colorMode]); -}; - -export const useSingleValueStyle = (): TRSStyleCallback => { - const { colorMode } = useSelectContext(); - - const color = useColorValue('black', 'whiteAlpha.800'); - const fontSize = useToken('fontSizes', 'lg'); - - const styles = { color, fontSize }; - return useCallback((base: TStyles) => mergeWith({}, base, styles), [color, colorMode]); -}; - -export const useMultiValueStyle = (props: TMultiValue): TRSStyleCallback => { - const { colorMode } = props; - - const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300'); - const color = useOpposingColor(backgroundColor); - - const styles = { backgroundColor, color }; - return useCallback((base: TStyles) => mergeWith({}, base, styles), [backgroundColor, colorMode]); -}; - -export const useMultiValueLabelStyle = (props: TMultiValue): TRSStyleCallback => { - const { colorMode } = props; - - const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300'); - const color = useOpposingColor(backgroundColor); - - const styles = { color }; - return useCallback((base: TStyles) => mergeWith({}, base, styles), [colorMode]); -}; - -export const useMultiValueRemoveStyle = (props: TMultiValue): TRSStyleCallback => { - const { colorMode } = props; - - const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300'); - const color = useOpposingColor(backgroundColor); - - const styles = { - color, - '&:hover': { backgroundColor: 'inherit', color, opacity: 0.7 }, - }; - return useCallback((base: TStyles) => mergeWith({}, base, styles), [colorMode]); -}; - -export const useRSTheme = (): TRSThemeCallback => { - const borderRadius = useToken('radii', 'md'); - return useCallback((t: TRSTheme): TRSTheme => ({ ...t, borderRadius }), []); -}; - -export const useMenuPortal = (): TRSStyleCallback => { - const isMobile = useMobile(); - const styles = { - zIndex: isMobile ? 1500 : 1, - }; - return useCallback((base: TStyles) => mergeWith({}, base, styles), [isMobile]); -}; diff --git a/hyperglass/ui/components/select/types.ts b/hyperglass/ui/components/select/types.ts index d717a7a77..1889776fe 100644 --- a/hyperglass/ui/components/select/types.ts +++ b/hyperglass/ui/components/select/types.ts @@ -1,81 +1,58 @@ -/* eslint @typescript-eslint/no-explicit-any: 0 */ -/* eslint @typescript-eslint/explicit-module-boundary-types: 0 */ +import * as ReactSelect from 'react-select'; -import type { - Props as IReactSelect, - ControlProps, - MenuProps, - MenuListComponentProps, - OptionProps, - MultiValueProps, - IndicatorProps, - Theme as RSTheme, - PlaceholderProps, - Styles as RSStyles, -} from 'react-select'; -import type { BoxProps } from '@chakra-ui/react'; -import type { Theme, TSelectOption, TSelectOptionMulti, TSelectOptionGroup } from '~/types'; +import type { CSSObjectWithLabel } from 'react-select'; +import type { StylesProps } from 'react-select/dist/declarations/src/styles'; +import type { Theme, SingleOption } from '~/types'; -export interface TSelectState { - [k: string]: string[]; -} - -export type TOptions = Array; +type StylesConfigFunction = (base: CSSObjectWithLabel, props: Props) => CSSObjectWithLabel; -export type TReactSelectChakra = Omit & - Omit; +export type SelectOnChange< + Opt extends SingleOption = SingleOption, + IsMulti extends boolean = boolean, +> = NonNullable, 'onChange'>>; -export interface TSelectBase extends TReactSelectChakra { +export interface SelectProps + extends ReactSelect.Props { name: string; - multi?: boolean; + isMulti?: IsMulti; isError?: boolean; - options: TOptions; required?: boolean; - onSelect?: (s: TSelectOption[]) => void; - onChange?: (c: TSelectOption | TSelectOptionMulti) => void; + onSelect?: (s: ReactSelect.MultiValue) => void; colorScheme?: Theme.ColorNames; } -export interface TSelectContext { +export interface SelectContextProps { colorMode: 'light' | 'dark'; isOpen: boolean; isError: boolean; } -export interface TMultiValueRemoveProps { - children: Node; - data: any; - innerProps: { - className: string; - onTouchEnd: (e: any) => void; - onClick: (e: any) => void; - onMouseDown: (e: any) => void; - }; - selectProps: any; -} - -export interface TRSTheme extends Omit { - borderRadius: string | number; +export interface RSStyleCallbackProps { + colorMode: 'light' | 'dark'; } -export type TControl = ControlProps; - -export type TMenu = MenuProps; - -export type TMenuList = MenuListComponentProps; +type StyleConfigKeys = keyof ReactSelect.StylesConfig< + SingleOption, + boolean, + ReactSelect.GroupBase +>; -export type TOption = OptionProps; +export type RSStyleFunction< + K extends StyleConfigKeys, + Opt extends SingleOption, + IsMulti extends boolean, +> = StylesConfigFunction>[K]>; -export type TMultiValueState = MultiValueProps; +export type RSThemeFunction = (theme: ReactSelect.Theme) => ReactSelect.Theme; -export type TIndicator = IndicatorProps; - -export type TPlaceholder = PlaceholderProps; - -export type TMultiValue = Pick; - -export type TRSStyleCallback = (base: TStyles) => TStyles; - -export type TRSThemeCallback = (theme: TRSTheme) => TRSTheme; +export function isSingleValue( + value: ReactSelect.SingleValue | ReactSelect.MultiValue, +): value is NonNullable> { + return value !== null && !Array.isArray(value); +} -export type TStyles = RSStyles; +export function isMultiValue( + value: ReactSelect.SingleValue | ReactSelect.MultiValue, +): value is NonNullable> { + return value !== null && Array.isArray(value); +} diff --git a/hyperglass/ui/components/submit/submit.tsx b/hyperglass/ui/components/submit-button.tsx similarity index 56% rename from hyperglass/ui/components/submit/submit.tsx rename to hyperglass/ui/components/submit-button.tsx index 4e718e871..ef4cbd5bc 100644 --- a/hyperglass/ui/components/submit/submit.tsx +++ b/hyperglass/ui/components/submit-button.tsx @@ -1,26 +1,33 @@ -import { forwardRef } from 'react'; import { + IconButton, Modal, - Popover, ModalBody, - IconButton, - PopoverBody, - ModalOverlay, + ModalCloseButton, ModalContent, + ModalOverlay, + Popover, PopoverArrow, - PopoverTrigger, - PopoverContent, - ModalCloseButton, + PopoverBody, PopoverCloseButton, + PopoverContent, + PopoverTrigger, } from '@chakra-ui/react'; -import { FiSearch } from '@meronex/icons/fi'; +import { forwardRef } from 'react'; import { useFormContext } from 'react-hook-form'; -import { If, ResolvedTarget } from '~/components'; -import { useMobile, useColorValue } from '~/context'; -import { useLGState, useLGMethods } from '~/hooks'; +import { Else, If, Then } from 'react-if'; +import { ResolvedTarget } from '~/components'; +import { DynamicIcon } from '~/elements'; +import { useColorValue, useFormState, useMobile } from '~/hooks'; import type { IconButtonProps } from '@chakra-ui/react'; -import type { TSubmitButton, TRSubmitButton } from './types'; + +type SubmitButtonProps = Omit; + +interface ResponsiveSubmitButtonProps { + isOpen: boolean; + onClose(): void; + children: React.ReactNode; +} const _SubmitIcon: React.ForwardRefRenderFunction< HTMLButtonElement, @@ -33,7 +40,7 @@ const _SubmitIcon: React.ForwardRefRenderFunction< size="lg" width={16} type="submit" - icon={} + icon={} title="Submit Query" colorScheme="primary" isLoading={isLoading} @@ -47,8 +54,8 @@ const SubmitIcon = forwardRef = (props: TRSubmitButton) => { - const { children, isOpen, onClose, onChange } = props; +const MSubmitButton = (props: ResponsiveSubmitButtonProps): JSX.Element => { + const { children, isOpen, onClose } = props; const bg = useColorValue('white', 'gray.900'); return ( <> @@ -66,7 +73,7 @@ const MSubmitButton: React.FC = (props: TRSubmitButton) => { - {isOpen && } + {isOpen && } @@ -77,8 +84,8 @@ const MSubmitButton: React.FC = (props: TRSubmitButton) => { /** * Desktop Submit Button */ -const DSubmitButton: React.FC = (props: TRSubmitButton) => { - const { children, isOpen, onClose, onChange } = props; +const DSubmitButton = (props: ResponsiveSubmitButtonProps): JSX.Element => { + const { children, isOpen, onClose } = props; const bg = useColorValue('white', 'gray.900'); return ( @@ -86,40 +93,45 @@ const DSubmitButton: React.FC = (props: TRSubmitButton) => { - - {isOpen && } - + {isOpen && } ); }; -export const SubmitButton: React.FC = (props: TSubmitButton) => { - const { handleChange } = props; +export const SubmitButton = (props: SubmitButtonProps): JSX.Element => { const isMobile = useMobile(); - const { resolvedIsOpen, btnLoading } = useLGState(); - const { resolvedClose, resetForm } = useLGMethods(); + const loading = useFormState(s => s.loading); + const { + resolvedIsOpen, + resolvedClose, + reset: resetForm, + } = useFormState(({ resolvedIsOpen, resolvedClose, reset }) => ({ + resolvedIsOpen, + resolvedClose, + reset, + })); const { reset } = useFormContext(); - function handleClose(): void { + async function handleClose() { reset(); resetForm(); resolvedClose(); } return ( - <> - - - + + + + - - - - + + + + - - + + ); }; diff --git a/hyperglass/ui/components/submit/index.ts b/hyperglass/ui/components/submit/index.ts deleted file mode 100644 index e9be2e304..000000000 --- a/hyperglass/ui/components/submit/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './submit'; diff --git a/hyperglass/ui/components/submit/types.ts b/hyperglass/ui/components/submit/types.ts deleted file mode 100644 index 91ca90bf5..000000000 --- a/hyperglass/ui/components/submit/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { IconButtonProps } from '@chakra-ui/react'; -import type { OnChangeArgs } from '~/types'; - -export interface TSubmitButton extends Omit { - handleChange(e: OnChangeArgs): void; -} - -export interface TRSubmitButton { - isOpen: boolean; - onClose(): void; - onChange(e: OnChangeArgs): void; - children: React.ReactNode; -} diff --git a/hyperglass/ui/components/table/body.tsx b/hyperglass/ui/components/table/body.tsx index 2d62a4f26..0f77a7d93 100644 --- a/hyperglass/ui/components/table/body.tsx +++ b/hyperglass/ui/components/table/body.tsx @@ -1,10 +1,9 @@ -import { Box } from '@chakra-ui/react'; +import { chakra } from '@chakra-ui/react'; import type { BoxProps } from '@chakra-ui/react'; -export const TableBody: React.FC = (props: BoxProps) => ( - ( + = (props: TTableIconButton) => ( +type TTableIconButton = Omit; + +export const TableIconButton = (props: TTableIconButton): JSX.Element => ( ); diff --git a/hyperglass/ui/components/table/cell.tsx b/hyperglass/ui/components/table/cell.tsx index 618d09470..228576ffc 100644 --- a/hyperglass/ui/components/table/cell.tsx +++ b/hyperglass/ui/components/table/cell.tsx @@ -1,9 +1,14 @@ -import { Box } from '@chakra-ui/react'; -import { useColorValue } from '~/context'; +import { chakra } from '@chakra-ui/react'; +import { useColorValue } from '~/hooks'; -import type { TTableCell } from './types'; +import type { BoxProps } from '@chakra-ui/react'; -export const TableCell: React.FC = (props: TTableCell) => { +interface TableCellProps extends Omit { + bordersVertical?: [boolean, number]; + align?: 'left' | 'right' | 'center'; +} + +export const TableCell = (props: TableCellProps): JSX.Element => { const { bordersVertical = [false, 0], align, ...rest } = props; const [doVerticalBorders, index] = bordersVertical; const borderLeftColor = useColorValue('blackAlpha.100', 'whiteAlpha.100'); @@ -14,11 +19,10 @@ export const TableCell: React.FC = (props: TTableCell) => { } return ( - = (props: BoxProps) => { +export const TableHead = (props: BoxProps): JSX.Element => { const bg = useColorValue('blackAlpha.100', 'whiteAlpha.100'); - return ; + return ; }; diff --git a/hyperglass/ui/components/table/index.ts b/hyperglass/ui/components/table/index.ts index 72853240f..aad1ca831 100644 --- a/hyperglass/ui/components/table/index.ts +++ b/hyperglass/ui/components/table/index.ts @@ -1,8 +1 @@ -export * from './body'; -export * from './button'; -export * from './cell'; -export * from './head'; export * from './main'; -export * from './main'; -export * from './pageSelect'; -export * from './row'; diff --git a/hyperglass/ui/components/table/main.tsx b/hyperglass/ui/components/table/main.tsx index 41bd90ac4..fd1eeef5f 100644 --- a/hyperglass/ui/components/table/main.tsx +++ b/hyperglass/ui/components/table/main.tsx @@ -1,43 +1,34 @@ // This rule isn't needed because react-table does this for us, for better or worse. /* eslint react/jsx-key: 0 */ - -import dynamic from 'next/dynamic'; -import { Flex, Icon, Text } from '@chakra-ui/react'; +import { Flex, Text } from '@chakra-ui/react'; import { usePagination, useSortBy, useTable } from 'react-table'; -import { useMobile } from '~/context'; -import { CardBody, CardFooter, CardHeader, If } from '~/components'; +import { If, Then, Else } from 'react-if'; +import { CardBody, CardFooter, CardHeader, DynamicIcon } from '~/elements'; +import { useMobile } from '~/hooks'; import { TableMain } from './table'; import { TableCell } from './cell'; import { TableHead } from './head'; import { TableRow } from './row'; import { TableBody } from './body'; import { TableIconButton } from './button'; -import { TableSelectShow } from './pageSelect'; +import { PageSelect } from './page-select'; import type { TableOptions, PluginHook } from 'react-table'; -import type { TCellRender } from '~/types'; -import type { TTable } from './types'; - -const ChevronRight = dynamic(() => - import('@meronex/icons/fa').then(i => i.FaChevronRight), -); - -const ChevronLeft = dynamic(() => - import('@meronex/icons/fa').then(i => i.FaChevronLeft), -); - -const ChevronDown = dynamic(() => - import('@meronex/icons/fa').then(i => i.FaChevronDown), -); - -const DoubleChevronRight = dynamic(() => - import('@meronex/icons/fi').then(i => i.FiChevronsRight), -); -const DoubleChevronLeft = dynamic(() => - import('@meronex/icons/fi').then(i => i.FiChevronsLeft), -); - -export const Table: React.FC = (props: TTable) => { +import type { Theme, TableColumn, CellRenderProps } from '~/types'; + +interface TableProps { + data: Route[]; + striped?: boolean; + columns: TableColumn[]; + heading?: React.ReactNode; + bordersVertical?: boolean; + bordersHorizontal?: boolean; + Cell?: React.FC; + rowHighlightProp?: keyof Route; + rowHighlightBg?: Theme.ColorNames; +} + +export const Table = (props: TableProps): JSX.Element => { const { data, columns, @@ -71,11 +62,11 @@ export const Table: React.FC = (props: TTable) => { defaultColumn, data, initialState: { hiddenColumns }, - } as TableOptions; + } as TableOptions; - const plugins = [useSortBy, usePagination] as PluginHook[]; + const plugins = [useSortBy, usePagination] as PluginHook[]; - const instance = useTable(options, ...plugins); + const instance = useTable(options, ...plugins); const { page, @@ -108,17 +99,21 @@ export const Table: React.FC = (props: TTable) => { {...column.getSortByToggleProps()} > - {column.render('Header')} + {column.render('Header') as React.ReactNode} - - - - - - - + + + + + + + + + + + + {''} - {''} ))} @@ -137,7 +132,7 @@ export const Table: React.FC = (props: TTable) => { {...row.getRowProps()} > {row.cells.map((cell, i) => { - const { column, row, value } = cell as TCellRender; + const { column, row, value } = cell as CellRenderProps; return ( = (props: TTable) => { {typeof Cell !== 'undefined' ? ( ) : ( - cell.render('Cell') + (cell.render('Cell') as React.ReactNode) )} ); @@ -163,13 +158,13 @@ export const Table: React.FC = (props: TTable) => { mr={2} onClick={() => gotoPage(0)} isDisabled={!canPreviousPage} - icon={} + icon={} /> previousPage()} isDisabled={!canPreviousPage} - icon={} + icon={} /> @@ -180,7 +175,7 @@ export const Table: React.FC = (props: TTable) => { {' '}
{!isMobile && ( - { setPageSize(Number(e.target.value)); @@ -193,12 +188,12 @@ export const Table: React.FC = (props: TTable) => { ml={2} onClick={nextPage} isDisabled={!canNextPage} - icon={} + icon={} /> } + icon={} onClick={() => gotoPage(pageCount ? pageCount - 1 : 1)} /> diff --git a/hyperglass/ui/components/table/pageSelect.tsx b/hyperglass/ui/components/table/page-select.tsx similarity index 69% rename from hyperglass/ui/components/table/pageSelect.tsx rename to hyperglass/ui/components/table/page-select.tsx index fafa6d902..1605ef4b4 100644 --- a/hyperglass/ui/components/table/pageSelect.tsx +++ b/hyperglass/ui/components/table/page-select.tsx @@ -1,7 +1,8 @@ import { Select } from '@chakra-ui/react'; -import { SelectProps } from '@chakra-ui/react'; -export const TableSelectShow: React.FC = (props: SelectProps) => { +import type { SelectProps } from '@chakra-ui/react'; + +export const PageSelect = (props: SelectProps): JSX.Element => { const { value, ...rest } = props; return (