forked from mirrors/thatmattlove-hyperglass
Compare commits
45 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d21da01a1 | |||
| ee7d8752f8 | |||
| 18e0b3e7e7 | |||
|
|
0b2fbb1b4d | ||
|
|
c2142ee76f | ||
|
|
ec3c55aa81 | ||
|
|
a5bc1ca8a0 | ||
|
|
0f52bc438f | ||
|
|
e0751311ba | ||
|
|
f340e65082 | ||
|
|
872c3ec654 | ||
|
|
41248231ae | ||
|
|
b617df24d1 | ||
|
|
3b0abd5ba8 | ||
|
|
0fd5bc7997 | ||
|
|
e8997b981c | ||
|
|
bbba29546c | ||
|
|
7eb8d8e925 | ||
|
|
4733dd1893 | ||
|
|
08fd310b44 | ||
|
|
6d06b9809d | ||
|
|
30fda91bc8 | ||
|
|
e19dd675e5 | ||
|
|
8a9766b99f | ||
|
|
4d5e259f6e | ||
|
|
f324d34323 | ||
|
|
3d82a21ba4 | ||
|
|
0137b042dc | ||
|
|
6b37ce96f6 | ||
|
|
aab4ada723 | ||
|
|
bfcae89bf0 | ||
|
|
8a3d704eca | ||
|
|
b796149a26 | ||
|
|
0c643c6abd | ||
|
|
7eb4f5ca93 | ||
|
|
35d9c26eff | ||
|
|
1d1dcd8319 | ||
|
|
c79ba8b727 | ||
|
|
c8a348ed0f | ||
|
|
4b6e6cba70 | ||
|
|
ad88d025c2 | ||
|
|
72887e0f9a | ||
|
|
85fa678d74 | ||
|
|
74a6ee3ab8 | ||
|
|
556dccf509 |
119 changed files with 3865 additions and 2953 deletions
30
.github/ISSUE_TEMPLATE/1-feature-request.md
vendored
30
.github/ISSUE_TEMPLATE/1-feature-request.md
vendored
|
|
@ -1,30 +0,0 @@
|
||||||
---
|
|
||||||
name: Feature Request
|
|
||||||
about: Suggest an idea for hyperglass
|
|
||||||
labels:
|
|
||||||
- feature
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If the answer to any of these questions is "no", your feature request will most likely be rejected (but will 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 end-users?
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Feature Description
|
|
||||||
|
|
||||||
<!-- Describe the solution or change you would like. -->
|
|
||||||
|
|
||||||
### Is your feature request related to a problem? Please describe.
|
|
||||||
|
|
||||||
<!-- If yes, a clear and concise description of what the problem is. -->
|
|
||||||
|
|
||||||
# Environment & Use Case
|
|
||||||
|
|
||||||
<!-- Describe your use case for hyperglass, the environment on which it runs, the predominant network device type, and any other relevant details. -->
|
|
||||||
|
|
||||||
# Additional Context
|
|
||||||
|
|
||||||
<!-- Add any other context or screenshots about the feature request here. -->
|
|
||||||
49
.github/ISSUE_TEMPLATE/1-feature-request.yaml
vendored
Normal file
49
.github/ISSUE_TEMPLATE/1-feature-request.yaml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
name: Feature Request
|
||||||
|
description: Suggest an idea for hyperglass
|
||||||
|
labels:
|
||||||
|
- feature
|
||||||
|
assignees:
|
||||||
|
- thatmattlove
|
||||||
|
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)?
|
||||||
64
.github/ISSUE_TEMPLATE/2-bug-report.md
vendored
64
.github/ISSUE_TEMPLATE/2-bug-report.md
vendored
|
|
@ -1,64 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug Report
|
|
||||||
about: Report a problem or unexpected behavior
|
|
||||||
labels:
|
|
||||||
- possible-bug
|
|
||||||
assignees: thatmattlove
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- Please provide a general summary of the issue in the Title. -->
|
|
||||||
|
|
||||||
# Bug Description
|
|
||||||
|
|
||||||
<!-- A clear and concise description of the bug. -->
|
|
||||||
|
|
||||||
# Expected behavior
|
|
||||||
|
|
||||||
<!-- A clear and concise description of what you expected to happen. -->
|
|
||||||
|
|
||||||
# Steps to Reproduce
|
|
||||||
|
|
||||||
<!-- Provide steps necessary to reproduce this issue. -->
|
|
||||||
|
|
||||||
## Local Configurations
|
|
||||||
|
|
||||||
<!-- If possible, please a link to a live example and the relevant sections of your hyperglass.yaml, commands.yaml, or devices.yaml in a code block. -->
|
|
||||||
|
|
||||||
```
|
|
||||||
<configs>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logs
|
|
||||||
|
|
||||||
<!-- If an error occurred, please paste the relevant error message(s) in the below code block. -->
|
|
||||||
|
|
||||||
```
|
|
||||||
<logs>
|
|
||||||
```
|
|
||||||
|
|
||||||
# Possible Solution
|
|
||||||
|
|
||||||
<!-- If you think you know what would fix this, please share your ideas. -->
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
|
|
||||||
## Server
|
|
||||||
|
|
||||||
<!-- Please paste the output from `hyperglass system-info` below: -->
|
|
||||||
|
|
||||||
<!-- If, for some reason, that doesn't work, please include the following:
|
|
||||||
- OS:
|
|
||||||
- Python Version:
|
|
||||||
- hyperglass Version:
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Client
|
|
||||||
|
|
||||||
- OS & Version: <!-- (e.g. Windows 10, macOS 10.15, Ubuntu Linux 18.04) -->
|
|
||||||
- Browser: <!-- (e.g. Chrome, Safari, Firefox, etc.) -->
|
|
||||||
|
|
||||||
### Smartphone Details (if applicable)
|
|
||||||
|
|
||||||
- Device: <!-- (e.g. iPhone, Samsung) -->
|
|
||||||
- OS: <!-- (e.g. iOS 13.1, Android 11) -->
|
|
||||||
- Browser: <!-- (e.g. Safari, Chrome) -->
|
|
||||||
98
.github/ISSUE_TEMPLATE/2-bug-report.yaml
vendored
Normal file
98
.github/ISSUE_TEMPLATE/2-bug-report.yaml
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
name: Bug Report
|
||||||
|
description: Report a problem or unexpected behavior
|
||||||
|
labels:
|
||||||
|
- possible-bug
|
||||||
|
assignees:
|
||||||
|
- thatmattlove
|
||||||
|
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
|
||||||
27
.github/ISSUE_TEMPLATE/3-new-nos.yaml
vendored
27
.github/ISSUE_TEMPLATE/3-new-nos.yaml
vendored
|
|
@ -1,27 +0,0 @@
|
||||||
---
|
|
||||||
name: New Platform
|
|
||||||
description: Request native support for a network operating system/platform
|
|
||||||
labels:
|
|
||||||
- feature
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: >
|
|
||||||
In order to natively support a new platform for hyperglass, please make sure it is supported by Netmiko ([see here](https://hyperglass.dev/platforms)).
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Manufacturer
|
|
||||||
description: What is the network vendor? For example, for Juniper Junos, this would be Juniper.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Name
|
|
||||||
description: What is the name of this platform? 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 the default hyperglass commands (IPv4 BGP Route, IPv6 BGP Route, BGP AS Path, BGP Community, IPv4 ping, IPv6 ping, IPv4 traceroute, and IPv6 traceroute). 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.
|
|
||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1 +1,5 @@
|
||||||
blank_issues_enabled: false
|
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.
|
||||||
|
|
|
||||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- PLEASE CONSULT CONTRIBUTING.MD PRIOR TO WORKING ON HYPERGLASS -->
|
<!-- PLEASE CONSULT CONTRIBUTING POLICY PRIOR TO WORKING ON HYPERGLASS -->
|
||||||
|
|
||||||
<!-- Provide a general summary of your changes in the Title. -->
|
<!-- Provide a general summary of your changes in the Title. -->
|
||||||
|
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
# Project
|
# Project
|
||||||
hyperglass/hyperglass/static
|
static/
|
||||||
TODO*
|
TODO*
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
|
|
||||||
49
CHANGELOG.md
49
CHANGELOG.md
|
|
@ -4,6 +4,55 @@ 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).
|
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).
|
||||||
|
|
||||||
|
## 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
|
## 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._
|
_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._
|
||||||
|
|
|
||||||
|
|
@ -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**
|
- **Pristine code quality**
|
||||||
- [Black](https://github.com/python/black) formatting for Python
|
- [Black](https://github.com/python/black) formatting for Python.
|
||||||
- Strict adherence to ESLint/Prettier configs for Javascript/React
|
- Strict adherence to ESLint/Prettier configs for frontend code.
|
||||||
- _ZERO_ linting errors
|
- _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.
|
- 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**
|
- **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.
|
- 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.
|
- This includes things like timeouts, error messages, etc.
|
||||||
- **Mobile & Accessible**
|
- **Mobile & Accessible**
|
||||||
- All UI element must be available on both desktop and mobile devices
|
- All UI element must be available on both desktop and mobile devices.
|
||||||
- UI must achieve a 100 Lighthouse/PageInsights score for accessibility
|
- UI must achieve a 100 Lighthouse/PageInsights score for accessibility.
|
||||||
- **IPv6 Support**
|
- **IPv6 Support**
|
||||||
- Any new device support must include IPv6 commands
|
- Any new device support must include IPv6 commands.
|
||||||
- All frontend and backend code must support IPv6, both for running the application and processing queries
|
- 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.
|
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
The Clear BSD License
|
The Clear BSD License
|
||||||
|
|
||||||
Copyright (c) 2021 Matthew Love
|
Copyright (c) 2024 Matthew Love
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ hyperglass is intended to make implementing a looking glass too easy not to do,
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### [Changelog](https://github.com/thatmattlove/hyperglass/blob/v2.0.0/CHANGELOG.md)
|
### [Changelog](https://hyperglass.dev/changelog)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
|
||||||
87
biome.json
87
biome.json
|
|
@ -1,47 +1,48 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"dist",
|
"dist",
|
||||||
".next/",
|
".next/",
|
||||||
"favicon-formats.ts",
|
"out/",
|
||||||
"custom.*[js, html]",
|
"favicon-formats.ts",
|
||||||
"hyperglass.json"
|
"custom.*[js, html]",
|
||||||
]
|
"hyperglass.json"
|
||||||
},
|
]
|
||||||
"linter": {
|
},
|
||||||
"enabled": true,
|
"linter": {
|
||||||
"rules": {
|
"enabled": true,
|
||||||
"recommended": true,
|
"rules": {
|
||||||
"complexity": {
|
"recommended": true,
|
||||||
"noUselessTypeConstraint": "off",
|
"complexity": {
|
||||||
"noBannedTypes": "off"
|
"noUselessTypeConstraint": "off",
|
||||||
},
|
"noBannedTypes": "off"
|
||||||
"style": {
|
},
|
||||||
"noInferrableTypes": "off",
|
"style": {
|
||||||
"noNonNullAssertion": "off"
|
"noInferrableTypes": "off",
|
||||||
},
|
"noNonNullAssertion": "off"
|
||||||
"correctness": {
|
},
|
||||||
"useExhaustiveDependencies": "off"
|
"correctness": {
|
||||||
}
|
"useExhaustiveDependencies": "off"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"formatter": {
|
},
|
||||||
"indentStyle": "space",
|
|
||||||
"lineWidth": 100,
|
|
||||||
"indentWidth": 2
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"quoteStyle": "single",
|
"indentStyle": "space",
|
||||||
"bracketSpacing": true,
|
"lineWidth": 100,
|
||||||
"semicolons": "always",
|
"indentWidth": 2
|
||||||
"arrowParentheses": "asNeeded",
|
},
|
||||||
"trailingComma": "all"
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"semicolons": "always",
|
||||||
|
"arrowParentheses": "asNeeded",
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,80 @@
|
||||||
interface Favicon {
|
interface Favicon {
|
||||||
rel: string | null;
|
rel: string | null;
|
||||||
dimensions: [number, number];
|
dimensions: [number, number];
|
||||||
image_format: string;
|
image_format: string;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ dimensions: [64, 64], image_format: 'ico', prefix: 'favicon', rel: null },
|
{ dimensions: [48, 48], image_format: "ico", prefix: "favicon", rel: null },
|
||||||
{ dimensions: [16, 16], image_format: 'png', prefix: 'favicon', rel: 'icon' },
|
{ dimensions: [16, 16], image_format: "png", prefix: "favicon", rel: "icon" },
|
||||||
{ dimensions: [32, 32], 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: [64, 64], image_format: "png", prefix: "favicon", rel: "icon" },
|
||||||
{ dimensions: [96, 96], 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: [180, 180], image_format: "png", prefix: "favicon", rel: "icon" },
|
||||||
{
|
{
|
||||||
dimensions: [57, 57],
|
dimensions: [57, 57],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dimensions: [60, 60],
|
dimensions: [60, 60],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dimensions: [72, 72],
|
dimensions: [72, 72],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dimensions: [76, 76],
|
dimensions: [76, 76],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dimensions: [114, 114],
|
dimensions: [114, 114],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dimensions: [120, 120],
|
dimensions: [120, 120],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dimensions: [144, 144],
|
dimensions: [144, 144],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dimensions: [152, 152],
|
dimensions: [152, 152],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dimensions: [167, 167],
|
dimensions: [167, 167],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dimensions: [180, 180],
|
dimensions: [180, 180],
|
||||||
image_format: 'png',
|
image_format: "png",
|
||||||
prefix: 'apple-touch-icon',
|
prefix: "apple-touch-icon",
|
||||||
rel: 'apple-touch-icon',
|
rel: "apple-touch-icon",
|
||||||
},
|
},
|
||||||
{ dimensions: [70, 70], image_format: 'png', prefix: 'mstile', rel: null },
|
{ dimensions: [70, 70], image_format: "png", prefix: "mstile", rel: null },
|
||||||
{ dimensions: [270, 270], 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, 310], image_format: "png", prefix: "mstile", rel: null },
|
||||||
{ dimensions: [310, 150], 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' },
|
{ dimensions: [196, 196], image_format: "png", prefix: "favicon", rel: "shortcut icon" },
|
||||||
] as Favicon[];
|
] as Favicon[];
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
const fs = require("node:fs");
|
|
||||||
const path = require("node:path");
|
|
||||||
|
|
||||||
function copyChangelog() {
|
|
||||||
const src = path.resolve(__dirname, "..", "CHANGELOG.md");
|
|
||||||
const data = fs.readFileSync(src);
|
|
||||||
const replaced = data.toString().replace("# Changelog\n\n", "");
|
|
||||||
const dst = path.resolve(__dirname, "pages", "changelog.mdx");
|
|
||||||
fs.writeFileSync(dst, replaced);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyChangelog();
|
|
||||||
|
|
||||||
const withNextra = require("nextra")({
|
|
||||||
theme: "nextra-theme-docs",
|
|
||||||
themeConfig: "./theme.config.tsx",
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {import('next').NextConfig}
|
|
||||||
*/
|
|
||||||
const config = {
|
|
||||||
images: {
|
|
||||||
unoptimized: true,
|
|
||||||
},
|
|
||||||
output: "export",
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = withNextra(config);
|
|
||||||
35
docs/next.config.mjs
Normal file
35
docs/next.config.mjs
Normal file
|
|
@ -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);
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
"license": "BSD-3-Clause-Clear",
|
"license": "BSD-3-Clause-Clear",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^14.1.1",
|
"next": "^14.1.1",
|
||||||
"nextra": "^2.13.4",
|
"nextra": "3.0.0-alpha.24",
|
||||||
"nextra-theme-docs": "^2.13.4",
|
"nextra-theme-docs": "3.0.0-alpha.24",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"demo": {
|
|
||||||
"title": "Demo",
|
|
||||||
"type": "page",
|
|
||||||
"href": "https://demo.hyperglass.dev",
|
|
||||||
"newWindow": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
42
docs/pages/_meta.tsx
Normal file
42
docs/pages/_meta.tsx
Normal file
|
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"overview": "Overview",
|
|
||||||
"config": "Config File",
|
|
||||||
"devices": "Devices File",
|
|
||||||
"directives": "Directives File",
|
|
||||||
"examples": "Examples"
|
|
||||||
}
|
|
||||||
7
docs/pages/configuration/_meta.tsx
Normal file
7
docs/pages/configuration/_meta.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
overview: "Overview",
|
||||||
|
config: "Config File",
|
||||||
|
devices: "Devices File",
|
||||||
|
directives: "Directives File",
|
||||||
|
examples: "Examples",
|
||||||
|
};
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"api-docs": "API Docs",
|
|
||||||
"caching": "Caching",
|
|
||||||
"logging": "Logging & Webhooks",
|
|
||||||
"messages": "Messages",
|
|
||||||
"structured-output": "Structured Output",
|
|
||||||
"web-ui": "Web UI"
|
|
||||||
}
|
|
||||||
8
docs/pages/configuration/config/_meta.tsx
Normal file
8
docs/pages/configuration/config/_meta.tsx
Normal file
|
|
@ -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",
|
||||||
|
};
|
||||||
|
|
@ -14,7 +14,7 @@ Additionally, hyperglass provides the ability to control which BGP communities a
|
||||||
|
|
||||||
| Parameter | Type | Default Value | Description |
|
| Parameter | Type | Default Value | Description |
|
||||||
| :----------------------------- | :-------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------- |
|
| :----------------------------- | :-------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `structured.rpki` | 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.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.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. |
|
| `structured.communities.items` | List of Strings | | List of communities to match. |
|
||||||
|
|
||||||
|
|
@ -24,14 +24,16 @@ Additionally, hyperglass provides the ability to control which BGP communities a
|
||||||
|
|
||||||
```yaml filename="config.yaml" copy {2}
|
```yaml filename="config.yaml" copy {2}
|
||||||
structured:
|
structured:
|
||||||
rpki: router
|
rpki:
|
||||||
|
mode: router
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Show RPKI State from a Public/External Perspective
|
#### Show RPKI State from a Public/External Perspective
|
||||||
|
|
||||||
```yaml filename="config.yaml" copy {2}
|
```yaml filename="config.yaml" copy {2}
|
||||||
structured:
|
structured:
|
||||||
rpki: external
|
rpki:
|
||||||
|
mode: external
|
||||||
```
|
```
|
||||||
|
|
||||||
### Community Filtering Examples
|
### Community Filtering Examples
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Callout } from "nextra-theme-docs";
|
import { Callout } from "nextra/components";
|
||||||
import { Color } from "~/components/color";
|
import { Color } from "~/components/color";
|
||||||
|
|
||||||
## Web UI
|
## Web UI
|
||||||
|
|
@ -15,9 +15,10 @@ hyperglass provides extensive customization options for the look and feel of the
|
||||||
|
|
||||||
[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.
|
[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 |
|
| Parameter | Type | Default Value | Description |
|
||||||
| :------------------ | :----- | :------------ | :-------------------------------------- |
|
| :------------------ | :----- | :------------------------------------- | :-------------------------------------------------------------------------- |
|
||||||
| `dns_provider.name` | String | cloudflare | Cloudflare or Google DNS are supported. |
|
| `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
|
### Logo
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Callout } from "nextra-theme-docs";
|
import { Callout } from "nextra/components";
|
||||||
import { SupportedPlatforms } from "~/components/platforms";
|
import { SupportedPlatforms } from "~/components/platforms";
|
||||||
import { DocsButton } from "~/components/docs-button";
|
import { DocsButton } from "~/components/docs-button";
|
||||||
|
|
||||||
|
|
@ -76,7 +76,10 @@ devices:
|
||||||
password: your password
|
password: your password
|
||||||
```
|
```
|
||||||
|
|
||||||
### <DocsButton href="/configuration/directives.mdx"/> With Directives
|
<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.
|
In this example, an additional directive `cisco-show-lldp-neighbors` is added to the built-in directives.
|
||||||
|
|
||||||
|
|
@ -92,7 +95,10 @@ devices:
|
||||||
- cisco-show-lldp-neighbors
|
- cisco-show-lldp-neighbors
|
||||||
```
|
```
|
||||||
|
|
||||||
### <DocsButton href="/configuration/directives.mdx"/> Disable Built-in Directives
|
<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.
|
In this example, _only_ the `cisco-show-lldp-neighbors` directive will be available. Built-in directives are disabled.
|
||||||
|
|
||||||
|
|
@ -109,7 +115,10 @@ devices:
|
||||||
- cisco-show-lldp-neighbors
|
- cisco-show-lldp-neighbors
|
||||||
```
|
```
|
||||||
|
|
||||||
### <DocsButton href="/configuration/directives.mdx"/> Enable Specifc Built-in Directives
|
<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.
|
In this example, only specified built-in directives are made available.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"credentials": "Credentials",
|
|
||||||
"http-device": "HTTP Device",
|
|
||||||
"ssh-proxy": "SSH Proxy"
|
|
||||||
}
|
|
||||||
5
docs/pages/configuration/devices/_meta.tsx
Normal file
5
docs/pages/configuration/devices/_meta.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default {
|
||||||
|
credentials: "Credentials",
|
||||||
|
"http-device": "HTTP Device",
|
||||||
|
"ssh-proxy": "SSH Proxy",
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Callout } from "nextra-theme-docs";
|
import { Callout } from "nextra/components";
|
||||||
|
|
||||||
## What is a directive?
|
## What is a directive?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"basic-configuration": "Basic Configuration",
|
|
||||||
"add-your-own-command": "Add Your Own Command",
|
|
||||||
"customize-the-ui": "Customize the UI"
|
|
||||||
}
|
|
||||||
5
docs/pages/configuration/examples/_meta.tsx
Normal file
5
docs/pages/configuration/examples/_meta.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default {
|
||||||
|
"basic-configuration": "Basic Configuration",
|
||||||
|
"add-your-own-command": "Add Your Own Command",
|
||||||
|
"customize-the-ui": "Customize the UI",
|
||||||
|
};
|
||||||
|
|
@ -3,7 +3,7 @@ title: Basic Configuration
|
||||||
description: Get started with a basic hyperglass configuration
|
description: Get started with a basic hyperglass configuration
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Callout } from "nextra-theme-docs";
|
import { Callout } from "nextra/components";
|
||||||
|
|
||||||
To get started, hyperglass only needs to know about your devices.
|
To get started, hyperglass only needs to know about your devices.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,22 @@
|
||||||
description: Customize hyperglass to fit your needs.
|
description: Customize hyperglass to fit your needs.
|
||||||
---
|
---
|
||||||
|
|
||||||
import { DocsButton } from "../../../components/docs-button";
|
import { DocsButton } from "~/components/docs-button";
|
||||||
|
|
||||||
### <DocsButton href="/configuration/config" /> Change the Title and Organization Name
|
<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"
|
```yaml filename="config.yaml"
|
||||||
site_title: Our super neat looking glass
|
site_title: Our super neat looking glass
|
||||||
org_name: Cool Company
|
org_name: Cool Company
|
||||||
```
|
```
|
||||||
|
|
||||||
### <DocsButton href="/configuration/config/web-ui#logo" /> Change the Logo
|
<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
|
```yaml filename="config.yaml" {2-4} copy
|
||||||
web:
|
web:
|
||||||
|
|
@ -20,7 +26,10 @@ web:
|
||||||
dark: <path to logo image file to use in dark mode>
|
dark: <path to logo image file to use in dark mode>
|
||||||
```
|
```
|
||||||
|
|
||||||
### <DocsButton href="/configuration/config/web-ui#theme" /> Change the Color Scheme
|
<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}
|
```yaml filename="config.yaml" copy {3-5}
|
||||||
web:
|
web:
|
||||||
|
|
@ -30,7 +39,10 @@ web:
|
||||||
secondary: "#118ab2"
|
secondary: "#118ab2"
|
||||||
```
|
```
|
||||||
|
|
||||||
### <DocsButton href="/configuration/config/web-ui#menus" /> Add a Link to the Footer
|
<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
|
```yaml filename="config.yaml" copy
|
||||||
web:
|
web:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Code, Table, Td, Th, Tr } from "nextra/components";
|
import { Code, Table, Td, Th, Tr, Callout } from "nextra/components";
|
||||||
import { Callout } from "nextra-theme-docs";
|
|
||||||
import { SupportedPlatforms } from "~/components/platforms";
|
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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@ title: Introduction
|
||||||
description: Get started with hyperglass
|
description: Get started with hyperglass
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Cards, Card } from "nextra/components";
|
import { Cards } from "nextra/components";
|
||||||
import { SupportedPlatforms } from "~/components/platforms";
|
import { SupportedPlatforms } from "~/components/platforms";
|
||||||
|
|
||||||
## What is hyperglass?
|
## 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.
|
<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.
|
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.
|
||||||
|
|
||||||
|
|
@ -29,5 +31,5 @@ hyperglass was created with the lofty goal of benefiting the internet community
|
||||||
- Browser-based DNS-over-HTTPS resolution of FQDN queries
|
- Browser-based DNS-over-HTTPS resolution of FQDN queries
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
<Card title="Get Started" href="installation/docker" arrow />
|
<Cards.Card title="Get Started" href="installation/docker" arrow />
|
||||||
</Cards>
|
</Cards>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Cards, Card } from "nextra/components";
|
import { Cards } from "nextra/components";
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
<Card href="installation/docker" title="Using Docker" />
|
<Cards.Card href="installation/docker" title="Using Docker" />
|
||||||
<Card href="installation/manual" title="Manual Installation" />
|
<Cards.Card href="installation/manual" title="Manual Installation" />
|
||||||
</Cards>
|
</Cards>
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
<Card href="installation/environment-variables" title="Environment Variables" />
|
<Cards.Card href="installation/environment-variables" title="Environment Variables" />
|
||||||
<Card href="installation/reverse-proxy" title="Reverse Proxy" />
|
<Cards.Card href="installation/reverse-proxy" title="Reverse Proxy" />
|
||||||
</Cards>
|
</Cards>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"docker": "Using Docker",
|
|
||||||
"manual": "Manual Installation",
|
|
||||||
"environment-variables": "Environment Variables",
|
|
||||||
"reverse-proxy": "Reverse Proxy",
|
|
||||||
"upgrading": "Upgrading hyperglass"
|
|
||||||
}
|
|
||||||
7
docs/pages/installation/_meta.tsx
Normal file
7
docs/pages/installation/_meta.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
docker: "Using Docker",
|
||||||
|
manual: "Manual Installation",
|
||||||
|
"environment-variables": "Environment Variables",
|
||||||
|
"reverse-proxy": "Reverse Proxy",
|
||||||
|
upgrading: "Upgrading hyperglass",
|
||||||
|
};
|
||||||
|
|
@ -3,8 +3,8 @@ title: Using Docker
|
||||||
description: Installing hyperglass with Docker
|
description: Installing hyperglass with Docker
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Card, Cards, Steps } from "nextra/components";
|
import { Cards, Steps, Callout } from "nextra/components";
|
||||||
import { Callout } from "nextra-theme-docs";
|
// import { Callout } from "nextra-theme-docs";
|
||||||
|
|
||||||
<Callout type="info">**Docker is the recommended method for running hyperglass.**</Callout>
|
<Callout type="info">**Docker is the recommended method for running hyperglass.**</Callout>
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { Callout } from "nextra-theme-docs";
|
||||||
### Install Docker
|
### Install Docker
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
<Card
|
<Cards.Card
|
||||||
title="Docker Engine Installation Guide"
|
title="Docker Engine Installation Guide"
|
||||||
href="https://docs.docker.com/engine/install/"
|
href="https://docs.docker.com/engine/install/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
@ -26,9 +26,8 @@ import { Callout } from "nextra-theme-docs";
|
||||||
```shell copy
|
```shell copy
|
||||||
mkdir /etc/hyperglass
|
mkdir /etc/hyperglass
|
||||||
cd /opt
|
cd /opt
|
||||||
git clone https://github.com/thatmattlove/hyperglass.git
|
git clone https://github.com/thatmattlove/hyperglass.git --depth=1
|
||||||
cd /opt/hyperglass
|
cd /opt/hyperglass
|
||||||
git checkout v2.0.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional: Quickstart
|
### Optional: Quickstart
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ title: Manual Installation
|
||||||
description: Installing hyperglass manually
|
description: Installing hyperglass manually
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Steps } from "nextra/components";
|
import { Steps, Callout } from "nextra/components";
|
||||||
import { Callout } from "nextra-theme-docs";
|
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
|
|
||||||
|
|
@ -12,8 +11,8 @@ import { Callout } from "nextra-theme-docs";
|
||||||
|
|
||||||
To install hyperglass manually, you'll need to install the following dependencies:
|
To install hyperglass manually, you'll need to install the following dependencies:
|
||||||
|
|
||||||
1. [Python 3.9, 3.10, 3.11, or 3.12](https://www.python.org/downloads/) and [`pip`](https://pip.pypa.io/en/stable/installation/)
|
1. [Python 3.11, or 3.12](https://www.python.org/downloads/) and [`pip`](https://pip.pypa.io/en/stable/installation/)
|
||||||
2. [NodeJS 18.17 or later](https://nodejs.org/en/download)
|
2. [NodeJS 20.14 or later](https://nodejs.org/en/download)
|
||||||
3. [PNPM 8 or later](https://pnpm.io/installation)
|
3. [PNPM 8 or later](https://pnpm.io/installation)
|
||||||
4. [Redis 7.2 or later](https://redis.io/download/)
|
4. [Redis 7.2 or later](https://redis.io/download/)
|
||||||
|
|
||||||
|
|
@ -24,7 +23,9 @@ To install hyperglass manually, you'll need to install the following dependencie
|
||||||
Once these dependencies are installed, install hyperglass via PyPI:
|
Once these dependencies are installed, install hyperglass via PyPI:
|
||||||
|
|
||||||
```shell copy
|
```shell copy
|
||||||
pip3 install hyperglass
|
git clone https://github.com/thatmattlove/hyperglass --depth=1
|
||||||
|
cd hyperglass
|
||||||
|
pip3 install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create app directory
|
### Create app directory
|
||||||
|
|
@ -43,14 +44,14 @@ mkdir /etc/hyperglass
|
||||||
Do this if you just want to see the hyperglass page working with default settings and a fake device.
|
Do this if you just want to see the hyperglass page working with default settings and a fake device.
|
||||||
|
|
||||||
```shell copy
|
```shell copy
|
||||||
curl -o /etc/hyperglass/devices.yaml https://raw.githubusercontent.com/thatmattlove/hyperglass/v2.0.0/.samples/sample_devices.yaml
|
curl -o /etc/hyperglass/devices.yaml https://raw.githubusercontent.com/thatmattlove/hyperglass/main/.samples/sample_devices.yaml
|
||||||
hyperglass start
|
hyperglass start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create a `systemd` service
|
### Create a `systemd` service
|
||||||
|
|
||||||
```shell copy
|
```shell copy
|
||||||
curl -o /etc/hyperglass/hyperglass.service https://raw.githubusercontent.com/thatmattlove/hyperglass/v2.0.0/.samples/hyperglass-manual.service
|
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
|
ln -s /etc/hyperglass/hyperglass.service /etc/systemd/system/hyperglass.service
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable hyperglass
|
systemctl enable hyperglass
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,19 @@ title: Reverse Proxy
|
||||||
description: Setting up a reverse proxy for hyperglass
|
description: Setting up a reverse proxy for hyperglass
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Cards, Card } from "nextra/components";
|
import { Cards, Callout } from "nextra/components";
|
||||||
import { Callout } from "nextra-theme-docs";
|
|
||||||
|
|
||||||
[Caddy](https://caddyserver.com) is recommended, but any reverse proxy ([NGINX](https://www.nginx.com), [Apache2](https://httpd.apache.org)) will work.
|
[Caddy](https://caddyserver.com) is recommended, but any reverse proxy ([NGINX](https://www.nginx.com), [Apache2](https://httpd.apache.org)) will work.
|
||||||
|
|
||||||
## Caddy
|
## Caddy
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
<Card title="Install Caddy" target="_blank" href="https://caddyserver.com/docs/install" arrow />
|
<Cards.Card
|
||||||
|
title="Install Caddy"
|
||||||
|
target="_blank"
|
||||||
|
href="https://caddyserver.com/docs/install"
|
||||||
|
arrow
|
||||||
|
/>
|
||||||
</Cards>
|
</Cards>
|
||||||
|
|
||||||
```shell copy
|
```shell copy
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
cd /opt/hyperglass
|
cd /opt/hyperglass
|
||||||
docker compose down
|
docker compose down
|
||||||
docker compose rm -f
|
docker compose rm -f
|
||||||
git pull
|
git fetch
|
||||||
|
git checkout v2.0.4
|
||||||
docker compose build
|
docker compose build
|
||||||
docker compose up
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
|
||||||
20
docs/pages/license.mdx
Normal file
20
docs/pages/license.mdx
Normal file
|
|
@ -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.
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
description: Platforms supported by hyperglass
|
description: Platforms supported by hyperglass
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Callout } from "nextra-theme-docs";
|
import { Callout } from "nextra/components";
|
||||||
import { PlatformTable } from "~/components/platforms";
|
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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ class Redact(OutputPlugin):
|
||||||
|
|
||||||
If the query output was:
|
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.
|
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
|
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
|
scelerisque eu ultrices vitae. Accumsan SuperSecretInfo tortor posuere ac ut consequat semper viverra nam libero. Libero id faucibus nisl
|
||||||
|
|
@ -133,7 +133,7 @@ diam in arcu cursus SuperSecretInfo.
|
||||||
|
|
||||||
The above plugin would transform the output to:
|
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.
|
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
|
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
|
scelerisque eu ultrices vitae. Accumsan <REDACTED> tortor posuere ac ut consequat semper viverra nam libero. Libero id faucibus nisl
|
||||||
|
|
|
||||||
4130
docs/pnpm-lock.yaml
generated
4130
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type DocsThemeConfig, useConfig } from "nextra-theme-docs";
|
import { type DocsThemeConfig, useConfig } from "nextra-theme-docs";
|
||||||
|
import "nextra-theme-docs/style.css";
|
||||||
import faviconFormats from "./favicon-formats";
|
import faviconFormats from "./favicon-formats";
|
||||||
import styles from "./global.module.css";
|
import styles from "./global.module.css";
|
||||||
|
|
||||||
|
|
@ -71,49 +72,54 @@ const config: DocsThemeConfig = {
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
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}` };
|
||||||
|
});
|
||||||
|
|
||||||
useNextSeoProps: () => {
|
return (
|
||||||
const { asPath } = useRouter();
|
<head>
|
||||||
const { frontMatter, title } = useConfig();
|
<title>{title}</title>
|
||||||
return {
|
<meta property="og:url" content={url} />
|
||||||
titleTemplate: "%s | hyperglass",
|
<meta property="og:title" content={title} />
|
||||||
title: frontMatter.title || title,
|
<meta property="og:description" content={description} />
|
||||||
openGraph: {
|
<meta property="og:type" content="website" />
|
||||||
type: "website",
|
<meta property="og:image" content="https://hyperglass.dev/opengraph.jpg" />
|
||||||
url: `https://hyperglass.dev${asPath}`,
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
title: frontMatter.title || title,
|
<meta name="twitter:domain" content="hyperglass.dev" />
|
||||||
description: frontMatter.description || "hyperglass Documentation",
|
<meta name="twitter:url" content="https://hyperglass.dev" />
|
||||||
images: [
|
<meta name="twitter:title" content={title} />
|
||||||
{
|
<meta name="twitter:description" content={description} />
|
||||||
url: "https://hyperglass.dev/opengraph.jpg",
|
<meta name="twitter:image" content="https://hyperglass.dev/opengraph.jpg" />
|
||||||
width: 1200,
|
<meta name="robots" content={index} />
|
||||||
height: 630,
|
<link rel="manifest" href="/img/manifest.json" />
|
||||||
alt: "hyperglass",
|
{favicons.map((props) => (
|
||||||
},
|
<link key={props.href} {...props} />
|
||||||
],
|
))}
|
||||||
},
|
</head>
|
||||||
twitter: {
|
);
|
||||||
handle: "@thatmattlove",
|
|
||||||
site: "@thatmattlove",
|
|
||||||
cardType: "summary_large_image",
|
|
||||||
},
|
|
||||||
noindex: NO_INDEX_FOLLOW,
|
|
||||||
nofollow: NO_INDEX_FOLLOW,
|
|
||||||
additionalLinkTags: 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}` };
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
docsRepositoryBase: "https://github.com/thatmattlove/hyperglass/tree/main/docs",
|
||||||
banner: {
|
banner: {
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
// text: "🎉 hyperglass 2.0 is here!",
|
content: "🎉 hyperglass 2.0 is here! This documentation is still in development, though.",
|
||||||
text: "😬 hyperglass 2.0 and its documentation is still in development!",
|
|
||||||
},
|
},
|
||||||
feedback: { content: null },
|
feedback: { content: null },
|
||||||
footer: { text: `© ${new Date().getFullYear()} hyperglass` },
|
footer: { content: `© ${new Date().getFullYear()} hyperglass` },
|
||||||
editLink: { component: null },
|
editLink: { component: null },
|
||||||
chat: {
|
chat: {
|
||||||
link: "https://netdev.chat/",
|
link: "https://netdev.chat/",
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./*"]
|
"~/*": ["./*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"exclude": ["node_modules", ".next"],
|
"exclude": ["node_modules", ".next"],
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js"]
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.mjs"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,13 +158,6 @@ BGP_ROUTES = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
STRUCTURED = BGPRouteTable(
|
|
||||||
vrf="default",
|
|
||||||
count=len(BGP_ROUTES),
|
|
||||||
routes=BGP_ROUTES,
|
|
||||||
winning_weight="high",
|
|
||||||
)
|
|
||||||
|
|
||||||
PING = r"""PING 1.1.1.1 (1.1.1.1): 56 data bytes
|
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=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=1 ttl=59 time=4.699 ms
|
||||||
|
|
@ -196,6 +189,11 @@ async def fake_output(query_type: str, structured: bool) -> t.Union[str, BGPRout
|
||||||
return TRACEROUTE
|
return TRACEROUTE
|
||||||
if "bgp" in query_type:
|
if "bgp" in query_type:
|
||||||
if structured:
|
if structured:
|
||||||
return STRUCTURED
|
return BGPRouteTable(
|
||||||
|
vrf="default",
|
||||||
|
count=len(BGP_ROUTES),
|
||||||
|
routes=BGP_ROUTES,
|
||||||
|
winning_weight="high",
|
||||||
|
)
|
||||||
return BGP_PLAIN
|
return BGP_PLAIN
|
||||||
return BGP_PLAIN
|
return BGP_PLAIN
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""API Routes."""
|
"""API Routes."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
import typing as t
|
import typing as t
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
@ -119,7 +120,10 @@ async def query(_state: HyperglassState, request: Request, data: Query) -> Query
|
||||||
json_output = is_type(output, OutputDataModel)
|
json_output = is_type(output, OutputDataModel)
|
||||||
|
|
||||||
if json_output:
|
if json_output:
|
||||||
raw_output = output.export_dict()
|
# 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:
|
else:
|
||||||
raw_output = str(output)
|
raw_output = str(output)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ def init_ui_params(*, params: "Params", devices: "Devices") -> "UIParameters":
|
||||||
_ui_params = params.frontend()
|
_ui_params = params.frontend()
|
||||||
_ui_params["web"]["logo"]["light_format"] = params.web.logo.light.suffix
|
_ui_params["web"]["logo"]["light_format"] = params.web.logo.light.suffix
|
||||||
_ui_params["web"]["logo"]["dark_format"] = params.web.logo.dark.suffix
|
_ui_params["web"]["logo"]["dark_format"] = params.web.logo.dark.suffix
|
||||||
|
_ui_params["web"]["logo"]["footer_format"] = params.web.logo.footer.suffix
|
||||||
|
|
||||||
return UIParameters(
|
return UIParameters(
|
||||||
**_ui_params,
|
**_ui_params,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
__name__ = "hyperglass"
|
__name__ = "hyperglass"
|
||||||
__version__ = "2.0.0"
|
__version__ = "2.0.4"
|
||||||
__author__ = "Matt Love"
|
__author__ = "Matt Love"
|
||||||
__copyright__ = f"Copyright {datetime.now().year} Matthew Love"
|
__copyright__ = f"Copyright {datetime.now().year} Matthew Love"
|
||||||
__license__ = "BSD 3-Clause Clear License"
|
__license__ = "BSD 3-Clause Clear License"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
CREDIT = """
|
CREDIT = """
|
||||||
Powered by [**hyperglass**](https://hyperglass.dev) version {version}. \
|
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 = """
|
DEFAULT_TERMS = """
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ JuniperTraceroute = BuiltinDirective(
|
||||||
RuleWithIPv6(
|
RuleWithIPv6(
|
||||||
condition="::/0",
|
condition="::/0",
|
||||||
action="permit",
|
action="permit",
|
||||||
command="traceroute inet6 {target} wait 1 source {source6}",
|
command="traceroute inet6 {target} wait 2 source {source6}",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
field=Text(description="IP Address, Prefix, or Hostname"),
|
field=Text(description="IP Address, Prefix, or Hostname"),
|
||||||
|
|
|
||||||
76
hyperglass/defaults/directives/sixwind_os.py
Normal file
76
hyperglass/defaults/directives/sixwind_os.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
"""Default 6WIND Directives."""
|
||||||
|
|
||||||
|
# Project
|
||||||
|
from hyperglass.models.directive import (
|
||||||
|
Text,
|
||||||
|
RuleWithIPv4,
|
||||||
|
RuleWithIPv6,
|
||||||
|
RuleWithPattern,
|
||||||
|
BuiltinDirective,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"SixWind_BGPRoute",
|
||||||
|
"SixWind_Ping",
|
||||||
|
"SixWind_Traceroute",
|
||||||
|
)
|
||||||
|
|
||||||
|
NAME = "6WIND"
|
||||||
|
PLATFORMS = ["sixwind_os"]
|
||||||
|
|
||||||
|
SixWind_BGPRoute = BuiltinDirective(
|
||||||
|
id="__hyperglass_sixwind_os_bgp_route__",
|
||||||
|
name="BGP Route",
|
||||||
|
rules=[
|
||||||
|
RuleWithIPv4(
|
||||||
|
condition="0.0.0.0/0",
|
||||||
|
action="permit",
|
||||||
|
command="show bgp ipv4 unicast prefix {target}",
|
||||||
|
),
|
||||||
|
RuleWithIPv6(
|
||||||
|
condition="::/0",
|
||||||
|
action="permit",
|
||||||
|
command="show bgp ipv6 unicast prefix {target}",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
field=Text(description="IP Address, Prefix, or Hostname"),
|
||||||
|
platforms=PLATFORMS,
|
||||||
|
)
|
||||||
|
|
||||||
|
SixWind_Ping = BuiltinDirective(
|
||||||
|
id="__hyperglass_sixwind_os_ping__",
|
||||||
|
name="Ping",
|
||||||
|
rules=[
|
||||||
|
RuleWithIPv4(
|
||||||
|
condition="0.0.0.0/0",
|
||||||
|
action="permit",
|
||||||
|
command="cmd ping count 8 packetsize 256 source {source4} {target}",
|
||||||
|
),
|
||||||
|
RuleWithIPv6(
|
||||||
|
condition="::/0",
|
||||||
|
action="permit",
|
||||||
|
command="cmd ping count 8 packetsize 256 ipv6 source {source6} {target}",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
field=Text(description="IP Address, Prefix, or Hostname"),
|
||||||
|
platforms=PLATFORMS,
|
||||||
|
)
|
||||||
|
|
||||||
|
SixWind_Traceroute = BuiltinDirective(
|
||||||
|
id="__hyperglass_sixwind_os_traceroute__",
|
||||||
|
name="Traceroute",
|
||||||
|
rules=[
|
||||||
|
RuleWithIPv4(
|
||||||
|
condition="0.0.0.0/0",
|
||||||
|
action="permit",
|
||||||
|
command="cmd traceroute source {source4} {target}",
|
||||||
|
),
|
||||||
|
RuleWithIPv6(
|
||||||
|
condition="::/0",
|
||||||
|
action="permit",
|
||||||
|
command="cmd traceroute ipv6 source {source6} {target}",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
field=Text(description="IP Address, Prefix, or Hostname"),
|
||||||
|
platforms=PLATFORMS,
|
||||||
|
)
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import json as _json
|
import json as _json
|
||||||
from typing import Any, Dict, List, Union, Literal, Optional
|
from typing import Any, Dict, List, Union, Literal, Optional, Set
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
@ -48,7 +48,7 @@ class HyperglassError(Exception):
|
||||||
return {
|
return {
|
||||||
"message": self._message,
|
"message": self._message,
|
||||||
"level": self._level,
|
"level": self._level,
|
||||||
"keywords": self._keywords,
|
"keywords": self.keywords,
|
||||||
}
|
}
|
||||||
|
|
||||||
def json(self) -> str:
|
def json(self) -> str:
|
||||||
|
|
@ -76,6 +76,18 @@ class HyperglassError(Exception):
|
||||||
|
|
||||||
return "\n".join(errs)
|
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
|
@property
|
||||||
def message(self) -> str:
|
def message(self) -> str:
|
||||||
"""Return the instance's `message` attribute."""
|
"""Return the instance's `message` attribute."""
|
||||||
|
|
@ -89,6 +101,7 @@ class HyperglassError(Exception):
|
||||||
@property
|
@property
|
||||||
def keywords(self) -> List[str]:
|
def keywords(self) -> List[str]:
|
||||||
"""Return the instance's `keywords` attribute."""
|
"""Return the instance's `keywords` attribute."""
|
||||||
|
self._process_keywords()
|
||||||
return self._keywords
|
return self._keywords
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,7 @@ netmiko_device_globals = {
|
||||||
"mikrotik_switchos": {"global_cmd_verify": False},
|
"mikrotik_switchos": {"global_cmd_verify": False},
|
||||||
}
|
}
|
||||||
|
|
||||||
netmiko_device_send_args = {
|
netmiko_device_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$"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class NetmikoConnection(SSHConnection):
|
class NetmikoConnection(SSHConnection):
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ async def execute(query: "Query") -> Union["OutputDataModel", str]:
|
||||||
"""Initiate query validation and execution."""
|
"""Initiate query validation and execution."""
|
||||||
params = use_state("params")
|
params = use_state("params")
|
||||||
output = params.messages.general
|
output = params.messages.general
|
||||||
_log = log.bind(query=query, device=query.device)
|
_log = log.bind(query=query.summary(), device=query.device.id)
|
||||||
_log.debug("")
|
_log.debug("")
|
||||||
|
|
||||||
mapped_driver = map_driver(query.device.driver)
|
mapped_driver = map_driver(query.device.driver)
|
||||||
|
|
@ -67,7 +67,6 @@ async def execute(query: "Query") -> Union["OutputDataModel", str]:
|
||||||
response = await driver.collect()
|
response = await driver.collect()
|
||||||
|
|
||||||
output = await driver.response(response)
|
output = await driver.response(response)
|
||||||
_log.bind(response=response).debug("Query response")
|
|
||||||
|
|
||||||
if is_series(output):
|
if is_series(output):
|
||||||
if len(output) == 0:
|
if len(output) == 0:
|
||||||
|
|
|
||||||
1
hyperglass/external/_base.py
vendored
1
hyperglass/external/_base.py
vendored
|
|
@ -252,7 +252,6 @@ class BaseExternal:
|
||||||
except TypeError as err:
|
except TypeError as err:
|
||||||
raise self._exception(f"Timeout must be an int, got: {str(timeout)}") from err
|
raise self._exception(f"Timeout must be an int, got: {str(timeout)}") from err
|
||||||
request["timeout"] = timeout
|
request["timeout"] = timeout
|
||||||
log.bind(request=request).debug("Constructed request parameters")
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
async def _arequest( # noqa: C901
|
async def _arequest( # noqa: C901
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,6 @@ async def read_package_json() -> t.Dict[str, t.Any]:
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise RuntimeError(f"Error reading package.json: {str(err)}") from err
|
raise RuntimeError(f"Error reading package.json: {str(err)}") from err
|
||||||
|
|
||||||
log.bind(package_json=package_json).debug("package.json value")
|
|
||||||
|
|
||||||
return package_json
|
return package_json
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -131,7 +129,9 @@ async def build_ui(app_path: Path):
|
||||||
log.error(err)
|
log.error(err)
|
||||||
raise RuntimeError(str(err)) from err
|
raise RuntimeError(str(err)) from err
|
||||||
|
|
||||||
shutil.copytree(src=out_dir, dst=build_dir, dirs_exist_ok=True)
|
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")
|
log.bind(src=out_dir, dst=build_dir).debug("Migrated Next.JS build output")
|
||||||
|
|
||||||
return "\n".join(all_messages)
|
return "\n".join(all_messages)
|
||||||
|
|
@ -203,7 +203,7 @@ def migrate_images(app_path: Path, params: "UIParameters"):
|
||||||
src_files = ()
|
src_files = ()
|
||||||
dst_files = ()
|
dst_files = ()
|
||||||
|
|
||||||
for image in ("light", "dark", "favicon"):
|
for image in ("light", "dark", "favicon", "footer"):
|
||||||
src: Path = getattr(params.web.logo, image)
|
src: Path = getattr(params.web.logo, image)
|
||||||
dst = images_dir / f"{image + src.suffix}"
|
dst = images_dir / f"{image + src.suffix}"
|
||||||
src_files += (src,)
|
src_files += (src,)
|
||||||
|
|
@ -326,7 +326,6 @@ async def build_frontend( # noqa: C901
|
||||||
}
|
}
|
||||||
|
|
||||||
build_json = json.dumps(build_data, default=str)
|
build_json = json.dumps(build_data, default=str)
|
||||||
log.bind(data=build_json).debug("UI Build Data")
|
|
||||||
|
|
||||||
# Create SHA256 hash from all parameters passed to UI, use as
|
# Create SHA256 hash from all parameters passed to UI, use as
|
||||||
# build identifier.
|
# build identifier.
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,12 @@ _LOG_LEVELS = [
|
||||||
{"name": "CRITICAL", "color": "<r>"},
|
{"name": "CRITICAL", "color": "<r>"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_EXCLUDE_MODULES = (
|
||||||
|
"PIL",
|
||||||
|
"svglib",
|
||||||
|
"paramiko.transport",
|
||||||
|
)
|
||||||
|
|
||||||
HyperglassConsole = Console(
|
HyperglassConsole = Console(
|
||||||
theme=Theme(
|
theme=Theme(
|
||||||
{
|
{
|
||||||
|
|
@ -119,6 +125,9 @@ class LibInterceptHandler(logging.Handler):
|
||||||
def init_logger(level: t.Union[int, str] = logging.INFO):
|
def init_logger(level: t.Union[int, str] = logging.INFO):
|
||||||
"""Initialize hyperglass logging instance."""
|
"""Initialize hyperglass logging instance."""
|
||||||
|
|
||||||
|
for mod in _EXCLUDE_MODULES:
|
||||||
|
logging.getLogger(mod).propagate = False
|
||||||
|
|
||||||
# Reset built-in Loguru configurations.
|
# Reset built-in Loguru configurations.
|
||||||
_loguru_logger.remove()
|
_loguru_logger.remove()
|
||||||
|
|
||||||
|
|
@ -133,6 +142,7 @@ def init_logger(level: t.Union[int, str] = logging.INFO):
|
||||||
log_time_format="[%Y%m%d %H:%M:%S]",
|
log_time_format="[%Y%m%d %H:%M:%S]",
|
||||||
),
|
),
|
||||||
format=formatter,
|
format=formatter,
|
||||||
|
colorize=False,
|
||||||
level=level,
|
level=level,
|
||||||
filter=filter_uvicorn_values,
|
filter=filter_uvicorn_values,
|
||||||
enqueue=True,
|
enqueue=True,
|
||||||
|
|
@ -144,6 +154,7 @@ def init_logger(level: t.Union[int, str] = logging.INFO):
|
||||||
enqueue=True,
|
enqueue=True,
|
||||||
format=_FMT if level == logging.INFO else _FMT_DEBUG,
|
format=_FMT if level == logging.INFO else _FMT_DEBUG,
|
||||||
level=level,
|
level=level,
|
||||||
|
colorize=False,
|
||||||
filter=filter_uvicorn_values,
|
filter=filter_uvicorn_values,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -192,6 +203,7 @@ def enable_file_logging(
|
||||||
serialize=structured,
|
serialize=structured,
|
||||||
level=level,
|
level=level,
|
||||||
encoding="utf8",
|
encoding="utf8",
|
||||||
|
colorize=False,
|
||||||
rotation=max_size.human_readable(),
|
rotation=max_size.human_readable(),
|
||||||
)
|
)
|
||||||
_loguru_logger.bind(path=log_file).debug("Logging to file")
|
_loguru_logger.bind(path=log_file).debug("Logging to file")
|
||||||
|
|
@ -207,5 +219,6 @@ def enable_syslog_logging(*, host: str, port: int) -> None:
|
||||||
SysLogHandler(address=(str(host), port)),
|
SysLogHandler(address=(str(host), port)),
|
||||||
format=_FMT_BASIC,
|
format=_FMT_BASIC,
|
||||||
enqueue=True,
|
enqueue=True,
|
||||||
|
colorize=False,
|
||||||
)
|
)
|
||||||
_loguru_logger.bind(host=host, port=port).debug("Logging to syslog target")
|
_loguru_logger.bind(host=host, port=port).debug("Logging to syslog target")
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,9 @@ def run(workers: int = None):
|
||||||
log.bind(
|
log.bind(
|
||||||
version=__version__,
|
version=__version__,
|
||||||
listening=f"http://{Settings.bind()}",
|
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,
|
workers=_workers,
|
||||||
).info(
|
).info(
|
||||||
"Starting hyperglass",
|
"Starting hyperglass",
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,12 @@ class ParamsPublic(HyperglassModel):
|
||||||
description="Your network's primary ASN. This field is used to set some useful defaults such as the subtitle and PeeringDB URL.",
|
description="Your network's primary ASN. This field is used to set some useful defaults such as the subtitle and PeeringDB URL.",
|
||||||
)
|
)
|
||||||
org_name: str = Field(
|
org_name: str = Field(
|
||||||
"Beloved Hyperglass User",
|
"Beloved Looking Glass User",
|
||||||
title="Organization Name",
|
title="Organization Name",
|
||||||
description="Your organization's name. This field is used in the UI & API documentation to set fields such as `<meta/>` HTML tags for SEO and the terms & conditions footer component.",
|
description="Your organization's name. This field is used in the UI & API documentation to set fields such as `<meta/>` HTML tags for SEO and the terms & conditions footer component.",
|
||||||
)
|
)
|
||||||
site_title: str = Field(
|
site_title: str = Field(
|
||||||
"hyperglass",
|
"Looking Glass",
|
||||||
title="Site Title",
|
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 `<title/>` HTML tag, and the terms & conditions footer component.",
|
description="The name of your hyperglass site. This field is used in the UI & API documentation to set fields such as the `<title/>` HTML tag, and the terms & conditions footer component.",
|
||||||
)
|
)
|
||||||
|
|
@ -131,8 +131,8 @@ class Params(ParamsPublic, HyperglassModel):
|
||||||
|
|
||||||
for menu in web.menus:
|
for menu in web.menus:
|
||||||
menu.content = menu.content.format(
|
menu.content = menu.content.format(
|
||||||
site_title=info.data.get("site_title", "hyperglass"),
|
site_title=info.data.get("site_title", "Looking Glass"),
|
||||||
org_name=info.data.get("org_name", "hyperglass"),
|
org_name=info.data.get("org_name", "Looking Glass"),
|
||||||
version=__version__,
|
version=__version__,
|
||||||
)
|
)
|
||||||
return web
|
return web
|
||||||
|
|
|
||||||
|
|
@ -86,10 +86,9 @@ class Logo(HyperglassModel):
|
||||||
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
|
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
|
||||||
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
|
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
|
||||||
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
|
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
|
||||||
width: str = Field(default="100%", pattern=PERCENTAGE_PATTERN)
|
footer: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
|
||||||
# width: t.Optional[t.Union[int, Percentage]] = "100%"
|
width: str = Field(default="50%", pattern=PERCENTAGE_PATTERN)
|
||||||
height: t.Optional[str] = Field(default=None, pattern=PERCENTAGE_PATTERN)
|
height: t.Optional[str] = Field(default=None, pattern=PERCENTAGE_PATTERN)
|
||||||
# height: t.Optional[t.Union[int, Percentage]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class LogoPublic(Logo):
|
class LogoPublic(Logo):
|
||||||
|
|
@ -97,13 +96,13 @@ class LogoPublic(Logo):
|
||||||
|
|
||||||
light_format: str
|
light_format: str
|
||||||
dark_format: str
|
dark_format: str
|
||||||
|
footer_format: str
|
||||||
|
|
||||||
class Text(HyperglassModel):
|
class Text(HyperglassModel):
|
||||||
"""Validation model for params.branding.text."""
|
"""Validation model for params.branding.text."""
|
||||||
|
|
||||||
title_mode: TitleMode = "logo_only"
|
title_mode: TitleMode = "logo_only"
|
||||||
title: str = Field(default="hyperglass", max_length=32)
|
title: str = Field(default="Looking Glass", max_length=32)
|
||||||
subtitle: str = Field(default="Network Looking Glass", max_length=32)
|
subtitle: str = Field(default="Network Looking Glass", max_length=32)
|
||||||
query_location: str = "Location"
|
query_location: str = "Location"
|
||||||
query_type: str = "Query Type"
|
query_type: str = "Query Type"
|
||||||
|
|
@ -134,7 +133,7 @@ class Text(HyperglassModel):
|
||||||
class ThemeColors(HyperglassModel):
|
class ThemeColors(HyperglassModel):
|
||||||
"""Validation model for theme colors."""
|
"""Validation model for theme colors."""
|
||||||
|
|
||||||
black: Color = "#000000"
|
black: Color = "#2d3635"
|
||||||
white: Color = "#ffffff"
|
white: Color = "#ffffff"
|
||||||
dark: Color = "#010101"
|
dark: Color = "#010101"
|
||||||
light: Color = "#f5f6f7"
|
light: Color = "#f5f6f7"
|
||||||
|
|
@ -171,7 +170,7 @@ class ThemeColors(HyperglassModel):
|
||||||
class ThemeFonts(HyperglassModel):
|
class ThemeFonts(HyperglassModel):
|
||||||
"""Validation model for theme fonts."""
|
"""Validation model for theme fonts."""
|
||||||
|
|
||||||
body: str = "Nunito"
|
body: str = "Inter"
|
||||||
mono: str = "Fira Code"
|
mono: str = "Fira Code"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -186,13 +185,19 @@ class Theme(HyperglassModel):
|
||||||
class DnsOverHttps(HyperglassModel):
|
class DnsOverHttps(HyperglassModel):
|
||||||
"""Validation model for DNS over HTTPS resolution."""
|
"""Validation model for DNS over HTTPS resolution."""
|
||||||
|
|
||||||
name: str = Field(default="cloudflare", pattern=DOH_PROVIDERS_PATTERN)
|
name: str = "cloudflare"
|
||||||
url: str = ""
|
url: str = ""
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
def validate_dns(cls, data: "DnsOverHttps") -> t.Dict[str, str]:
|
def validate_dns(cls, data: "DnsOverHttps") -> t.Dict[str, str]:
|
||||||
"""Assign url field to model based on selected provider."""
|
"""Assign url field to model based on selected provider."""
|
||||||
name = data.get("name", "cloudflare")
|
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]
|
url = DNS_OVER_HTTPS[name]
|
||||||
return {
|
return {
|
||||||
"name": name,
|
"name": name,
|
||||||
|
|
@ -222,6 +227,7 @@ class HighlightPattern(HyperglassModel):
|
||||||
class Web(HyperglassModel):
|
class Web(HyperglassModel):
|
||||||
"""Validation model for all web/browser-related configuration."""
|
"""Validation model for all web/browser-related configuration."""
|
||||||
|
|
||||||
|
copyright: t.Optional[str] = "All rights reserved"
|
||||||
credit: Credit = Credit()
|
credit: Credit = Credit()
|
||||||
dns_provider: DnsOverHttps = DnsOverHttps()
|
dns_provider: DnsOverHttps = DnsOverHttps()
|
||||||
links: t.Sequence[Link] = [
|
links: t.Sequence[Link] = [
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import typing as t
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import field_validator
|
from pydantic import field_validator, ValidationInfo
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.state import use_state
|
from hyperglass.state import use_state
|
||||||
|
|
@ -70,7 +70,7 @@ class BGPRoute(HyperglassModel):
|
||||||
return [c for c in value if func(c)]
|
return [c for c in value if func(c)]
|
||||||
|
|
||||||
@field_validator("rpki_state")
|
@field_validator("rpki_state")
|
||||||
def validate_rpki_state(cls, value, values):
|
def validate_rpki_state(cls, value, info: ValidationInfo):
|
||||||
"""If external RPKI validation is enabled, get validation state."""
|
"""If external RPKI validation is enabled, get validation state."""
|
||||||
|
|
||||||
(structured := use_state("params").structured)
|
(structured := use_state("params").structured)
|
||||||
|
|
@ -82,7 +82,7 @@ class BGPRoute(HyperglassModel):
|
||||||
if structured.rpki.mode == "external":
|
if structured.rpki.mode == "external":
|
||||||
# If external validation is enabled, validate the prefix
|
# If external validation is enabled, validate the prefix
|
||||||
# & asn with Cloudflare's RPKI API.
|
# & asn with Cloudflare's RPKI API.
|
||||||
as_path = values["as_path"]
|
as_path = info.data.get("as_path", [])
|
||||||
|
|
||||||
if len(as_path) == 0:
|
if len(as_path) == 0:
|
||||||
# If the AS_PATH length is 0, i.e. for an internal route,
|
# If the AS_PATH length is 0, i.e. for an internal route,
|
||||||
|
|
@ -92,13 +92,13 @@ class BGPRoute(HyperglassModel):
|
||||||
asn = as_path[-1]
|
asn = as_path[-1]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
net = ip_network(values["prefix"])
|
net = ip_network(info.data["prefix"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
# Only do external RPKI lookups for global prefixes.
|
# Only do external RPKI lookups for global prefixes.
|
||||||
if net.is_global:
|
if net.is_global:
|
||||||
return rpki_state(prefix=values["prefix"], asn=asn)
|
return rpki_state(prefix=info.data["prefix"], asn=asn)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ class RuleWithPattern(Rule):
|
||||||
return InputValidationError(target=value, error="Denied")
|
return InputValidationError(target=value, error="Denied")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if isinstance(target, t.List) and multiple:
|
if isinstance(target, t.List):
|
||||||
for result in (validate_single_value(v) for v in target):
|
for result in (validate_single_value(v) for v in target):
|
||||||
if isinstance(result, BaseException):
|
if isinstance(result, BaseException):
|
||||||
self._passed = False
|
self._passed = False
|
||||||
|
|
@ -227,9 +227,6 @@ class RuleWithPattern(Rule):
|
||||||
self._passed = True
|
self._passed = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if isinstance(target, t.List) and not multiple:
|
|
||||||
raise InputValidationError(error="Target must be a single value", target=target)
|
|
||||||
|
|
||||||
result = validate_single_value(target)
|
result = validate_single_value(target)
|
||||||
|
|
||||||
if isinstance(result, BaseException):
|
if isinstance(result, BaseException):
|
||||||
|
|
|
||||||
|
|
@ -59,15 +59,23 @@ class HyperglassModel(BaseModel):
|
||||||
if isinstance(value, Path):
|
if isinstance(value, Path):
|
||||||
if Settings.container:
|
if Settings.container:
|
||||||
return Settings.default_app_path.joinpath(
|
return Settings.default_app_path.joinpath(
|
||||||
*(p for p in value.parts if p not in Settings.original_app_path.parts)
|
*(
|
||||||
|
p
|
||||||
|
for p in value.parts
|
||||||
|
if p not in Settings.original_app_path.absolute().parts
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(value, str):
|
if isinstance(value, str) and str(Settings.original_app_path.absolute()) in value:
|
||||||
if Settings.container:
|
if Settings.container:
|
||||||
path = Path(value)
|
path = Path(value)
|
||||||
return str(
|
return str(
|
||||||
Settings.default_app_path.joinpath(
|
Settings.default_app_path.joinpath(
|
||||||
*(p for p in path.parts if p not in Settings.original_app_path.parts)
|
*(
|
||||||
|
p
|
||||||
|
for p in path.parts
|
||||||
|
if p not in Settings.original_app_path.absolute().parts
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,6 @@ def parse_juniper(output: Sequence[str]) -> "OutputDataModel": # noqa: C901
|
||||||
parsed: "OrderedDict" = xmltodict.parse(
|
parsed: "OrderedDict" = xmltodict.parse(
|
||||||
cleaned, force_list=("rt", "rt-entry", "community")
|
cleaned, force_list=("rt", "rt-entry", "community")
|
||||||
)
|
)
|
||||||
_log.debug("Pre-parsed data", data=parsed)
|
|
||||||
|
|
||||||
if "rpc-reply" in parsed.keys():
|
if "rpc-reply" in parsed.keys():
|
||||||
if "xnm:error" in parsed["rpc-reply"]:
|
if "xnm:error" in parsed["rpc-reply"]:
|
||||||
if "message" in parsed["rpc-reply"]["xnm:error"]:
|
if "message" in parsed["rpc-reply"]["xnm:error"]:
|
||||||
|
|
|
||||||
|
|
@ -37,45 +37,46 @@ class MikrotikGarbageOutput(OutputPlugin):
|
||||||
result = ()
|
result = ()
|
||||||
|
|
||||||
for each_output in output:
|
for each_output in output:
|
||||||
if each_output.split()[-1] in ("DISTANCE", "STATUS"):
|
if len(each_output) != 0:
|
||||||
# Mikrotik shows the columns with no rows if there is no data.
|
if each_output.split()[-1] in ("DISTANCE", "STATUS"):
|
||||||
# Rather than send back an empty table, send back an empty
|
# Mikrotik shows the columns with no rows if there is no data.
|
||||||
# response which is handled with a warning message.
|
# Rather than send back an empty table, send back an empty
|
||||||
each_output = ""
|
# response which is handled with a warning message.
|
||||||
else:
|
each_output = ""
|
||||||
remove_lines = ()
|
else:
|
||||||
all_lines = each_output.splitlines()
|
remove_lines = ()
|
||||||
# Starting index for rows (after the column row).
|
all_lines = each_output.splitlines()
|
||||||
start = 1
|
# Starting index for rows (after the column row).
|
||||||
# Extract the column row.
|
start = 1
|
||||||
column_line = " ".join(all_lines[0].split())
|
# Extract the column row.
|
||||||
|
column_line = " ".join(all_lines[0].split())
|
||||||
|
|
||||||
for i, line in enumerate(all_lines[1:]):
|
for i, line in enumerate(all_lines[1:]):
|
||||||
# Remove all the newline characters (which differ line to
|
# Remove all the newline characters (which differ line to
|
||||||
# line) for comparison purposes.
|
# line) for comparison purposes.
|
||||||
normalized = " ".join(line.split())
|
normalized = " ".join(line.split())
|
||||||
|
|
||||||
# Remove ansii characters that aren't caught by Netmiko.
|
# Remove ansii characters that aren't caught by Netmiko.
|
||||||
normalized = re.sub(r"\\x1b\[\S{2}\s", "", normalized)
|
normalized = re.sub(r"\\x1b\[\S{2}\s", "", normalized)
|
||||||
|
|
||||||
if column_line in normalized:
|
if column_line in normalized:
|
||||||
# Mikrotik often re-inserts the column row in the output,
|
# Mikrotik often re-inserts the column row in the output,
|
||||||
# effectively 'starting over'. In that case, re-assign
|
# effectively 'starting over'. In that case, re-assign
|
||||||
# the column row and starting index to that point.
|
# the column row and starting index to that point.
|
||||||
column_line = re.sub(r"\[\S{2}\s", "", line)
|
column_line = re.sub(r"\[\S{2}\s", "", line)
|
||||||
start = i + 2
|
start = i + 2
|
||||||
|
|
||||||
if "[Q quit|D dump|C-z pause]" in normalized:
|
if "[Q quit|D dump|C-z pause]" in normalized:
|
||||||
# Remove Mikrotik's unhelpful helpers from the output.
|
# Remove Mikrotik's unhelpful helpers from the output.
|
||||||
remove_lines += (i + 1,)
|
remove_lines += (i + 1,)
|
||||||
|
|
||||||
# Combine the column row and the data rows from the starting
|
# Combine the column row and the data rows from the starting
|
||||||
# index onward.
|
# index onward.
|
||||||
lines = [column_line, *all_lines[start:]]
|
lines = [column_line, *all_lines[start:]]
|
||||||
|
|
||||||
# Remove any lines marked for removal and re-join with a single
|
# Remove any lines marked for removal and re-join with a single
|
||||||
# newline character.
|
# newline character.
|
||||||
lines = [line for idx, line in enumerate(lines) if idx not in remove_lines]
|
lines = [line for idx, line in enumerate(lines) if idx not in remove_lines]
|
||||||
result += ("\n".join(lines),)
|
result += ("\n".join(lines),)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ class InputPluginManager(PluginManager[InputPlugin], type="input"):
|
||||||
"""Execute all input transformation plugins."""
|
"""Execute all input transformation plugins."""
|
||||||
result = query.query_target
|
result = query.query_target
|
||||||
for plugin in self._gather_plugins(query):
|
for plugin in self._gather_plugins(query):
|
||||||
result = plugin.transform(query=query)
|
result = plugin.transform(query=query.summary())
|
||||||
log.bind(name=plugin.name, result=repr(result)).debug("Input Plugin Transform")
|
log.bind(name=plugin.name, result=repr(result)).debug("Input Plugin Transform")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@ import { useMemo } from 'react';
|
||||||
import { Button, Menu, MenuButton, MenuList } from '@chakra-ui/react';
|
import { Button, Menu, MenuButton, MenuList } from '@chakra-ui/react';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { Markdown } from '~/elements';
|
import { Markdown } from '~/elements';
|
||||||
import { useColorValue, useBreakpointValue, useOpposingColor, useStrf } from '~/hooks';
|
import { useStrf } from '~/hooks';
|
||||||
|
|
||||||
import type { MenuListProps } from '@chakra-ui/react';
|
import type { MenuListProps } from '@chakra-ui/react';
|
||||||
import type { Config } from '~/types';
|
import type { Config } from '~/types';
|
||||||
|
|
||||||
interface FooterButtonProps extends Omit<MenuListProps, 'title'> {
|
interface FooterButtonProps extends Omit<MenuListProps, 'title'> {
|
||||||
side: 'left' | 'right';
|
|
||||||
title?: MenuListProps['children'];
|
title?: MenuListProps['children'];
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
@ -27,26 +26,26 @@ function getConfigFmt(config: Config): Record<string, string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FooterButton = (props: FooterButtonProps): JSX.Element => {
|
export const FooterButton = (props: FooterButtonProps): JSX.Element => {
|
||||||
const { content, title, side, ...rest } = props;
|
const { content, title, ...rest } = props;
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const strF = useStrf();
|
const strF = useStrf();
|
||||||
const fmt = useMemo(() => getConfigFmt(config), [config]);
|
const fmt = useMemo(() => getConfigFmt(config), [config]);
|
||||||
const fmtContent = useMemo(() => strF(content, fmt), [fmt, content, strF]);
|
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');
|
|
||||||
const color = useOpposingColor(bg);
|
|
||||||
const size = useBreakpointValue({ base: 'xs', lg: 'sm' });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu placement={placement} preventOverflow isLazy>
|
<Menu
|
||||||
|
placement="top"
|
||||||
|
preventOverflow
|
||||||
|
isLazy
|
||||||
|
>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
zIndex={2}
|
zIndex={2}
|
||||||
|
py={1}
|
||||||
|
fontWeight="normal"
|
||||||
as={Button}
|
as={Button}
|
||||||
size={size}
|
variant="link"
|
||||||
variant="ghost"
|
colorScheme="transparent"
|
||||||
lineHeight={0}
|
|
||||||
aria-label={typeof title === 'string' ? title : undefined}
|
aria-label={typeof title === 'string' ? title : undefined}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
|
|
@ -54,10 +53,10 @@ export const FooterButton = (props: FooterButtonProps): JSX.Element => {
|
||||||
<MenuList
|
<MenuList
|
||||||
px={6}
|
px={6}
|
||||||
py={4}
|
py={4}
|
||||||
bg={bg}
|
bg="white"
|
||||||
// Ensure the height doesn't overtake the viewport, especially on mobile. See overflow also.
|
// Ensure the height doesn't overtake the viewport, especially on mobile. See overflow also.
|
||||||
maxH="50vh"
|
maxH="50vh"
|
||||||
color={color}
|
color="black"
|
||||||
boxShadow="2xl"
|
boxShadow="2xl"
|
||||||
textAlign="left"
|
textAlign="left"
|
||||||
overflowY="auto"
|
overflowY="auto"
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { Flex, HStack, useToken } from '@chakra-ui/react';
|
import { Box, Container, Flex, Grid, GridItem } from '@chakra-ui/react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { DynamicIcon } from '~/elements';
|
import { DynamicIcon, Markdown } from '~/elements';
|
||||||
import { useBreakpointValue, useColorValue, useMobile } from '~/hooks';
|
import { useMobile } from '~/hooks';
|
||||||
import { isLink, isMenu } from '~/types';
|
import { isLink, isMenu } from '~/types';
|
||||||
import { FooterButton } from './button';
|
import { FooterButton } from './button';
|
||||||
import { ColorModeToggle } from './color-mode';
|
|
||||||
import { FooterLink } from './link';
|
import { FooterLink } from './link';
|
||||||
|
|
||||||
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
|
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
|
||||||
import type { Link, Menu } from '~/types';
|
import type { Link, Menu } from '~/types';
|
||||||
|
import { Logo } from './logo';
|
||||||
|
|
||||||
type MenuItems = (Link | Menu)[];
|
type MenuItems = (Link | Menu)[];
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ function buildItems(links: Link[], menus: Menu[]): [MenuItems, MenuItems] {
|
||||||
return [left, right];
|
return [left, right];
|
||||||
}
|
}
|
||||||
|
|
||||||
const LinkOnSide = (props: { item: ArrayElement<MenuItems>; side: 'left' | 'right' }) => {
|
const NavLink = (props: { item: ArrayElement<MenuItems> }) => {
|
||||||
const { item, side } = props;
|
const { item, side } = props;
|
||||||
if (isLink(item)) {
|
if (isLink(item)) {
|
||||||
const icon: Partial<ButtonProps & LinkProps> = {};
|
const icon: Partial<ButtonProps & LinkProps> = {};
|
||||||
|
|
@ -40,49 +40,55 @@ const LinkOnSide = (props: { item: ArrayElement<MenuItems>; side: 'left' | 'righ
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Footer = (): JSX.Element => {
|
export const Footer = (): JSX.Element => {
|
||||||
const { web, content } = useConfig();
|
const { web } = useConfig();
|
||||||
|
|
||||||
const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
|
|
||||||
const footerColor = useColorValue('black', 'white');
|
|
||||||
|
|
||||||
const size = useBreakpointValue({ base: useToken('sizes', 4), lg: useToken('sizes', 6) });
|
|
||||||
|
|
||||||
const isMobile = useMobile();
|
|
||||||
|
|
||||||
const [left, right] = useMemo(() => buildItems(web.links, web.menus), [web.links, web.menus]);
|
const [left, right] = useMemo(() => buildItems(web.links, web.menus), [web.links, web.menus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<Box
|
||||||
px={6}
|
|
||||||
py={4}
|
|
||||||
w="100%"
|
w="100%"
|
||||||
zIndex={1}
|
bg="#005e8a"
|
||||||
as="footer"
|
color="#ffffff"
|
||||||
bg={footerBg}
|
fontSize=".945rem"
|
||||||
whiteSpace="nowrap"
|
|
||||||
color={footerColor}
|
|
||||||
spacing={{ base: 8, lg: 6 }}
|
|
||||||
display={{ base: 'inline-block', lg: 'flex' }}
|
|
||||||
overflowY={{ base: 'auto', lg: 'unset' }}
|
|
||||||
justifyContent={{ base: 'center', lg: 'space-between' }}
|
|
||||||
>
|
>
|
||||||
{left.map(item => (
|
<Container maxW="8xl">
|
||||||
<LinkOnSide key={item.title} item={item} side="left" />
|
<Grid
|
||||||
))}
|
px={6}
|
||||||
{!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />}
|
py={4}
|
||||||
{right.map(item => (
|
w="100%"
|
||||||
<LinkOnSide key={item.title} item={item} side="right" />
|
zIndex={1}
|
||||||
))}
|
as="footer"
|
||||||
{web.credit.enable && (
|
whiteSpace="nowrap"
|
||||||
<FooterButton
|
templateColumns="repeat(4, 1fr)"
|
||||||
key="credit"
|
gap={6}
|
||||||
side="right"
|
>
|
||||||
content={content.credit}
|
<GridItem colSpan={{base: 4, md: 2}}>
|
||||||
title={<DynamicIcon icon={{ fi: 'FiCode' }} boxSize={size} />}
|
<Flex
|
||||||
/>
|
flex="1 0 auto"
|
||||||
)}
|
key="credit"
|
||||||
|
side="left"
|
||||||
<ColorModeToggle size={size} />
|
maxW="50%"
|
||||||
</HStack>
|
>
|
||||||
|
<Box w="50px" maxW="15vw" my="2" mr="4">
|
||||||
|
<Logo />
|
||||||
|
</Box>
|
||||||
|
<Markdown content={web.copyright} />
|
||||||
|
</Flex>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem colSpan={{base: 4, md: 1}}>
|
||||||
|
<Flex flexDirection="column" alignItems="start">
|
||||||
|
{left.map(item => (
|
||||||
|
<NavLink key={item.title} item={item} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem colSpan={{base: 4, md: 1}}>
|
||||||
|
{right.map(item => (
|
||||||
|
<NavLink key={item.title} item={item} />
|
||||||
|
))}
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Button, Link } from '@chakra-ui/react';
|
import { Button, Link } from '@chakra-ui/react';
|
||||||
import { useBreakpointValue } from '~/hooks';
|
|
||||||
|
|
||||||
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
|
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
|
@ -7,9 +6,17 @@ type FooterLinkProps = ButtonProps & LinkProps & { title: string };
|
||||||
|
|
||||||
export const FooterLink = (props: FooterLinkProps): JSX.Element => {
|
export const FooterLink = (props: FooterLinkProps): JSX.Element => {
|
||||||
const { title } = props;
|
const { title } = props;
|
||||||
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
|
|
||||||
return (
|
return (
|
||||||
<Button as={Link} isExternal size={btnSize} variant="ghost" aria-label={title} {...props}>
|
<Button
|
||||||
|
as={Link}
|
||||||
|
isExternal
|
||||||
|
py={1}
|
||||||
|
fontWeight="normal"
|
||||||
|
variant="link"
|
||||||
|
colorScheme="transparent"
|
||||||
|
aria-label={title}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
37
hyperglass/ui/components/footer/logo.tsx
Normal file
37
hyperglass/ui/components/footer/logo.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Image, Skeleton } from '@chakra-ui/react';
|
||||||
|
import { useConfig } from '~/context';
|
||||||
|
|
||||||
|
import type { ImageProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export const Logo = (props: ImageProps): JSX.Element => {
|
||||||
|
const { web } = useConfig();
|
||||||
|
const { footerFormat } = web.logo;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={`/images/footer${footerFormat}`}
|
||||||
|
alt={web.text.title}
|
||||||
|
width="100%"
|
||||||
|
css={{
|
||||||
|
objectFit: 'contain',
|
||||||
|
objectPosition: 'top',
|
||||||
|
userDrag: 'none',
|
||||||
|
userSelect: 'none',
|
||||||
|
msUserSelect: 'none',
|
||||||
|
MozUserSelect: 'none',
|
||||||
|
WebkitUserDrag: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
}}
|
||||||
|
fallback={
|
||||||
|
<Skeleton
|
||||||
|
isLoaded={false}
|
||||||
|
borderRadius="md"
|
||||||
|
endColor="light.500"
|
||||||
|
startColor="whiteSolid.100"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -2,7 +2,7 @@ import { Flex, FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { If, Then } from 'react-if';
|
import { If, Then } from 'react-if';
|
||||||
import { useBooleanValue, useColorValue } from '~/hooks';
|
import { useBooleanValue } from '~/hooks';
|
||||||
|
|
||||||
import type { FormControlProps } from '@chakra-ui/react';
|
import type { FormControlProps } from '@chakra-ui/react';
|
||||||
import type { FieldError } from 'react-hook-form';
|
import type { FieldError } from 'react-hook-form';
|
||||||
|
|
@ -18,8 +18,8 @@ interface FormFieldProps extends FormControlProps {
|
||||||
|
|
||||||
export const FormField = (props: FormFieldProps): JSX.Element => {
|
export const FormField = (props: FormFieldProps): JSX.Element => {
|
||||||
const { name, label, children, labelAddOn, fieldAddOn, hiddenLabels = false, ...rest } = props;
|
const { name, label, children, labelAddOn, fieldAddOn, hiddenLabels = false, ...rest } = props;
|
||||||
const labelColor = useColorValue('blackAlpha.700', 'whiteAlpha.700');
|
const labelColor = 'blackAlpha.700';
|
||||||
const errorColor = useColorValue('red.500', 'red.300');
|
const errorColor = 'red.500';
|
||||||
const opacity = useBooleanValue(hiddenLabels, 0, undefined);
|
const opacity = useBooleanValue(hiddenLabels, 0, undefined);
|
||||||
|
|
||||||
const { formState } = useFormContext<FormData>();
|
const { formState } = useFormContext<FormData>();
|
||||||
|
|
@ -47,7 +47,6 @@ export const FormField = (props: FormFieldProps): JSX.Element => {
|
||||||
>
|
>
|
||||||
<FormLabel
|
<FormLabel
|
||||||
pr={0}
|
pr={0}
|
||||||
mb={{ lg: 4 }}
|
|
||||||
htmlFor={name}
|
htmlFor={name}
|
||||||
display="flex"
|
display="flex"
|
||||||
opacity={opacity}
|
opacity={opacity}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
import { If, Then } from 'react-if';
|
import { If, Then } from 'react-if';
|
||||||
import { Markdown } from '~/elements';
|
import { Markdown } from '~/elements';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { useGreeting, useColorValue, useOpposingColor } from '~/hooks';
|
import { useGreeting } from '~/hooks';
|
||||||
|
|
||||||
import type { ModalContentProps } from '@chakra-ui/react';
|
import type { ModalContentProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
|
@ -20,9 +20,6 @@ export const Greeting = (props: ModalContentProps): JSX.Element => {
|
||||||
const { web, content } = useConfig();
|
const { web, content } = useConfig();
|
||||||
const { isAck, isOpen, open, ack } = useGreeting();
|
const { isAck, isOpen, open, ack } = useGreeting();
|
||||||
|
|
||||||
const bg = useColorValue('white', 'gray.800');
|
|
||||||
const color = useOpposingColor(bg);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!web.greeting.enable && !web.greeting.required) {
|
if (!web.greeting.enable && !web.greeting.required) {
|
||||||
ack(true, false);
|
ack(true, false);
|
||||||
|
|
@ -44,8 +41,8 @@ export const Greeting = (props: ModalContentProps): JSX.Element => {
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent
|
<ModalContent
|
||||||
py={4}
|
py={4}
|
||||||
bg={bg}
|
bg="white"
|
||||||
color={color}
|
color="black"
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
maxW={{ base: '95%', md: '75%' }}
|
maxW={{ base: '95%', md: '75%' }}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { Flex, ScaleFade } from '@chakra-ui/react';
|
import { Container, Flex, ScaleFade } from '@chakra-ui/react';
|
||||||
import { motionChakra } from '~/elements';
|
import { motionChakra } from '~/elements';
|
||||||
import { useBooleanValue, useFormInteractive, useBreakpointValue } from '~/hooks';
|
import { useBooleanValue, useBreakpointValue, useFormInteractive } from '~/hooks';
|
||||||
import { Title } from './title';
|
import { Title } from './title';
|
||||||
|
|
||||||
const Wrapper = motionChakra('header', {
|
const Wrapper = motionChakra('header');
|
||||||
baseStyle: { display: 'flex', px: 4, pt: 6, minH: 16, w: 'full', flex: '0 1 auto' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Header = (): JSX.Element => {
|
export const Header = (): JSX.Element => {
|
||||||
const formInteractive = useFormInteractive();
|
const formInteractive = useFormInteractive();
|
||||||
|
|
@ -16,21 +14,28 @@ export const Header = (): JSX.Element => {
|
||||||
{ base: '75%', lg: '75%' },
|
{ base: '75%', lg: '75%' },
|
||||||
);
|
);
|
||||||
|
|
||||||
const justify = useBreakpointValue({ base: 'flex-start', lg: 'center' });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper layout="position">
|
<Wrapper layout="position">
|
||||||
<ScaleFade in initialScale={0.5} style={{ width: '100%' }}>
|
<Container
|
||||||
<Flex
|
maxW="8xl"
|
||||||
height="100%"
|
display="flex"
|
||||||
maxW={titleWidth}
|
px={4}
|
||||||
// This is here for the logo
|
pt={6}
|
||||||
justifyContent={justify}
|
minH={16}
|
||||||
mx={{ base: formInteractive ? 'auto' : 0, lg: 'auto' }}
|
flex="0 1 auto"
|
||||||
>
|
>
|
||||||
<Title />
|
<ScaleFade in initialScale={0.5} style={{ width: '100%' }}>
|
||||||
</Flex>
|
<Flex
|
||||||
</ScaleFade>
|
height="100%"
|
||||||
|
maxW={titleWidth}
|
||||||
|
// This is here for the logo
|
||||||
|
justifyContent="center"
|
||||||
|
mx="auto"
|
||||||
|
>
|
||||||
|
<Title />
|
||||||
|
</Flex>
|
||||||
|
</ScaleFade>
|
||||||
|
</Container>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,19 @@
|
||||||
import { Image, Skeleton } from '@chakra-ui/react';
|
import { Image, Skeleton } from '@chakra-ui/react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { useColorValue } from '~/hooks';
|
import { useColorValue } from '~/hooks';
|
||||||
|
|
||||||
import type { ImageProps } from '@chakra-ui/react';
|
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 { darkFormat, lightFormat } = web.logo;
|
|
||||||
|
|
||||||
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 [fallback, setSource] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// 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('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], [fallback, setFallback, src]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Logo = (props: ImageProps): JSX.Element => {
|
export const Logo = (props: ImageProps): JSX.Element => {
|
||||||
const { web } = useConfig();
|
const { web } = useConfig();
|
||||||
const { width } = web.logo;
|
const { lightFormat, width } = web.logo;
|
||||||
|
|
||||||
const skeletonA = useColorValue('whiteSolid.100', 'blackSolid.800');
|
|
||||||
const skeletonB = useColorValue('light.500', 'dark.500');
|
|
||||||
|
|
||||||
const [source, setFallback] = useLogo();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
src={source}
|
src={`/images/light${lightFormat}`}
|
||||||
alt={web.text.title}
|
alt={web.text.title}
|
||||||
onError={setFallback}
|
maxW={{ base: '100%', md: width }}
|
||||||
width={width ?? 'auto'}
|
width="auto"
|
||||||
css={{
|
css={{
|
||||||
userDrag: 'none',
|
userDrag: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
|
@ -54,8 +26,8 @@ export const Logo = (props: ImageProps): JSX.Element => {
|
||||||
<Skeleton
|
<Skeleton
|
||||||
isLoaded={false}
|
isLoaded={false}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
endColor={skeletonB}
|
endColor="light.500"
|
||||||
startColor={skeletonA}
|
startColor="whiteSolid.100"
|
||||||
width={{ base: 64, lg: 80 }}
|
width={{ base: 64, lg: 80 }}
|
||||||
height={{ base: 12, lg: 16 }}
|
height={{ base: 12, lg: 16 }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ const MWrapper = (props: MWrapperProps): JSX.Element => {
|
||||||
layout
|
layout
|
||||||
spacing={1}
|
spacing={1}
|
||||||
alignItems={formInteractive ? 'center' : 'flex-start'}
|
alignItems={formInteractive ? 'center' : 'flex-start'}
|
||||||
maxWidth="25%"
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -47,7 +46,7 @@ const DWrapper = (props: DWrapperProps): JSX.Element => {
|
||||||
animate={formInteractive}
|
animate={formInteractive}
|
||||||
transition={{ damping: 15, type: 'spring', stiffness: 100 }}
|
transition={{ damping: 15, type: 'spring', stiffness: 100 }}
|
||||||
variants={{ results: { scale: 0.5 }, form: { scale: 1 } }}
|
variants={{ results: { scale: 0.5 }, form: { scale: 1 } }}
|
||||||
maxWidth="25%"
|
maxWidth="75%"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -145,7 +144,7 @@ export const Title = (props: FlexProps): JSX.Element => {
|
||||||
variant="link"
|
variant="link"
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
flexDir="column"
|
flexDir="column"
|
||||||
onClick={() => reset()}
|
onClick={async () => await reset()}
|
||||||
_focus={{ boxShadow: 'none' }}
|
_focus={{ boxShadow: 'none' }}
|
||||||
_hover={{ textDecoration: 'none' }}
|
_hover={{ textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
import { Container, Flex } from '@chakra-ui/react';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import { isSafari } from 'react-device-detect';
|
import { isSafari } from 'react-device-detect';
|
||||||
import { If, Then } from 'react-if';
|
import { If, Then } from 'react-if';
|
||||||
import { Debugger, Greeting, Footer, Header, ResetButton } from '~/components';
|
import { Debugger, Footer, Greeting, Header, ResetButton } from '~/components';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { motionChakra } from '~/elements';
|
import { motionChakra } from '~/elements';
|
||||||
import { useFormState } from '~/hooks';
|
import { useFormState } from '~/hooks';
|
||||||
|
|
@ -31,7 +31,7 @@ export const Layout = (props: FlexProps): JSX.Element => {
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>({} as HTMLDivElement);
|
const containerRef = useRef<HTMLDivElement>({} as HTMLDivElement);
|
||||||
|
|
||||||
function handleReset(): void {
|
async function handleReset() {
|
||||||
containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
setStatus('form');
|
setStatus('form');
|
||||||
reset();
|
reset();
|
||||||
|
|
@ -53,15 +53,17 @@ export const Layout = (props: FlexProps): JSX.Element => {
|
||||||
minHeight={isSafari ? '-webkit-fill-available' : '100vh'}
|
minHeight={isSafari ? '-webkit-fill-available' : '100vh'}
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<Main
|
<Container maxW="8xl" display="flex" flex="1 1 auto">
|
||||||
layout
|
<Main
|
||||||
animate={{ opacity: 1, y: 0 }}
|
layout
|
||||||
transition={{ duration: 0.3 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, x: -300 }}
|
transition={{ duration: 0.3 }}
|
||||||
initial={{ opacity: 0, y: 300 }}
|
exit={{ opacity: 0, x: -300 }}
|
||||||
>
|
initial={{ opacity: 0, y: 300 }}
|
||||||
{props.children}
|
>
|
||||||
</Main>
|
{props.children}
|
||||||
|
</Main>
|
||||||
|
</Container>
|
||||||
<Footer />
|
<Footer />
|
||||||
<If condition={developerMode}>
|
<If condition={developerMode}>
|
||||||
<Then>
|
<Then>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Flex, Avatar, chakra } from '@chakra-ui/react';
|
import { Flex, Avatar, chakra } from '@chakra-ui/react';
|
||||||
import { motionChakra } from '~/elements';
|
import { motionChakra } from '~/elements';
|
||||||
import { useColorValue, useOpposingColor } from '~/hooks';
|
|
||||||
|
|
||||||
import type { SingleOption } from '~/types';
|
import type { SingleOption } from '~/types';
|
||||||
import type { LocationOption } from './query-location';
|
import type { LocationOption } from './query-location';
|
||||||
|
|
@ -43,11 +42,9 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bg = useColorValue('white', 'blackSolid.600');
|
const imageBorder = 'gray.600';
|
||||||
const imageBorder = useColorValue('gray.600', 'whiteAlpha.800');
|
const checkedBorder = 'brand.500';
|
||||||
const fg = useOpposingColor(bg);
|
const errorBorder = 'red.500';
|
||||||
const checkedBorder = useColorValue('blue.400', 'blue.300');
|
|
||||||
const errorBorder = useColorValue('red.500', 'red.300');
|
|
||||||
|
|
||||||
const borderColor = useMemo(
|
const borderColor = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -64,7 +61,7 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<LocationCardWrapper
|
<LocationCardWrapper
|
||||||
bg={bg}
|
bg="white"
|
||||||
key={label}
|
key={label}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
|
|
@ -76,7 +73,6 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
|
||||||
<>
|
<>
|
||||||
<Flex justifyContent="space-between" alignItems="center">
|
<Flex justifyContent="space-between" alignItems="center">
|
||||||
<chakra.h2
|
<chakra.h2
|
||||||
color={fg}
|
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
mt={{ base: 2, md: 0 }}
|
mt={{ base: 2, md: 0 }}
|
||||||
fontSize={{ base: 'lg', md: 'xl' }}
|
fontSize={{ base: 'lg', md: 'xl' }}
|
||||||
|
|
@ -84,7 +80,7 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
|
||||||
{label}
|
{label}
|
||||||
</chakra.h2>
|
</chakra.h2>
|
||||||
<Avatar
|
<Avatar
|
||||||
color={fg}
|
color="black"
|
||||||
name={label}
|
name={label}
|
||||||
boxSize={12}
|
boxSize={12}
|
||||||
rounded="full"
|
rounded="full"
|
||||||
|
|
@ -97,7 +93,7 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{option?.data?.description && (
|
{option?.data?.description && (
|
||||||
<chakra.p mt={2} color={fg} opacity={0.6} fontSize="sm">
|
<chakra.p mt={2} opacity={0.6} fontSize="sm">
|
||||||
{option.data.description as string}
|
{option.data.description as string}
|
||||||
</chakra.p>
|
</chakra.p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,9 @@ export const LookingGlassForm = (): JSX.Element => {
|
||||||
|
|
||||||
const isFqdnQuery = useCallback(
|
const isFqdnQuery = useCallback(
|
||||||
(target: string | string[], fieldType: Directive['fieldType'] | null): boolean =>
|
(target: string | string[], fieldType: Directive['fieldType'] | null): boolean =>
|
||||||
typeof target === 'string' && fieldType === 'text' && isFQDN(target),
|
(typeof target === 'string' || Array.isArray(target)) &&
|
||||||
|
fieldType === 'text' &&
|
||||||
|
isFQDN(target),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -171,7 +173,7 @@ export const LookingGlassForm = (): JSX.Element => {
|
||||||
w="100%"
|
w="100%"
|
||||||
mx="auto"
|
mx="auto"
|
||||||
textAlign="left"
|
textAlign="left"
|
||||||
maxW={{ base: '100%', lg: '75%' }}
|
maxW="100%"
|
||||||
onSubmit={handleSubmit(submitHandler)}
|
onSubmit={handleSubmit(submitHandler)}
|
||||||
>
|
>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export const Meta = (): JSX.Element => {
|
||||||
const [location, setLocation] = useState('/');
|
const [location, setLocation] = useState('/');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
siteTitle: title = 'hyperglass',
|
siteTitle: title = 'Looking Glass',
|
||||||
siteDescription: description = 'Network Looking Glass',
|
siteDescription: description = 'Network Looking Glass',
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
||||||
|
|
@ -23,16 +23,13 @@ export const Meta = (): JSX.Element => {
|
||||||
<Head>
|
<Head>
|
||||||
<title key="title">{title}</title>
|
<title key="title">{title}</title>
|
||||||
<meta name="url" content={location} />
|
<meta name="url" content={location} />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="og:title" content={title} />
|
<meta name="og:title" content={title} />
|
||||||
<meta name="og:url" content={location} />
|
<meta name="og:url" content={location} />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<meta property="og:image:alt" content={siteName} />
|
<meta property="og:image:alt" content={siteName} />
|
||||||
<meta name="og:description" content={description} />
|
<meta name="og:description" content={description} />
|
||||||
<meta name="hyperglass-version" content={config.version} />
|
<meta name="lg-version" content={config.version} />
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0"
|
|
||||||
/>
|
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -147,8 +147,6 @@ export const ASPath = (props: ASPathProps): JSX.Element => {
|
||||||
export const Communities = (props: CommunitiesProps): JSX.Element => {
|
export const Communities = (props: CommunitiesProps): JSX.Element => {
|
||||||
const { communities } = props;
|
const { communities } = props;
|
||||||
const { web } = useConfig();
|
const { web } = useConfig();
|
||||||
const bg = useColorValue('white', 'gray.900');
|
|
||||||
const color = useOpposingColor(bg);
|
|
||||||
return (
|
return (
|
||||||
<If condition={communities.length === 0}>
|
<If condition={communities.length === 0}>
|
||||||
<Then>
|
<Then>
|
||||||
|
|
@ -165,10 +163,10 @@ export const Communities = (props: CommunitiesProps): JSX.Element => {
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList
|
<MenuList
|
||||||
p={3}
|
p={3}
|
||||||
bg={bg}
|
bg="white"
|
||||||
minW={32}
|
minW={32}
|
||||||
width="unset"
|
width="unset"
|
||||||
color={color}
|
color="black"
|
||||||
boxShadow="2xl"
|
boxShadow="2xl"
|
||||||
textAlign="left"
|
textAlign="left"
|
||||||
fontFamily="mono"
|
fontFamily="mono"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import ReactFlow, {
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { useASNDetail, useColorToken, useColorValue } from '~/hooks';
|
import { useASNDetail, useColorToken, useColorValue } from '~/hooks';
|
||||||
import { Controls } from './controls';
|
import { Controls } from './controls';
|
||||||
import { useElements } from './useElements';
|
import { useElements } from './use-elements';
|
||||||
|
|
||||||
import type { NodeProps as ReactFlowNodeProps } from 'reactflow';
|
import type { NodeProps as ReactFlowNodeProps } from 'reactflow';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export const PathButton = (props: PathButtonProps): JSX.Element => {
|
||||||
const { onOpen } = props;
|
const { onOpen } = props;
|
||||||
return (
|
return (
|
||||||
<Tooltip hasArrow label="View AS Path" placement="top">
|
<Tooltip hasArrow label="View AS Path" placement="top">
|
||||||
<Button as="a" mx={1} size="sm" variant="ghost" onClick={onOpen} colorScheme="secondary">
|
<Button as="a" mx={1} size="sm" variant="ghost" onClick={onOpen} colorScheme="primary">
|
||||||
<DynamicIcon icon={{ bi: 'BiNetworkChart' }} boxSize="16px" />
|
<DynamicIcon icon={{ bi: 'BiNetworkChart' }} boxSize="16px" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@ export const Path = (props: PathProps): JSX.Element => {
|
||||||
<ModalHeader>{`Path to ${displayTarget}`}</ModalHeader>
|
<ModalHeader>{`Path to ${displayTarget}`}</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{response !== null ? <Chart data={output} /> : <Skeleton w="500px" h="300px" />}
|
<Skeleton isLoaded={response != null}>
|
||||||
|
<Chart data={output} />
|
||||||
|
</Skeleton>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,17 @@ import {
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverCloseButton,
|
PopoverCloseButton,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useColorValue } from '~/hooks';
|
|
||||||
|
|
||||||
import type { PromptProps } from './types';
|
import type { PromptProps } from './types';
|
||||||
|
|
||||||
export const DesktopPrompt = (props: PromptProps): JSX.Element => {
|
export const DesktopPrompt = (props: PromptProps): JSX.Element => {
|
||||||
const { trigger, children, ...disclosure } = props;
|
const { trigger, children, ...disclosure } = props;
|
||||||
const bg = useColorValue('white', 'gray.900');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover closeOnBlur={false} {...disclosure}>
|
<Popover closeOnBlur={false} {...disclosure}>
|
||||||
<PopoverTrigger>{trigger}</PopoverTrigger>
|
<PopoverTrigger>{trigger}</PopoverTrigger>
|
||||||
<PopoverContent bg={bg}>
|
<PopoverContent bg="white">
|
||||||
<PopoverArrow bg={bg} />
|
<PopoverArrow bg="white" />
|
||||||
<PopoverCloseButton />
|
<PopoverCloseButton />
|
||||||
<PopoverBody p={6}>{children}</PopoverBody>
|
<PopoverBody p={6}>{children}</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { Modal, ModalBody, ModalOverlay, ModalContent, ModalCloseButton } from '@chakra-ui/react';
|
import { Modal, ModalBody, ModalOverlay, ModalContent, ModalCloseButton } from '@chakra-ui/react';
|
||||||
import { useColorValue } from '~/hooks';
|
|
||||||
|
|
||||||
import type { PromptProps } from './types';
|
import type { PromptProps } from './types';
|
||||||
|
|
||||||
export const MobilePrompt = (props: PromptProps): JSX.Element => {
|
export const MobilePrompt = (props: PromptProps): JSX.Element => {
|
||||||
const { children, trigger, ...disclosure } = props;
|
const { children, trigger, ...disclosure } = props;
|
||||||
const bg = useColorValue('white', 'gray.900');
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{trigger}
|
{trigger}
|
||||||
|
|
@ -18,7 +16,7 @@ export const MobilePrompt = (props: PromptProps): JSX.Element => {
|
||||||
{...disclosure}
|
{...disclosure}
|
||||||
>
|
>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent bg={bg}>
|
<ModalContent bg="white">
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody px={4} py={10}>
|
<ModalBody px={4} py={10}>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -120,10 +120,10 @@ export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
|
||||||
<>
|
<>
|
||||||
{options.length === 1 ? (
|
{options.length === 1 ? (
|
||||||
<Wrap
|
<Wrap
|
||||||
p={{ lg: 4 }}
|
p={{ lg: 2 }}
|
||||||
align="flex-start"
|
align="flex-start"
|
||||||
shouldWrapChildren
|
shouldWrapChildren
|
||||||
spacing={{ base: 4, lg: 8 }}
|
spacing={{ base: 4, lg: 6 }}
|
||||||
justify={{ base: 'center', lg: 'center' }}
|
justify={{ base: 'center', lg: 'center' }}
|
||||||
>
|
>
|
||||||
{options[0].options.map(opt => {
|
{options[0].options.map(opt => {
|
||||||
|
|
@ -170,7 +170,7 @@ export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
|
||||||
options={options}
|
options={options}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
name="queryLocation"
|
name="queryLocation"
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={true}
|
||||||
onChange={handleSelectChange}
|
onChange={handleSelectChange}
|
||||||
value={selections.queryLocation}
|
value={selections.queryLocation}
|
||||||
isError={typeof errors.queryLocation !== 'undefined'}
|
isError={typeof errors.queryLocation !== 'undefined'}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { useDirective, useFormState } from '~/hooks';
|
||||||
import { isSelectDirective } from '~/types';
|
import { isSelectDirective } from '~/types';
|
||||||
import { UserIP } from './user-ip';
|
import { UserIP } from './user-ip';
|
||||||
|
|
||||||
import { type UseFormRegister, useForm } from 'react-hook-form';
|
import { type UseFormRegister } from 'react-hook-form';
|
||||||
import type { GroupBase, OptionProps } from 'react-select';
|
import type { GroupBase, OptionProps } from 'react-select';
|
||||||
import type { SelectOnChange } from '~/components/select';
|
import type { SelectOnChange } from '~/components/select';
|
||||||
import type { Directive, FormData, OnChangeArgs, SingleOption } from '~/types';
|
import type { Directive, FormData, OnChangeArgs, SingleOption } from '~/types';
|
||||||
|
|
@ -87,7 +87,7 @@ export const QueryTarget = (props: QueryTargetProps): JSX.Element => {
|
||||||
<InputGroup size="lg">
|
<InputGroup size="lg">
|
||||||
<Input
|
<Input
|
||||||
bg="white"
|
bg="white"
|
||||||
color="gray.400"
|
color="black"
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
borderColor="gray.100"
|
borderColor="gray.100"
|
||||||
value={displayTarget}
|
value={displayTarget}
|
||||||
|
|
@ -96,12 +96,6 @@ export const QueryTarget = (props: QueryTargetProps): JSX.Element => {
|
||||||
name="queryTargetDisplay"
|
name="queryTargetDisplay"
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
_placeholder={{ color: 'gray.600' }}
|
_placeholder={{ color: 'gray.600' }}
|
||||||
_dark={{
|
|
||||||
bg: 'blackSolid.800',
|
|
||||||
color: 'whiteAlpha.800',
|
|
||||||
borderColor: 'whiteAlpha.50',
|
|
||||||
_placeholder: { color: 'whiteAlpha.700' },
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<InputRightElement w="max-content" pr={2}>
|
<InputRightElement w="max-content" pr={2}>
|
||||||
<UserIP setTarget={handleUserIPChange} />
|
<UserIP setTarget={handleUserIPChange} />
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,16 @@ interface ResetButtonProps extends FlexProps {
|
||||||
export const ResetButton = (props: ResetButtonProps): JSX.Element => {
|
export const ResetButton = (props: ResetButtonProps): JSX.Element => {
|
||||||
const { developerMode, resetForm, ...rest } = props;
|
const { developerMode, resetForm, ...rest } = props;
|
||||||
const status = useFormState(s => s.status);
|
const status = useFormState(s => s.status);
|
||||||
const bg = useColorValue('primary.500', 'primary.300');
|
|
||||||
const color = useOpposingColor(bg);
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{status === 'results' && (
|
{status === 'results' && (
|
||||||
<AnimatedDiv
|
<AnimatedDiv
|
||||||
bg={bg}
|
bg="brand.500"
|
||||||
left={0}
|
left={0}
|
||||||
zIndex={4}
|
zIndex={4}
|
||||||
bottom={24}
|
bottom={24}
|
||||||
boxSize={12}
|
boxSize={12}
|
||||||
color={color}
|
color="white"
|
||||||
position="fixed"
|
position="fixed"
|
||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
exit={{ x: '-100%' }}
|
exit={{ x: '-100%' }}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||||
import { Button, Stack, Text, VStack } from '@chakra-ui/react';
|
import { Button, Stack, Text, VStack } from '@chakra-ui/react';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { DynamicIcon } from '~/elements';
|
import { DynamicIcon } from '~/elements';
|
||||||
import { useStrf, useColorValue, useDNSQuery, useFormState } from '~/hooks';
|
import { useStrf, useDNSQuery, useFormState } from '~/hooks';
|
||||||
|
|
||||||
import type { DnsOverHttps } from '~/types';
|
import type { DnsOverHttps } from '~/types';
|
||||||
|
|
||||||
|
|
@ -28,8 +28,8 @@ export const ResolvedTarget = (props: ResolvedTargetProps): JSX.Element => {
|
||||||
const displayTarget = useFormState(s => s.target.display);
|
const displayTarget = useFormState(s => s.target.display);
|
||||||
const setFormValue = useFormState(s => s.setFormValue);
|
const setFormValue = useFormState(s => s.setFormValue);
|
||||||
|
|
||||||
const color = useColorValue('secondary.500', 'secondary.300');
|
const color = 'secondary.500';
|
||||||
const errorColor = useColorValue('red.500', 'red.300');
|
const errorColor = 'red.500';
|
||||||
|
|
||||||
const tooltip4 = strF(web.text.fqdnTooltip, { protocol: 'IPv4' });
|
const tooltip4 = strF(web.text.fqdnTooltip, { protocol: 'IPv4' });
|
||||||
const tooltip6 = strF(web.text.fqdnTooltip, { protocol: 'IPv6' });
|
const tooltip6 = strF(web.text.fqdnTooltip, { protocol: 'IPv6' });
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export const CopyButton = (props: CopyButtonProps): JSX.Element => {
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={onCopy}
|
onClick={onCopy}
|
||||||
colorScheme="secondary"
|
colorScheme="primary"
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<DynamicIcon icon={{ fi: hasCopied ? 'FiCheck' : 'FiCopy' }} boxSize="16px" />
|
<DynamicIcon icon={{ fi: hasCopied ? 'FiCheck' : 'FiCopy' }} boxSize="16px" />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
import { AccordionIcon, Box, HStack, Spinner, Text, Tooltip } from '@chakra-ui/react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { AccordionIcon, Box, Spinner, HStack, Text, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { DynamicIcon } from '~/elements';
|
import { DynamicIcon } from '~/elements';
|
||||||
import { useColorValue, useOpposingColor, useStrf } from '~/hooks';
|
import { useOpposingColor, useStrf } from '~/hooks';
|
||||||
|
|
||||||
import type { ErrorLevels } from '~/types';
|
import type { ErrorLevels } from '~/types';
|
||||||
|
|
||||||
|
|
@ -26,9 +26,9 @@ const runtimeText = (runtime: number, text: string): string => {
|
||||||
export const ResultHeader = (props: ResultHeaderProps): JSX.Element => {
|
export const ResultHeader = (props: ResultHeaderProps): JSX.Element => {
|
||||||
const { title, loading, isError, errorMsg, errorLevel, runtime } = props;
|
const { title, loading, isError, errorMsg, errorLevel, runtime } = props;
|
||||||
|
|
||||||
const status = useColorValue('primary.500', 'primary.300');
|
const status = 'primary.500';
|
||||||
const warning = useColorValue(`${errorLevel}.500`, `${errorLevel}.300`);
|
const warning = `${errorLevel}.500`;
|
||||||
const defaultStatus = useColorValue('success.500', 'success.300');
|
const defaultStatus = 'success.500';
|
||||||
|
|
||||||
const { web } = useConfig();
|
const { web } = useConfig();
|
||||||
const strF = useStrf();
|
const strF = useStrf();
|
||||||
|
|
@ -52,7 +52,7 @@ export const ResultHeader = (props: ResultHeaderProps): JSX.Element => {
|
||||||
<Spinner size="sm" mr={4} color={status} />
|
<Spinner size="sm" mr={4} color={status} />
|
||||||
) : (
|
) : (
|
||||||
<DynamicIcon
|
<DynamicIcon
|
||||||
icon={isError ? { bi: 'BisError' } : { fa: 'FaCheckCircle' }}
|
icon={isError ? { bi: 'BiError' } : { fa: 'FaCheckCircle' }}
|
||||||
color={isError ? warning : defaultStatus}
|
color={isError ? warning : defaultStatus}
|
||||||
mr={4}
|
mr={4}
|
||||||
boxSize="100%"
|
boxSize="100%"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import startCase from 'lodash/startCase';
|
import startCase from 'lodash/startCase';
|
||||||
import { forwardRef, memo, useEffect, useMemo } from 'react';
|
import { forwardRef, memo, useEffect, useMemo, useState } from 'react';
|
||||||
import isEqual from 'react-fast-compare';
|
import isEqual from 'react-fast-compare';
|
||||||
import { Else, If, Then } from 'react-if';
|
import { Else, If, Then } from 'react-if';
|
||||||
import { BGPTable, Path, TextOutput } from '~/components';
|
import { BGPTable, Path, TextOutput } from '~/components';
|
||||||
|
|
@ -48,6 +48,7 @@ const AccordionHeaderWrapper = chakra('div', {
|
||||||
baseStyle: {
|
baseStyle: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
bg: 'white',
|
||||||
_hover: { bg: 'blackAlpha.50' },
|
_hover: { bg: 'blackAlpha.50' },
|
||||||
_focus: { boxShadow: 'outline' },
|
_focus: { boxShadow: 'outline' },
|
||||||
},
|
},
|
||||||
|
|
@ -72,24 +73,44 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
|
||||||
|
|
||||||
const addResponse = useFormState(s => s.addResponse);
|
const addResponse = useFormState(s => s.addResponse);
|
||||||
const form = useFormState(s => s.form);
|
const form = useFormState(s => s.form);
|
||||||
|
const [errorLevel, _setErrorLevel] = useState<ErrorLevels>('error');
|
||||||
|
|
||||||
const { data, error, isError, isLoading, refetch, isFetchedAfterMount } = useLGQuery(
|
const setErrorLevel = (level: ResponseLevel): void => {
|
||||||
{
|
let e: ErrorLevels = 'error';
|
||||||
queryLocation,
|
switch (level) {
|
||||||
queryTarget: form.queryTarget,
|
case 'success':
|
||||||
queryType: form.queryType,
|
e = level;
|
||||||
},
|
break;
|
||||||
|
case 'warning' || 'error':
|
||||||
|
e = 'warning';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_setErrorLevel(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error, isLoading, refetch, isFetchedAfterMount } = useLGQuery(
|
||||||
|
{ queryLocation, queryTarget: form.queryTarget, queryType: form.queryType },
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
if (device !== null) {
|
if (device !== null) {
|
||||||
addResponse(device.id, data);
|
addResponse(device.id, data);
|
||||||
}
|
}
|
||||||
|
if (isLGOutputOrError(data)) {
|
||||||
|
console.error(data);
|
||||||
|
setErrorLevel(data.level);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error(error);
|
console.error({ error });
|
||||||
|
if (isLGOutputOrError(error)) {
|
||||||
|
setErrorLevel(error.level);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isError = useMemo(() => isLGOutputOrError(data), [data, error]);
|
||||||
|
|
||||||
const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [data, isFetchedAfterMount]);
|
const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [data, isFetchedAfterMount]);
|
||||||
|
|
||||||
const strF = useStrf();
|
const strF = useStrf();
|
||||||
|
|
@ -123,23 +144,6 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
|
||||||
return messages.general;
|
return messages.general;
|
||||||
}, [error, data, messages.general, messages.requestTimeout]);
|
}, [error, data, messages.general, messages.requestTimeout]);
|
||||||
|
|
||||||
const errorLevel = useMemo<ErrorLevels>(() => {
|
|
||||||
const statusMap = {
|
|
||||||
success: 'success',
|
|
||||||
warning: 'warning',
|
|
||||||
error: 'warning',
|
|
||||||
danger: 'error',
|
|
||||||
} as { [K in ResponseLevel]: 'success' | 'warning' | 'error' };
|
|
||||||
|
|
||||||
let e: ErrorLevels = 'error';
|
|
||||||
|
|
||||||
if (isLGError(error)) {
|
|
||||||
const idx = error.level as ResponseLevel;
|
|
||||||
e = statusMap[idx];
|
|
||||||
}
|
|
||||||
return e;
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
const tableComponent = useMemo<boolean>(() => {
|
const tableComponent = useMemo<boolean>(() => {
|
||||||
let result = false;
|
let result = false;
|
||||||
if (data?.format === 'application/json') {
|
if (data?.format === 'application/json') {
|
||||||
|
|
@ -220,6 +224,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
|
||||||
</AccordionHeaderWrapper>
|
</AccordionHeaderWrapper>
|
||||||
<AccordionPanel
|
<AccordionPanel
|
||||||
pb={4}
|
pb={4}
|
||||||
|
bg="white"
|
||||||
overflowX="auto"
|
overflowX="auto"
|
||||||
css={{
|
css={{
|
||||||
WebkitOverflowScrolling: 'touch',
|
WebkitOverflowScrolling: 'touch',
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const _RequeryButton: React.ForwardRefRenderFunction<HTMLButtonElement, RequeryB
|
||||||
zIndex="1"
|
zIndex="1"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={requery as Get<RequeryButtonProps, 'onClick'>}
|
onClick={requery as Get<RequeryButtonProps, 'onClick'>}
|
||||||
colorScheme="secondary"
|
colorScheme="primary"
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<DynamicIcon icon={{ fi: 'FiRepeat' }} boxSize="16px" />
|
<DynamicIcon icon={{ fi: 'FiRepeat' }} boxSize="16px" />
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export const Option = <Opt extends SingleOption, IsMulti extends boolean>(
|
||||||
display={{ base: 'flex', lg: 'inline-flex' }}
|
display={{ base: 'flex', lg: 'inline-flex' }}
|
||||||
>
|
>
|
||||||
{tags.map(tag => (
|
{tags.map(tag => (
|
||||||
<Badge fontSize="xs" variant="subtle" key={tag} colorScheme="gray" textTransform="none">
|
<Badge fontSize="xs" variant="subtle" key={tag} colorScheme="primary" textTransform="none">
|
||||||
{tag}
|
{tag}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue