Compare commits

..

45 commits
v2.0.0 ... main

Author SHA1 Message Date
8d21da01a1
Fix 6WIND ping directive 2024-11-15 20:01:04 +00:00
ee7d8752f8 Add Witine customisations (#2)
Reviewed-on: #2
2024-11-15 18:40:53 +00:00
18e0b3e7e7 Add 6WIND directives (#1)
Reviewed-on: #1
2024-11-15 17:34:29 +00:00
thatmattlove
0b2fbb1b4d Update changelog with fix from #264 2024-07-01 00:08:01 -04:00
Matt Love
c2142ee76f
Merge pull request #264 from openxbr/main
Fix for IPv6 traceroute for juniper devices
2024-07-01 00:06:15 -04:00
thatmattlove
ec3c55aa81 prepare v2.0.4 2024-07-01 00:01:03 -04:00
thatmattlove
a5bc1ca8a0 fix formatting 2024-06-30 23:55:59 -04:00
thatmattlove
0f52bc438f closes #268: fix issue with mikrotik commands 2024-06-30 23:55:05 -04:00
thatmattlove
e0751311ba remove unnecessary logging 2024-06-30 23:54:37 -04:00
thatmattlove
f340e65082 fix BGP route validation 2024-06-30 23:33:06 -04:00
thatmattlove
872c3ec654 Closes #269: fix RPKI docs 2024-06-30 23:26:52 -04:00
thatmattlove
41248231ae fix logging issues 2024-06-30 23:22:46 -04:00
thatmattlove
b617df24d1 fixes #267: fix response caching 2024-06-30 22:22:44 -04:00
thatmattlove
3b0abd5ba8 don't cache errors 2024-06-30 21:34:42 -04:00
thatmattlove
0fd5bc7997 fix incorrectly named ui filename 2024-06-30 21:16:21 -04:00
Renato Ornelas
e8997b981c Fix for IPv6 traceroute for juniper devices 2024-06-19 15:11:22 -03:00
thatmattlove
bbba29546c fix biome ignore 2024-06-16 17:20:14 -04:00
thatmattlove
7eb8d8e925 fix formatting 2024-06-16 17:18:58 -04:00
thatmattlove
4733dd1893 prepare v2.0.3 2024-06-16 17:17:13 -04:00
thatmattlove
08fd310b44 fix issue where pattern rules failed validation 2024-06-16 17:12:54 -04:00
thatmattlove
6d06b9809d (possibly) fix log width issue 2024-06-16 16:59:22 -04:00
thatmattlove
30fda91bc8 closes #262: fix mikrotik error 2024-06-16 16:54:14 -04:00
thatmattlove
e19dd675e5 fix issue where results accordion did not re-open 2024-06-16 16:53:46 -04:00
thatmattlove
8a9766b99f formatting cleanup 2024-06-16 16:53:30 -04:00
thatmattlove
4d5e259f6e fix non-success display issue 2024-06-16 16:53:14 -04:00
thatmattlove
f324d34323 housekeeping 2024-06-01 23:23:08 -04:00
thatmattlove
3d82a21ba4 fix changelog link 2024-06-01 15:55:07 -04:00
thatmattlove
0137b042dc prepare v2.0.2 2024-06-01 15:44:49 -04:00
thatmattlove
6b37ce96f6 fix logo alignment on small screens; closes #258 2024-06-01 15:40:49 -04:00
thatmattlove
aab4ada723 add support for any DoH provider; closes #254; closes #256 2024-06-01 15:25:41 -04:00
thatmattlove
bfcae89bf0 fix prepending of HYPERGLASS_APP_PATH to values; closes #253 2024-06-01 15:08:59 -04:00
thatmattlove
8a3d704eca fix broken license link 2024-06-01 14:50:00 -04:00
thatmattlove
b796149a26 fix dropdown remaining open after selection; closes #257 2024-06-01 14:49:05 -04:00
thatmattlove
0c643c6abd update upgrade docs & version updating script 2024-06-01 11:08:03 -04:00
thatmattlove
7eb4f5ca93 fix leftover issue from #247 if build_dir does not exist 2024-05-31 23:11:08 -04:00
thatmattlove
35d9c26eff update version to 2.0.1 2024-05-31 23:06:33 -04:00
thatmattlove
1d1dcd8319 fix logo width on mobile 2024-05-31 23:00:51 -04:00
thatmattlove
c79ba8b727 fix browser DNS resolution; closes #251 2024-05-31 22:56:55 -04:00
thatmattlove
c8a348ed0f fix invalid next.config.js options 2024-05-31 22:56:26 -04:00
thatmattlove
4b6e6cba70 fix config value overwrite; closes #249 2024-05-31 22:24:41 -04:00
thatmattlove
ad88d025c2 fix leftover issue from #247 if build_dir does not exist 2024-05-31 22:22:39 -04:00
Matt Love
72887e0f9a
Merge pull request #247 from maluueu/244-build-ui-error
hyperglass: frontend: delete build dir before copying generated code to it
2024-05-31 22:14:25 -04:00
Marlon Alkan
85fa678d74
hyperglass: frontend: delete build dir before copying generated code to it
fixes #244
2024-05-29 18:03:06 +02:00
thatmattlove
74a6ee3ab8 fix docs opengraph tags 2024-05-28 21:35:58 -04:00
thatmattlove
556dccf509 update docs backend
fixes issue with OpenGraph tags
2024-05-28 21:30:33 -04:00
119 changed files with 3865 additions and 2953 deletions

View file

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

View 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)?

View file

@ -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) -->

View 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

View file

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

View file

@ -1 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Contributing Policy
url: https://github.com/thatmattlove/hyperglass/blob/main/CONTRIBUTING.md
about: Please read through the contributing policy before opening an issue or pull request.

View file

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

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
# Project
hyperglass/hyperglass/static
static/
TODO*
.env

View file

@ -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).
## 2.0.4 - 2024-06-30
### Fixed
- [#264](https://github.com/thatmattlove/hyperglass/issues/264): Fixed issue where IPv6 traceroutes fail on Juniper devices due to `traceroute: wait must be >1 sec.` error. Thanks @renatoornelas!
- [#267](https://github.com/thatmattlove/hyperglass/issues/267): Fixed issue where responses were incorrectly cached, resulting in no data being shown in the AS Path viewer.
- [#268](https://github.com/thatmattlove/hyperglass/issues/268): Fixed issue where some Mikrotik commands failed to execute properly.
- [#269](https://github.com/thatmattlove/hyperglass/issues/269): Updated documentation regarding `structured.rpki.mode`.
- Removed unnecessary logging statements which caused logging errors.
- Fixed issue where validation of structured BGP route data may have failed under certain conditions.
### Changed
- Error responses are no longer cached.
## 2.0.3 - 2024-06-16
### Fixed
- [#262](https://github.com/thatmattlove/hyperglass/issues/262): Fix issue where Mikrotik output was improperly parsed and displayed an error as a result.
- Fixed issue where incorrect error styles were displayed.
- Fixed issue where 'results' accordion component did not re-open when closed.
- Fixed issue where pattern-based directive rules failed validation.
### Changed
- Set default logo width (back) to 50%, adjusted how the `web.logo.width` setting is handled in the UI.
## 2.0.2 - 2024-06-01
### Fixed
- [#257](https://github.com/thatmattlove/hyperglass/issues/257): Fix issue where if `web.location_display_mode` is set to `dropdown` (automatically or otherwise), the menu would remain open but become detached from the main element because the Query Type element came into view.
- [#253](https://github.com/thatmattlove/hyperglass/issues/253): _Actually_ fix issue where configuration values were improperly prepended with the `HYPERGLASS_APP_PATH` value.
- [#258](https://github.com/thatmattlove/hyperglass/issues/258): Center logo alignment on small screens.
- Fix broken license link in default credit menu.
### Added
- Added license to docs.
- [#254](https://github.com/thatmattlove/hyperglass/issues/254): Users may specify their own DNS over HTTPS provider if desired.
## 2.0.1 - 2024-05-31
### Fixed
- [#244](https://github.com/thatmattlove/hyperglass/issues/244): Fix issue with UI build where UI build directory already existed and therefore could not be created.
- [#249](https://github.com/thatmattlove/hyperglass/issues/249): Fix issue where configuration values were improperly prepended with the `HYPERGLASS_APP_PATH` value.
- [#251](https://github.com/thatmattlove/hyperglass/issues/251): Fix issue where browser-based DNS resolution did not show, causing FQDN queries to fail due to validation.
- Fix issue where logo was improperly sized on small screens.
## 2.0.0 - 2024-05-28
_v2.0.0 is a major release of hyperglass. Many things have changed, and it is likely best to redeploy hyperglass in a new environment to migrate to v2._

View file

@ -1,29 +1,18 @@
hyperglass is primarily maintained by me, [Matt Love](https://github.com/thatmattlove). This is my first ever open source application, and as such, it's kind of my "baby". When I first started writing hyperglass, I knew _nothing_ about development, Python, Javascript, or Github. I was a network engineer trying to solve a problem and learn a few things while I was at it.
hyperglass is primarily maintained by me, [Matt Love](https://github.com/thatmattlove). This was my first ever open source application, and as such, it's kind of my "baby". When I first started writing hyperglass, I knew _nothing_ about development, Python, JavaScript/TypeScript, or GitHub. I was a network engineer trying to solve a problem and learn a few things while I was at it.
Because I've been solo-maintaining and building hyperglass since around April 2019, I've become pretty particular about things that might seem trivial to someone just trying to help out. While I **absolutely welcome development contributions**, please don't be offended if pull requests are denied, or if I request things to be done a certain way. To help understand why, here are some of the development design goals for hyperglass:
Because I've been solo-maintaining and building hyperglass since around April 2019, I've become pretty particular about things that might seem trivial to someone just trying to help out. While I welcome development contributions, please don't be offended if pull requests are denied, if I request things to be done a certain way, or if I integrate something similar to your changes separately from your PR. To help understand why, here are some of the development design goals for hyperglass:
- **Pristine code quality**
- [Black](https://github.com/python/black) formatting for Python
- Strict adherence to ESLint/Prettier configs for Javascript/React
- _ZERO_ linting errors
- [Black](https://github.com/python/black) formatting for Python.
- Strict adherence to ESLint/Prettier configs for frontend code.
- _ZERO_ linting errors.
- Linting exceptions only used when there is _no other way_, and should be accompanied with comments about why there is no other way.
- **No hard-coding**
- Anything visible to the end-user _must_ be customizable by the administrator. If it's not, or can't be, leave code or PR comments as to why.
- This includes things like timeouts, error messages, etc.
- **Mobile & Accessible**
- All UI element must be available on both desktop and mobile devices
- UI must achieve a 100 Lighthouse/PageInsights score for accessibility
- All UI element must be available on both desktop and mobile devices.
- UI must achieve a 100 Lighthouse/PageInsights score for accessibility.
- **IPv6 Support**
- Any new device support must include IPv6 commands
- All frontend and backend code must support IPv6, both for running the application and processing queries
## Branches
The following are the primary branches used for development and release management:
| Branch Name | Function |
| :---------- | :--------------------- |
| `main` | Tagged Stable Releases |
| `develop` | Ongoing Development |
Pull requests should be made against the `develop` branch.
- Any new device support must include IPv6 commands.
- All frontend and backend code must support IPv6, both for running the application and processing queries.

View file

@ -1,6 +1,6 @@
The Clear BSD License
Copyright (c) 2021 Matthew Love
Copyright (c) 2024 Matthew Love
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -22,7 +22,7 @@ hyperglass is intended to make implementing a looking glass too easy not to do,
</div>
### [Changelog](https://github.com/thatmattlove/hyperglass/blob/v2.0.0/CHANGELOG.md)
### [Changelog](https://hyperglass.dev/changelog)
## Features

View file

@ -1,47 +1,48 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"organizeImports": {
"enabled": true
},
"files": {
"ignore": [
"node_modules",
"dist",
".next/",
"favicon-formats.ts",
"custom.*[js, html]",
"hyperglass.json"
]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noUselessTypeConstraint": "off",
"noBannedTypes": "off"
},
"style": {
"noInferrableTypes": "off",
"noNonNullAssertion": "off"
},
"correctness": {
"useExhaustiveDependencies": "off"
}
}
},
"formatter": {
"indentStyle": "space",
"lineWidth": 100,
"indentWidth": 2
},
"javascript": {
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"organizeImports": {
"enabled": true
},
"files": {
"ignore": [
"node_modules",
"dist",
".next/",
"out/",
"favicon-formats.ts",
"custom.*[js, html]",
"hyperglass.json"
]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noUselessTypeConstraint": "off",
"noBannedTypes": "off"
},
"style": {
"noInferrableTypes": "off",
"noNonNullAssertion": "off"
},
"correctness": {
"useExhaustiveDependencies": "off"
}
}
},
"formatter": {
"quoteStyle": "single",
"bracketSpacing": true,
"semicolons": "always",
"arrowParentheses": "asNeeded",
"trailingComma": "all"
"indentStyle": "space",
"lineWidth": 100,
"indentWidth": 2
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"bracketSpacing": true,
"semicolons": "always",
"arrowParentheses": "asNeeded",
"trailingComma": "all"
}
}
}
}

View file

@ -1,80 +1,80 @@
interface Favicon {
rel: string | null;
dimensions: [number, number];
image_format: string;
prefix: string;
rel: string | null;
dimensions: [number, number];
image_format: string;
prefix: string;
}
export default [
{ dimensions: [64, 64], image_format: 'ico', prefix: 'favicon', rel: null },
{ dimensions: [16, 16], image_format: 'png', prefix: 'favicon', rel: 'icon' },
{ dimensions: [32, 32], image_format: 'png', prefix: 'favicon', rel: 'icon' },
{ dimensions: [64, 64], image_format: 'png', prefix: 'favicon', rel: 'icon' },
{ dimensions: [96, 96], image_format: 'png', prefix: 'favicon', rel: 'icon' },
{ dimensions: [180, 180], image_format: 'png', prefix: 'favicon', rel: 'icon' },
{
dimensions: [57, 57],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{
dimensions: [60, 60],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{
dimensions: [72, 72],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{
dimensions: [76, 76],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{
dimensions: [114, 114],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{
dimensions: [120, 120],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{
dimensions: [144, 144],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{
dimensions: [152, 152],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{
dimensions: [167, 167],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{
dimensions: [180, 180],
image_format: 'png',
prefix: 'apple-touch-icon',
rel: 'apple-touch-icon',
},
{ dimensions: [70, 70], image_format: 'png', prefix: 'mstile', rel: null },
{ dimensions: [270, 270], image_format: 'png', prefix: 'mstile', rel: null },
{ dimensions: [310, 310], image_format: 'png', prefix: 'mstile', rel: null },
{ dimensions: [310, 150], image_format: 'png', prefix: 'mstile', rel: null },
{ dimensions: [196, 196], image_format: 'png', prefix: 'favicon', rel: 'shortcut icon' },
{ dimensions: [48, 48], image_format: "ico", prefix: "favicon", rel: null },
{ dimensions: [16, 16], image_format: "png", prefix: "favicon", rel: "icon" },
{ dimensions: [32, 32], image_format: "png", prefix: "favicon", rel: "icon" },
{ dimensions: [64, 64], image_format: "png", prefix: "favicon", rel: "icon" },
{ dimensions: [96, 96], image_format: "png", prefix: "favicon", rel: "icon" },
{ dimensions: [180, 180], image_format: "png", prefix: "favicon", rel: "icon" },
{
dimensions: [57, 57],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{
dimensions: [60, 60],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{
dimensions: [72, 72],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{
dimensions: [76, 76],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{
dimensions: [114, 114],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{
dimensions: [120, 120],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{
dimensions: [144, 144],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{
dimensions: [152, 152],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{
dimensions: [167, 167],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{
dimensions: [180, 180],
image_format: "png",
prefix: "apple-touch-icon",
rel: "apple-touch-icon",
},
{ dimensions: [70, 70], image_format: "png", prefix: "mstile", rel: null },
{ dimensions: [270, 270], image_format: "png", prefix: "mstile", rel: null },
{ dimensions: [310, 310], image_format: "png", prefix: "mstile", rel: null },
{ dimensions: [310, 150], image_format: "png", prefix: "mstile", rel: null },
{ dimensions: [196, 196], image_format: "png", prefix: "favicon", rel: "shortcut icon" },
] as Favicon[];

View file

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

View file

@ -12,8 +12,8 @@
"license": "BSD-3-Clause-Clear",
"dependencies": {
"next": "^14.1.1",
"nextra": "^2.13.4",
"nextra-theme-docs": "^2.13.4",
"nextra": "3.0.0-alpha.24",
"nextra-theme-docs": "3.0.0-alpha.24",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},

View file

@ -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
View 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,
},
};

View file

@ -1,7 +0,0 @@
{
"overview": "Overview",
"config": "Config File",
"devices": "Devices File",
"directives": "Directives File",
"examples": "Examples"
}

View file

@ -0,0 +1,7 @@
export default {
overview: "Overview",
config: "Config File",
devices: "Devices File",
directives: "Directives File",
examples: "Examples",
};

View file

@ -1,8 +0,0 @@
{
"api-docs": "API Docs",
"caching": "Caching",
"logging": "Logging & Webhooks",
"messages": "Messages",
"structured-output": "Structured Output",
"web-ui": "Web UI"
}

View 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",
};

View file

@ -14,7 +14,7 @@ Additionally, hyperglass provides the ability to control which BGP communities a
| 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.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}
structured:
rpki: router
rpki:
mode: router
```
#### Show RPKI State from a Public/External Perspective
```yaml filename="config.yaml" copy {2}
structured:
rpki: external
rpki:
mode: external
```
### Community Filtering Examples

View file

@ -1,4 +1,4 @@
import { Callout } from "nextra-theme-docs";
import { Callout } from "nextra/components";
import { Color } from "~/components/color";
## 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.
| Parameter | Type | Default Value | Description |
| :------------------ | :----- | :------------ | :-------------------------------------- |
| `dns_provider.name` | String | cloudflare | Cloudflare or Google DNS are supported. |
| Parameter | Type | Default Value | Description |
| :------------------ | :----- | :------------------------------------- | :-------------------------------------------------------------------------- |
| `dns_provider.name` | String | cloudflare | If `cloudflare` or `google` are provided, no URL is necessary. |
| `dns_provider.url` | String | `https://cloudflare-dns.com/dns-query` | Provide a custom DNS over HTTPS URL if you'd like to use your own resolver. |
### Logo

View file

@ -1,4 +1,4 @@
import { Callout } from "nextra-theme-docs";
import { Callout } from "nextra/components";
import { SupportedPlatforms } from "~/components/platforms";
import { DocsButton } from "~/components/docs-button";
@ -76,7 +76,10 @@ devices:
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.
@ -92,7 +95,10 @@ devices:
- 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.
@ -109,7 +115,10 @@ devices:
- 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.

View file

@ -1,5 +0,0 @@
{
"credentials": "Credentials",
"http-device": "HTTP Device",
"ssh-proxy": "SSH Proxy"
}

View file

@ -0,0 +1,5 @@
export default {
credentials: "Credentials",
"http-device": "HTTP Device",
"ssh-proxy": "SSH Proxy",
};

View file

@ -1,4 +1,4 @@
import { Callout } from "nextra-theme-docs";
import { Callout } from "nextra/components";
## What is a directive?

View file

@ -1,5 +0,0 @@
{
"basic-configuration": "Basic Configuration",
"add-your-own-command": "Add Your Own Command",
"customize-the-ui": "Customize the UI"
}

View 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",
};

View file

@ -3,7 +3,7 @@ title: Basic 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.

View file

@ -2,16 +2,22 @@
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"
site_title: Our super neat looking glass
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
web:
@ -20,7 +26,10 @@ web:
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}
web:
@ -30,7 +39,10 @@ web:
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
web:

View file

@ -1,5 +1,4 @@
import { Code, Table, Td, Th, Tr } from "nextra/components";
import { Callout } from "nextra-theme-docs";
import { Code, Table, Td, Th, Tr, Callout } from "nextra/components";
import { SupportedPlatforms } from "~/components/platforms";
Once you've gotten started with a basic configuration, you'll probably want to customize the look and feel of hyperglass by changing the logo or color scheme. Fortunately, there are _a lot_ ways to customize hyperglass.

View file

@ -3,12 +3,14 @@ title: Introduction
description: Get started with hyperglass
---
import { Cards, Card } from "nextra/components";
import { Cards } from "nextra/components";
import { SupportedPlatforms } from "~/components/platforms";
## 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.
@ -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
<Cards>
<Card title="Get Started" href="installation/docker" arrow />
<Cards.Card title="Get Started" href="installation/docker" arrow />
</Cards>

View file

@ -1,11 +1,11 @@
import { Cards, Card } from "nextra/components";
import { Cards } from "nextra/components";
<Cards>
<Card href="installation/docker" title="Using Docker" />
<Card href="installation/manual" title="Manual Installation" />
<Cards.Card href="installation/docker" title="Using Docker" />
<Cards.Card href="installation/manual" title="Manual Installation" />
</Cards>
<Cards>
<Card href="installation/environment-variables" title="Environment Variables" />
<Card href="installation/reverse-proxy" title="Reverse Proxy" />
<Cards.Card href="installation/environment-variables" title="Environment Variables" />
<Cards.Card href="installation/reverse-proxy" title="Reverse Proxy" />
</Cards>

View file

@ -1,7 +0,0 @@
{
"docker": "Using Docker",
"manual": "Manual Installation",
"environment-variables": "Environment Variables",
"reverse-proxy": "Reverse Proxy",
"upgrading": "Upgrading hyperglass"
}

View file

@ -0,0 +1,7 @@
export default {
docker: "Using Docker",
manual: "Manual Installation",
"environment-variables": "Environment Variables",
"reverse-proxy": "Reverse Proxy",
upgrading: "Upgrading hyperglass",
};

View file

@ -3,8 +3,8 @@ title: Using Docker
description: Installing hyperglass with Docker
---
import { Card, Cards, Steps } from "nextra/components";
import { Callout } from "nextra-theme-docs";
import { Cards, Steps, Callout } from "nextra/components";
// import { Callout } from "nextra-theme-docs";
<Callout type="info">**Docker is the recommended method for running hyperglass.**</Callout>
@ -13,7 +13,7 @@ import { Callout } from "nextra-theme-docs";
### Install Docker
<Cards>
<Card
<Cards.Card
title="Docker Engine Installation Guide"
href="https://docs.docker.com/engine/install/"
target="_blank"
@ -26,9 +26,8 @@ import { Callout } from "nextra-theme-docs";
```shell copy
mkdir /etc/hyperglass
cd /opt
git clone https://github.com/thatmattlove/hyperglass.git
git clone https://github.com/thatmattlove/hyperglass.git --depth=1
cd /opt/hyperglass
git checkout v2.0.0
```
### Optional: Quickstart

View file

@ -3,8 +3,7 @@ title: Manual Installation
description: Installing hyperglass manually
---
import { Steps } from "nextra/components";
import { Callout } from "nextra-theme-docs";
import { Steps, Callout } from "nextra/components";
<Steps>
@ -12,8 +11,8 @@ import { Callout } from "nextra-theme-docs";
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/)
2. [NodeJS 18.17 or later](https://nodejs.org/en/download)
1. [Python 3.11, or 3.12](https://www.python.org/downloads/) and [`pip`](https://pip.pypa.io/en/stable/installation/)
2. [NodeJS 20.14 or later](https://nodejs.org/en/download)
3. [PNPM 8 or later](https://pnpm.io/installation)
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:
```shell copy
pip3 install hyperglass
git clone https://github.com/thatmattlove/hyperglass --depth=1
cd hyperglass
pip3 install -e .
```
### 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.
```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
```
### Create a `systemd` service
```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
systemctl daemon-reload
systemctl enable hyperglass

View file

@ -3,15 +3,19 @@ title: Reverse Proxy
description: Setting up a reverse proxy for hyperglass
---
import { Cards, Card } from "nextra/components";
import { Callout } from "nextra-theme-docs";
import { Cards, Callout } from "nextra/components";
[Caddy](https://caddyserver.com) is recommended, but any reverse proxy ([NGINX](https://www.nginx.com), [Apache2](https://httpd.apache.org)) will work.
## Caddy
<Cards>
<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>
```shell copy

View file

@ -4,7 +4,8 @@
cd /opt/hyperglass
docker compose down
docker compose rm -f
git pull
git fetch
git checkout v2.0.4
docker compose build
docker compose up
```

20
docs/pages/license.mdx Normal file
View 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.

View file

@ -2,7 +2,7 @@
description: Platforms supported by hyperglass
---
import { Callout } from "nextra-theme-docs";
import { Callout } from "nextra/components";
import { PlatformTable } from "~/components/platforms";
hyperglass uses [Netmiko](https://github.com/ktbyers/netmiko) to interact with devices via SSH/telnet. [All platforms supported by Netmiko](https://github.com/ktbyers/netmiko/blob/develop/PLATFORMS.md) are supported by hyperglass.

View file

@ -121,7 +121,7 @@ class Redact(OutputPlugin):
If the query output was:
```
```text
Lorem ipsum dolor sit amet, SuperSecretInfo consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Viverra suspendisse potenti nullam ac. At elementum eu facilisis sed odio morbi. SuperSecretInfo iaculis urna id volutpat lacus.Nisl nisi
scelerisque eu ultrices vitae. Accumsan SuperSecretInfo tortor posuere ac ut consequat semper viverra nam libero. Libero id faucibus nisl
@ -133,7 +133,7 @@ diam in arcu cursus SuperSecretInfo.
The above plugin would transform the output to:
```
```text
Lorem ipsum dolor sit amet, <REDACTED> consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Viverra suspendisse potenti nullam ac. At elementum eu facilisis sed odio morbi. <REDACTED> iaculis urna id volutpat lacus.Nisl nisi
scelerisque eu ultrices vitae. Accumsan <REDACTED> tortor posuere ac ut consequat semper viverra nam libero. Libero id faucibus nisl

4130
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
import { useRouter } from "next/router";
import { type DocsThemeConfig, useConfig } from "nextra-theme-docs";
import "nextra-theme-docs/style.css";
import faviconFormats from "./favicon-formats";
import styles from "./global.module.css";
@ -71,49 +72,54 @@ const config: DocsThemeConfig = {
</svg>
</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: () => {
const { asPath } = useRouter();
const { frontMatter, title } = useConfig();
return {
titleTemplate: "%s | hyperglass",
title: frontMatter.title || title,
openGraph: {
type: "website",
url: `https://hyperglass.dev${asPath}`,
title: frontMatter.title || title,
description: frontMatter.description || "hyperglass Documentation",
images: [
{
url: "https://hyperglass.dev/opengraph.jpg",
width: 1200,
height: 630,
alt: "hyperglass",
},
],
},
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}` };
}),
};
return (
<head>
<title>{title}</title>
<meta property="og:url" content={url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://hyperglass.dev/opengraph.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:domain" content="hyperglass.dev" />
<meta name="twitter:url" content="https://hyperglass.dev" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content="https://hyperglass.dev/opengraph.jpg" />
<meta name="robots" content={index} />
<link rel="manifest" href="/img/manifest.json" />
{favicons.map((props) => (
<link key={props.href} {...props} />
))}
</head>
);
},
docsRepositoryBase: "https://github.com/thatmattlove/hyperglass/tree/main/docs",
banner: {
dismissible: true,
// text: "🎉 hyperglass 2.0 is here!",
text: "😬 hyperglass 2.0 and its documentation is still in development!",
content: "🎉 hyperglass 2.0 is here! This documentation is still in development, though.",
},
feedback: { content: null },
footer: { text: `© ${new Date().getFullYear()} hyperglass` },
footer: { content: `© ${new Date().getFullYear()} hyperglass` },
editLink: { component: null },
chat: {
link: "https://netdev.chat/",

View file

@ -1,26 +1,26 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "esnext",
"downlevelIteration": true,
"strict": true,
"baseUrl": ".",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"noEmit": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"~/*": ["./*"]
}
},
"compilerOptions": {
"target": "ESNext",
"module": "esnext",
"downlevelIteration": true,
"strict": true,
"baseUrl": ".",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"noEmit": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"~/*": ["./*"]
}
},
"exclude": ["node_modules", ".next"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js"]
"exclude": ["node_modules", ".next"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.mjs"]
}

View file

@ -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
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
@ -196,6 +189,11 @@ async def fake_output(query_type: str, structured: bool) -> t.Union[str, BGPRout
return TRACEROUTE
if "bgp" in query_type:
if structured:
return STRUCTURED
return BGPRouteTable(
vrf="default",
count=len(BGP_ROUTES),
routes=BGP_ROUTES,
winning_weight="high",
)
return BGP_PLAIN
return BGP_PLAIN

View file

@ -1,6 +1,7 @@
"""API Routes."""
# Standard Library
import json
import time
import typing as t
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)
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:
raw_output = str(output)

View file

@ -125,6 +125,7 @@ def init_ui_params(*, params: "Params", devices: "Devices") -> "UIParameters":
_ui_params = params.frontend()
_ui_params["web"]["logo"]["light_format"] = params.web.logo.light.suffix
_ui_params["web"]["logo"]["dark_format"] = params.web.logo.dark.suffix
_ui_params["web"]["logo"]["footer_format"] = params.web.logo.footer.suffix
return UIParameters(
**_ui_params,

View file

@ -4,7 +4,7 @@
from datetime import datetime
__name__ = "hyperglass"
__version__ = "2.0.0"
__version__ = "2.0.4"
__author__ = "Matt Love"
__copyright__ = f"Copyright {datetime.now().year} Matthew Love"
__license__ = "BSD 3-Clause Clear License"

View file

@ -2,7 +2,7 @@
CREDIT = """
Powered by [**hyperglass**](https://hyperglass.dev) version {version}. \
Source code licensed [_BSD 3-Clause Clear_](https://hyperglass.dev/docs/license/).
Source code licensed [_BSD 3-Clause Clear_](https://hyperglass.dev/license/).
"""
DEFAULT_TERMS = """

View file

@ -111,7 +111,7 @@ JuniperTraceroute = BuiltinDirective(
RuleWithIPv6(
condition="::/0",
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"),

View 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,
)

View file

@ -2,7 +2,7 @@
# Standard Library
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
from pydantic import ValidationError
@ -48,7 +48,7 @@ class HyperglassError(Exception):
return {
"message": self._message,
"level": self._level,
"keywords": self._keywords,
"keywords": self.keywords,
}
def json(self) -> str:
@ -76,6 +76,18 @@ class HyperglassError(Exception):
return "\n".join(errs)
def _process_keywords(self) -> None:
out: Set[str] = set()
for val in self._keywords:
if isinstance(val, str):
out.add(val)
elif isinstance(val, list):
for v in val:
out.add(v)
else:
out.add(str(val))
self._keywords = list(out)
@property
def message(self) -> str:
"""Return the instance's `message` attribute."""
@ -89,6 +101,7 @@ class HyperglassError(Exception):
@property
def keywords(self) -> List[str]:
"""Return the instance's `keywords` attribute."""
self._process_keywords()
return self._keywords
@property

View file

@ -29,12 +29,7 @@ netmiko_device_globals = {
"mikrotik_switchos": {"global_cmd_verify": False},
}
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$"},
}
netmiko_device_send_args = {}
class NetmikoConnection(SSHConnection):

View file

@ -47,7 +47,7 @@ async def execute(query: "Query") -> Union["OutputDataModel", str]:
"""Initiate query validation and execution."""
params = use_state("params")
output = params.messages.general
_log = log.bind(query=query, device=query.device)
_log = log.bind(query=query.summary(), device=query.device.id)
_log.debug("")
mapped_driver = map_driver(query.device.driver)
@ -67,7 +67,6 @@ async def execute(query: "Query") -> Union["OutputDataModel", str]:
response = await driver.collect()
output = await driver.response(response)
_log.bind(response=response).debug("Query response")
if is_series(output):
if len(output) == 0:

View file

@ -252,7 +252,6 @@ class BaseExternal:
except TypeError as err:
raise self._exception(f"Timeout must be an int, got: {str(timeout)}") from err
request["timeout"] = timeout
log.bind(request=request).debug("Constructed request parameters")
return request
async def _arequest( # noqa: C901

View file

@ -56,8 +56,6 @@ async def read_package_json() -> t.Dict[str, t.Any]:
except Exception as 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
@ -131,7 +129,9 @@ async def build_ui(app_path: Path):
log.error(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")
return "\n".join(all_messages)
@ -203,7 +203,7 @@ def migrate_images(app_path: Path, params: "UIParameters"):
src_files = ()
dst_files = ()
for image in ("light", "dark", "favicon"):
for image in ("light", "dark", "favicon", "footer"):
src: Path = getattr(params.web.logo, image)
dst = images_dir / f"{image + src.suffix}"
src_files += (src,)
@ -326,7 +326,6 @@ async def build_frontend( # noqa: C901
}
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
# build identifier.

View file

@ -46,6 +46,12 @@ _LOG_LEVELS = [
{"name": "CRITICAL", "color": "<r>"},
]
_EXCLUDE_MODULES = (
"PIL",
"svglib",
"paramiko.transport",
)
HyperglassConsole = Console(
theme=Theme(
{
@ -119,6 +125,9 @@ class LibInterceptHandler(logging.Handler):
def init_logger(level: t.Union[int, str] = logging.INFO):
"""Initialize hyperglass logging instance."""
for mod in _EXCLUDE_MODULES:
logging.getLogger(mod).propagate = False
# Reset built-in Loguru configurations.
_loguru_logger.remove()
@ -133,6 +142,7 @@ def init_logger(level: t.Union[int, str] = logging.INFO):
log_time_format="[%Y%m%d %H:%M:%S]",
),
format=formatter,
colorize=False,
level=level,
filter=filter_uvicorn_values,
enqueue=True,
@ -144,6 +154,7 @@ def init_logger(level: t.Union[int, str] = logging.INFO):
enqueue=True,
format=_FMT if level == logging.INFO else _FMT_DEBUG,
level=level,
colorize=False,
filter=filter_uvicorn_values,
)
@ -192,6 +203,7 @@ def enable_file_logging(
serialize=structured,
level=level,
encoding="utf8",
colorize=False,
rotation=max_size.human_readable(),
)
_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)),
format=_FMT_BASIC,
enqueue=True,
colorize=False,
)
_loguru_logger.bind(host=host, port=port).debug("Logging to syslog target")

View file

@ -162,6 +162,9 @@ def run(workers: int = None):
log.bind(
version=__version__,
listening=f"http://{Settings.bind()}",
app_path=f"{Settings.app_path.absolute()!s}",
container=Settings.container,
original_app_path=f"{Settings.original_app_path.absolute()!s}",
workers=_workers,
).info(
"Starting hyperglass",

View file

@ -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.",
)
org_name: str = Field(
"Beloved Hyperglass User",
"Beloved Looking Glass User",
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.",
)
site_title: str = Field(
"hyperglass",
"Looking Glass",
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.",
)
@ -131,8 +131,8 @@ class Params(ParamsPublic, HyperglassModel):
for menu in web.menus:
menu.content = menu.content.format(
site_title=info.data.get("site_title", "hyperglass"),
org_name=info.data.get("org_name", "hyperglass"),
site_title=info.data.get("site_title", "Looking Glass"),
org_name=info.data.get("org_name", "Looking Glass"),
version=__version__,
)
return web

View file

@ -86,10 +86,9 @@ class Logo(HyperglassModel):
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
width: str = Field(default="100%", pattern=PERCENTAGE_PATTERN)
# width: t.Optional[t.Union[int, Percentage]] = "100%"
footer: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
width: str = Field(default="50%", pattern=PERCENTAGE_PATTERN)
height: t.Optional[str] = Field(default=None, pattern=PERCENTAGE_PATTERN)
# height: t.Optional[t.Union[int, Percentage]] = None
class LogoPublic(Logo):
@ -97,13 +96,13 @@ class LogoPublic(Logo):
light_format: str
dark_format: str
footer_format: str
class Text(HyperglassModel):
"""Validation model for params.branding.text."""
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)
query_location: str = "Location"
query_type: str = "Query Type"
@ -134,7 +133,7 @@ class Text(HyperglassModel):
class ThemeColors(HyperglassModel):
"""Validation model for theme colors."""
black: Color = "#000000"
black: Color = "#2d3635"
white: Color = "#ffffff"
dark: Color = "#010101"
light: Color = "#f5f6f7"
@ -171,7 +170,7 @@ class ThemeColors(HyperglassModel):
class ThemeFonts(HyperglassModel):
"""Validation model for theme fonts."""
body: str = "Nunito"
body: str = "Inter"
mono: str = "Fira Code"
@ -186,13 +185,19 @@ class Theme(HyperglassModel):
class DnsOverHttps(HyperglassModel):
"""Validation model for DNS over HTTPS resolution."""
name: str = Field(default="cloudflare", pattern=DOH_PROVIDERS_PATTERN)
name: str = "cloudflare"
url: str = ""
@model_validator(mode="before")
def validate_dns(cls, data: "DnsOverHttps") -> t.Dict[str, str]:
"""Assign url field to model based on selected provider."""
name = data.get("name", "cloudflare")
url = data.get("url", DNS_OVER_HTTPS["cloudflare"])
if url not in DNS_OVER_HTTPS.values():
return {
"name": "custom",
"url": url,
}
url = DNS_OVER_HTTPS[name]
return {
"name": name,
@ -222,6 +227,7 @@ class HighlightPattern(HyperglassModel):
class Web(HyperglassModel):
"""Validation model for all web/browser-related configuration."""
copyright: t.Optional[str] = "All rights reserved"
credit: Credit = Credit()
dns_provider: DnsOverHttps = DnsOverHttps()
links: t.Sequence[Link] = [

View file

@ -6,7 +6,7 @@ import typing as t
from ipaddress import ip_network
# Third Party
from pydantic import field_validator
from pydantic import field_validator, ValidationInfo
# Project
from hyperglass.state import use_state
@ -70,7 +70,7 @@ class BGPRoute(HyperglassModel):
return [c for c in value if func(c)]
@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."""
(structured := use_state("params").structured)
@ -82,7 +82,7 @@ class BGPRoute(HyperglassModel):
if structured.rpki.mode == "external":
# If external validation is enabled, validate the prefix
# & asn with Cloudflare's RPKI API.
as_path = values["as_path"]
as_path = info.data.get("as_path", [])
if len(as_path) == 0:
# If the AS_PATH length is 0, i.e. for an internal route,
@ -92,13 +92,13 @@ class BGPRoute(HyperglassModel):
asn = as_path[-1]
try:
net = ip_network(values["prefix"])
net = ip_network(info.data["prefix"])
except ValueError:
return 3
# Only do external RPKI lookups for global prefixes.
if net.is_global:
return rpki_state(prefix=values["prefix"], asn=asn)
return rpki_state(prefix=info.data["prefix"], asn=asn)
return value

View file

@ -216,7 +216,7 @@ class RuleWithPattern(Rule):
return InputValidationError(target=value, error="Denied")
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):
if isinstance(result, BaseException):
self._passed = False
@ -227,9 +227,6 @@ class RuleWithPattern(Rule):
self._passed = 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)
if isinstance(result, BaseException):

View file

@ -59,15 +59,23 @@ class HyperglassModel(BaseModel):
if isinstance(value, Path):
if Settings.container:
return Settings.default_app_path.joinpath(
*(p for p in value.parts if p not in Settings.original_app_path.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:
path = Path(value)
return str(
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
)
)
)

View file

@ -77,8 +77,6 @@ def parse_juniper(output: Sequence[str]) -> "OutputDataModel": # noqa: C901
parsed: "OrderedDict" = xmltodict.parse(
cleaned, force_list=("rt", "rt-entry", "community")
)
_log.debug("Pre-parsed data", data=parsed)
if "rpc-reply" in parsed.keys():
if "xnm:error" in parsed["rpc-reply"]:
if "message" in parsed["rpc-reply"]["xnm:error"]:

View file

@ -37,45 +37,46 @@ class MikrotikGarbageOutput(OutputPlugin):
result = ()
for each_output in output:
if each_output.split()[-1] in ("DISTANCE", "STATUS"):
# Mikrotik shows the columns with no rows if there is no data.
# Rather than send back an empty table, send back an empty
# response which is handled with a warning message.
each_output = ""
else:
remove_lines = ()
all_lines = each_output.splitlines()
# Starting index for rows (after the column row).
start = 1
# Extract the column row.
column_line = " ".join(all_lines[0].split())
if len(each_output) != 0:
if each_output.split()[-1] in ("DISTANCE", "STATUS"):
# Mikrotik shows the columns with no rows if there is no data.
# Rather than send back an empty table, send back an empty
# response which is handled with a warning message.
each_output = ""
else:
remove_lines = ()
all_lines = each_output.splitlines()
# Starting index for rows (after the column row).
start = 1
# Extract the column row.
column_line = " ".join(all_lines[0].split())
for i, line in enumerate(all_lines[1:]):
# Remove all the newline characters (which differ line to
# line) for comparison purposes.
normalized = " ".join(line.split())
for i, line in enumerate(all_lines[1:]):
# Remove all the newline characters (which differ line to
# line) for comparison purposes.
normalized = " ".join(line.split())
# Remove ansii characters that aren't caught by Netmiko.
normalized = re.sub(r"\\x1b\[\S{2}\s", "", normalized)
# Remove ansii characters that aren't caught by Netmiko.
normalized = re.sub(r"\\x1b\[\S{2}\s", "", normalized)
if column_line in normalized:
# Mikrotik often re-inserts the column row in the output,
# effectively 'starting over'. In that case, re-assign
# the column row and starting index to that point.
column_line = re.sub(r"\[\S{2}\s", "", line)
start = i + 2
if column_line in normalized:
# Mikrotik often re-inserts the column row in the output,
# effectively 'starting over'. In that case, re-assign
# the column row and starting index to that point.
column_line = re.sub(r"\[\S{2}\s", "", line)
start = i + 2
if "[Q quit|D dump|C-z pause]" in normalized:
# Remove Mikrotik's unhelpful helpers from the output.
remove_lines += (i + 1,)
if "[Q quit|D dump|C-z pause]" in normalized:
# Remove Mikrotik's unhelpful helpers from the output.
remove_lines += (i + 1,)
# Combine the column row and the data rows from the starting
# index onward.
lines = [column_line, *all_lines[start:]]
# Combine the column row and the data rows from the starting
# index onward.
lines = [column_line, *all_lines[start:]]
# Remove any lines marked for removal and re-join with a single
# newline character.
lines = [line for idx, line in enumerate(lines) if idx not in remove_lines]
result += ("\n".join(lines),)
# Remove any lines marked for removal and re-join with a single
# newline character.
lines = [line for idx, line in enumerate(lines) if idx not in remove_lines]
result += ("\n".join(lines),)
return result

View file

@ -162,7 +162,7 @@ class InputPluginManager(PluginManager[InputPlugin], type="input"):
"""Execute all input transformation plugins."""
result = query.query_target
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")
return result

View file

@ -2,13 +2,12 @@ import { useMemo } from 'react';
import { Button, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import { useConfig } from '~/context';
import { Markdown } from '~/elements';
import { useColorValue, useBreakpointValue, useOpposingColor, useStrf } from '~/hooks';
import { useStrf } from '~/hooks';
import type { MenuListProps } from '@chakra-ui/react';
import type { Config } from '~/types';
interface FooterButtonProps extends Omit<MenuListProps, 'title'> {
side: 'left' | 'right';
title?: MenuListProps['children'];
content: string;
}
@ -27,26 +26,26 @@ function getConfigFmt(config: Config): Record<string, string> {
}
export const FooterButton = (props: FooterButtonProps): JSX.Element => {
const { content, title, side, ...rest } = props;
const { content, title, ...rest } = props;
const config = useConfig();
const strF = useStrf();
const fmt = useMemo(() => getConfigFmt(config), [config]);
const fmtContent = useMemo(() => strF(content, fmt), [fmt, content, strF]);
const placement = side === 'left' ? 'top' : side === 'right' ? 'top-end' : undefined;
const bg = useColorValue('white', 'gray.900');
const color = useOpposingColor(bg);
const size = useBreakpointValue({ base: 'xs', lg: 'sm' });
return (
<Menu placement={placement} preventOverflow isLazy>
<Menu
placement="top"
preventOverflow
isLazy
>
<MenuButton
zIndex={2}
py={1}
fontWeight="normal"
as={Button}
size={size}
variant="ghost"
lineHeight={0}
variant="link"
colorScheme="transparent"
aria-label={typeof title === 'string' ? title : undefined}
>
{title}
@ -54,10 +53,10 @@ export const FooterButton = (props: FooterButtonProps): JSX.Element => {
<MenuList
px={6}
py={4}
bg={bg}
bg="white"
// Ensure the height doesn't overtake the viewport, especially on mobile. See overflow also.
maxH="50vh"
color={color}
color="black"
boxShadow="2xl"
textAlign="left"
overflowY="auto"

View file

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

View file

@ -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 { useConfig } from '~/context';
import { DynamicIcon } from '~/elements';
import { useBreakpointValue, useColorValue, useMobile } from '~/hooks';
import { DynamicIcon, Markdown } from '~/elements';
import { useMobile } from '~/hooks';
import { isLink, isMenu } from '~/types';
import { FooterButton } from './button';
import { ColorModeToggle } from './color-mode';
import { FooterLink } from './link';
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
import type { Link, Menu } from '~/types';
import { Logo } from './logo';
type MenuItems = (Link | Menu)[];
@ -24,7 +24,7 @@ function buildItems(links: Link[], menus: Menu[]): [MenuItems, MenuItems] {
return [left, right];
}
const LinkOnSide = (props: { item: ArrayElement<MenuItems>; side: 'left' | 'right' }) => {
const NavLink = (props: { item: ArrayElement<MenuItems> }) => {
const { item, side } = props;
if (isLink(item)) {
const icon: Partial<ButtonProps & LinkProps> = {};
@ -40,49 +40,55 @@ const LinkOnSide = (props: { item: ArrayElement<MenuItems>; side: 'left' | 'righ
};
export const Footer = (): JSX.Element => {
const { web, content } = 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 { web } = useConfig();
const [left, right] = useMemo(() => buildItems(web.links, web.menus), [web.links, web.menus]);
return (
<HStack
px={6}
py={4}
<Box
w="100%"
zIndex={1}
as="footer"
bg={footerBg}
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' }}
bg="#005e8a"
color="#ffffff"
fontSize=".945rem"
>
{left.map(item => (
<LinkOnSide key={item.title} item={item} side="left" />
))}
{!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />}
{right.map(item => (
<LinkOnSide key={item.title} item={item} side="right" />
))}
{web.credit.enable && (
<FooterButton
key="credit"
side="right"
content={content.credit}
title={<DynamicIcon icon={{ fi: 'FiCode' }} boxSize={size} />}
/>
)}
<ColorModeToggle size={size} />
</HStack>
<Container maxW="8xl">
<Grid
px={6}
py={4}
w="100%"
zIndex={1}
as="footer"
whiteSpace="nowrap"
templateColumns="repeat(4, 1fr)"
gap={6}
>
<GridItem colSpan={{base: 4, md: 2}}>
<Flex
flex="1 0 auto"
key="credit"
side="left"
maxW="50%"
>
<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>
);
};

View file

@ -1,5 +1,4 @@
import { Button, Link } from '@chakra-ui/react';
import { useBreakpointValue } from '~/hooks';
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 => {
const { title } = props;
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
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}
</Button>
);

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

View file

@ -2,7 +2,7 @@ import { Flex, FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { If, Then } from 'react-if';
import { useBooleanValue, useColorValue } from '~/hooks';
import { useBooleanValue } from '~/hooks';
import type { FormControlProps } from '@chakra-ui/react';
import type { FieldError } from 'react-hook-form';
@ -18,8 +18,8 @@ interface FormFieldProps extends FormControlProps {
export const FormField = (props: FormFieldProps): JSX.Element => {
const { name, label, children, labelAddOn, fieldAddOn, hiddenLabels = false, ...rest } = props;
const labelColor = useColorValue('blackAlpha.700', 'whiteAlpha.700');
const errorColor = useColorValue('red.500', 'red.300');
const labelColor = 'blackAlpha.700';
const errorColor = 'red.500';
const opacity = useBooleanValue(hiddenLabels, 0, undefined);
const { formState } = useFormContext<FormData>();
@ -47,7 +47,6 @@ export const FormField = (props: FormFieldProps): JSX.Element => {
>
<FormLabel
pr={0}
mb={{ lg: 4 }}
htmlFor={name}
display="flex"
opacity={opacity}

View file

@ -12,7 +12,7 @@ import {
import { If, Then } from 'react-if';
import { Markdown } from '~/elements';
import { useConfig } from '~/context';
import { useGreeting, useColorValue, useOpposingColor } from '~/hooks';
import { useGreeting } from '~/hooks';
import type { ModalContentProps } from '@chakra-ui/react';
@ -20,9 +20,6 @@ export const Greeting = (props: ModalContentProps): JSX.Element => {
const { web, content } = useConfig();
const { isAck, isOpen, open, ack } = useGreeting();
const bg = useColorValue('white', 'gray.800');
const color = useOpposingColor(bg);
useEffect(() => {
if (!web.greeting.enable && !web.greeting.required) {
ack(true, false);
@ -44,8 +41,8 @@ export const Greeting = (props: ModalContentProps): JSX.Element => {
<ModalOverlay />
<ModalContent
py={4}
bg={bg}
color={color}
bg="white"
color="black"
borderRadius="md"
maxW={{ base: '95%', md: '75%' }}
{...props}

View file

@ -1,11 +1,9 @@
import { Flex, ScaleFade } from '@chakra-ui/react';
import { Container, Flex, ScaleFade } from '@chakra-ui/react';
import { motionChakra } from '~/elements';
import { useBooleanValue, useFormInteractive, useBreakpointValue } from '~/hooks';
import { useBooleanValue, useBreakpointValue, useFormInteractive } from '~/hooks';
import { Title } from './title';
const Wrapper = motionChakra('header', {
baseStyle: { display: 'flex', px: 4, pt: 6, minH: 16, w: 'full', flex: '0 1 auto' },
});
const Wrapper = motionChakra('header');
export const Header = (): JSX.Element => {
const formInteractive = useFormInteractive();
@ -16,21 +14,28 @@ export const Header = (): JSX.Element => {
{ base: '75%', lg: '75%' },
);
const justify = useBreakpointValue({ base: 'flex-start', lg: 'center' });
return (
<Wrapper layout="position">
<ScaleFade in initialScale={0.5} style={{ width: '100%' }}>
<Flex
height="100%"
maxW={titleWidth}
// This is here for the logo
justifyContent={justify}
mx={{ base: formInteractive ? 'auto' : 0, lg: 'auto' }}
>
<Title />
</Flex>
</ScaleFade>
<Container
maxW="8xl"
display="flex"
px={4}
pt={6}
minH={16}
flex="0 1 auto"
>
<ScaleFade in initialScale={0.5} style={{ width: '100%' }}>
<Flex
height="100%"
maxW={titleWidth}
// This is here for the logo
justifyContent="center"
mx="auto"
>
<Title />
</Flex>
</ScaleFade>
</Container>
</Wrapper>
);
};

View file

@ -1,47 +1,19 @@
import { Image, Skeleton } from '@chakra-ui/react';
import { useCallback, useMemo, useState } from 'react';
import { useConfig } from '~/context';
import { useColorValue } from '~/hooks';
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 => {
const { web } = useConfig();
const { width } = web.logo;
const skeletonA = useColorValue('whiteSolid.100', 'blackSolid.800');
const skeletonB = useColorValue('light.500', 'dark.500');
const [source, setFallback] = useLogo();
const { lightFormat, width } = web.logo;
return (
<Image
src={source}
src={`/images/light${lightFormat}`}
alt={web.text.title}
onError={setFallback}
width={width ?? 'auto'}
maxW={{ base: '100%', md: width }}
width="auto"
css={{
userDrag: 'none',
userSelect: 'none',
@ -54,8 +26,8 @@ export const Logo = (props: ImageProps): JSX.Element => {
<Skeleton
isLoaded={false}
borderRadius="md"
endColor={skeletonB}
startColor={skeletonA}
endColor="light.500"
startColor="whiteSolid.100"
width={{ base: 64, lg: 80 }}
height={{ base: 12, lg: 16 }}
/>

View file

@ -28,7 +28,6 @@ const MWrapper = (props: MWrapperProps): JSX.Element => {
layout
spacing={1}
alignItems={formInteractive ? 'center' : 'flex-start'}
maxWidth="25%"
{...props}
/>
);
@ -47,7 +46,7 @@ const DWrapper = (props: DWrapperProps): JSX.Element => {
animate={formInteractive}
transition={{ damping: 15, type: 'spring', stiffness: 100 }}
variants={{ results: { scale: 0.5 }, form: { scale: 1 } }}
maxWidth="25%"
maxWidth="75%"
{...props}
/>
);
@ -145,7 +144,7 @@ export const Title = (props: FlexProps): JSX.Element => {
variant="link"
flexWrap="wrap"
flexDir="column"
onClick={() => reset()}
onClick={async () => await reset()}
_focus={{ boxShadow: 'none' }}
_hover={{ textDecoration: 'none' }}
>

View file

@ -1,8 +1,8 @@
import { Container, Flex } from '@chakra-ui/react';
import { useCallback, useRef } from 'react';
import { Flex } from '@chakra-ui/react';
import { isSafari } from 'react-device-detect';
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 { motionChakra } from '~/elements';
import { useFormState } from '~/hooks';
@ -31,7 +31,7 @@ export const Layout = (props: FlexProps): JSX.Element => {
const containerRef = useRef<HTMLDivElement>({} as HTMLDivElement);
function handleReset(): void {
async function handleReset() {
containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
setStatus('form');
reset();
@ -53,15 +53,17 @@ export const Layout = (props: FlexProps): JSX.Element => {
minHeight={isSafari ? '-webkit-fill-available' : '100vh'}
>
<Header />
<Main
layout
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}
>
{props.children}
</Main>
<Container maxW="8xl" display="flex" flex="1 1 auto">
<Main
layout
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}
>
{props.children}
</Main>
</Container>
<Footer />
<If condition={developerMode}>
<Then>

View file

@ -1,7 +1,6 @@
import { useMemo, useState } from 'react';
import { Flex, Avatar, chakra } from '@chakra-ui/react';
import { motionChakra } from '~/elements';
import { useColorValue, useOpposingColor } from '~/hooks';
import type { SingleOption } from '~/types';
import type { LocationOption } from './query-location';
@ -43,11 +42,9 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
}
}
const bg = useColorValue('white', 'blackSolid.600');
const imageBorder = useColorValue('gray.600', 'whiteAlpha.800');
const fg = useOpposingColor(bg);
const checkedBorder = useColorValue('blue.400', 'blue.300');
const errorBorder = useColorValue('red.500', 'red.300');
const imageBorder = 'gray.600';
const checkedBorder = 'brand.500';
const errorBorder = 'red.500';
const borderColor = useMemo(
() =>
@ -64,7 +61,7 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
);
return (
<LocationCardWrapper
bg={bg}
bg="white"
key={label}
whileHover={{ scale: 1.05 }}
borderColor={borderColor}
@ -76,7 +73,6 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
<>
<Flex justifyContent="space-between" alignItems="center">
<chakra.h2
color={fg}
fontWeight="bold"
mt={{ base: 2, md: 0 }}
fontSize={{ base: 'lg', md: 'xl' }}
@ -84,7 +80,7 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
{label}
</chakra.h2>
<Avatar
color={fg}
color="black"
name={label}
boxSize={12}
rounded="full"
@ -97,7 +93,7 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
</Flex>
{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}
</chakra.p>
)}

View file

@ -72,7 +72,9 @@ export const LookingGlassForm = (): JSX.Element => {
const isFqdnQuery = useCallback(
(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%"
mx="auto"
textAlign="left"
maxW={{ base: '100%', lg: '75%' }}
maxW="100%"
onSubmit={handleSubmit(submitHandler)}
>
<FormRow>

View file

@ -7,7 +7,7 @@ export const Meta = (): JSX.Element => {
const [location, setLocation] = useState('/');
const {
siteTitle: title = 'hyperglass',
siteTitle: title = 'Looking Glass',
siteDescription: description = 'Network Looking Glass',
} = useConfig();
@ -23,16 +23,13 @@ export const Meta = (): JSX.Element => {
<Head>
<title key="title">{title}</title>
<meta name="url" content={location} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="og:title" content={title} />
<meta name="og:url" content={location} />
<meta name="description" content={description} />
<meta property="og:image:alt" content={siteName} />
<meta name="og:description" content={description} />
<meta name="hyperglass-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"
/>
<meta name="lg-version" content={config.version} />
</Head>
);
};

View file

@ -147,8 +147,6 @@ export const ASPath = (props: ASPathProps): JSX.Element => {
export const Communities = (props: CommunitiesProps): JSX.Element => {
const { communities } = props;
const { web } = useConfig();
const bg = useColorValue('white', 'gray.900');
const color = useOpposingColor(bg);
return (
<If condition={communities.length === 0}>
<Then>
@ -165,10 +163,10 @@ export const Communities = (props: CommunitiesProps): JSX.Element => {
</MenuButton>
<MenuList
p={3}
bg={bg}
bg="white"
minW={32}
width="unset"
color={color}
color="black"
boxShadow="2xl"
textAlign="left"
fontFamily="mono"

View file

@ -11,7 +11,7 @@ import ReactFlow, {
import { useConfig } from '~/context';
import { useASNDetail, useColorToken, useColorValue } from '~/hooks';
import { Controls } from './controls';
import { useElements } from './useElements';
import { useElements } from './use-elements';
import type { NodeProps as ReactFlowNodeProps } from 'reactflow';

View file

@ -9,7 +9,7 @@ export const PathButton = (props: PathButtonProps): JSX.Element => {
const { onOpen } = props;
return (
<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" />
</Button>
</Tooltip>

View file

@ -40,7 +40,9 @@ export const Path = (props: PathProps): JSX.Element => {
<ModalHeader>{`Path to ${displayTarget}`}</ModalHeader>
<ModalCloseButton />
<ModalBody>
{response !== null ? <Chart data={output} /> : <Skeleton w="500px" h="300px" />}
<Skeleton isLoaded={response != null}>
<Chart data={output} />
</Skeleton>
</ModalBody>
</ModalContent>
</Modal>

View file

@ -6,19 +6,17 @@ import {
PopoverContent,
PopoverCloseButton,
} from '@chakra-ui/react';
import { useColorValue } from '~/hooks';
import type { PromptProps } from './types';
export const DesktopPrompt = (props: PromptProps): JSX.Element => {
const { trigger, children, ...disclosure } = props;
const bg = useColorValue('white', 'gray.900');
return (
<Popover closeOnBlur={false} {...disclosure}>
<PopoverTrigger>{trigger}</PopoverTrigger>
<PopoverContent bg={bg}>
<PopoverArrow bg={bg} />
<PopoverContent bg="white">
<PopoverArrow bg="white" />
<PopoverCloseButton />
<PopoverBody p={6}>{children}</PopoverBody>
</PopoverContent>

View file

@ -1,11 +1,9 @@
import { Modal, ModalBody, ModalOverlay, ModalContent, ModalCloseButton } from '@chakra-ui/react';
import { useColorValue } from '~/hooks';
import type { PromptProps } from './types';
export const MobilePrompt = (props: PromptProps): JSX.Element => {
const { children, trigger, ...disclosure } = props;
const bg = useColorValue('white', 'gray.900');
return (
<>
{trigger}
@ -18,7 +16,7 @@ export const MobilePrompt = (props: PromptProps): JSX.Element => {
{...disclosure}
>
<ModalOverlay />
<ModalContent bg={bg}>
<ModalContent bg="white">
<ModalCloseButton />
<ModalBody px={4} py={10}>
{children}

View file

@ -120,10 +120,10 @@ export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
<>
{options.length === 1 ? (
<Wrap
p={{ lg: 4 }}
p={{ lg: 2 }}
align="flex-start"
shouldWrapChildren
spacing={{ base: 4, lg: 8 }}
spacing={{ base: 4, lg: 6 }}
justify={{ base: 'center', lg: 'center' }}
>
{options[0].options.map(opt => {
@ -170,7 +170,7 @@ export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
options={options}
aria-label={label}
name="queryLocation"
closeMenuOnSelect={false}
closeMenuOnSelect={true}
onChange={handleSelectChange}
value={selections.queryLocation}
isError={typeof errors.queryLocation !== 'undefined'}

View file

@ -7,7 +7,7 @@ import { useDirective, useFormState } from '~/hooks';
import { isSelectDirective } from '~/types';
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 { SelectOnChange } from '~/components/select';
import type { Directive, FormData, OnChangeArgs, SingleOption } from '~/types';
@ -87,7 +87,7 @@ export const QueryTarget = (props: QueryTargetProps): JSX.Element => {
<InputGroup size="lg">
<Input
bg="white"
color="gray.400"
color="black"
borderRadius="md"
borderColor="gray.100"
value={displayTarget}
@ -96,12 +96,6 @@ export const QueryTarget = (props: QueryTargetProps): JSX.Element => {
name="queryTargetDisplay"
onChange={handleInputChange}
_placeholder={{ color: 'gray.600' }}
_dark={{
bg: 'blackSolid.800',
color: 'whiteAlpha.800',
borderColor: 'whiteAlpha.50',
_placeholder: { color: 'whiteAlpha.700' },
}}
/>
<InputRightElement w="max-content" pr={2}>
<UserIP setTarget={handleUserIPChange} />

View file

@ -13,18 +13,16 @@ interface ResetButtonProps extends FlexProps {
export const ResetButton = (props: ResetButtonProps): JSX.Element => {
const { developerMode, resetForm, ...rest } = props;
const status = useFormState(s => s.status);
const bg = useColorValue('primary.500', 'primary.300');
const color = useOpposingColor(bg);
return (
<AnimatePresence>
{status === 'results' && (
<AnimatedDiv
bg={bg}
bg="brand.500"
left={0}
zIndex={4}
bottom={24}
boxSize={12}
color={color}
color="white"
position="fixed"
animate={{ x: 0 }}
exit={{ x: '-100%' }}

View file

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { Button, Stack, Text, VStack } from '@chakra-ui/react';
import { useConfig } from '~/context';
import { DynamicIcon } from '~/elements';
import { useStrf, useColorValue, useDNSQuery, useFormState } from '~/hooks';
import { useStrf, useDNSQuery, useFormState } from '~/hooks';
import type { DnsOverHttps } from '~/types';
@ -28,8 +28,8 @@ export const ResolvedTarget = (props: ResolvedTargetProps): JSX.Element => {
const displayTarget = useFormState(s => s.target.display);
const setFormValue = useFormState(s => s.setFormValue);
const color = useColorValue('secondary.500', 'secondary.300');
const errorColor = useColorValue('red.500', 'red.300');
const color = 'secondary.500';
const errorColor = 'red.500';
const tooltip4 = strF(web.text.fqdnTooltip, { protocol: 'IPv4' });
const tooltip6 = strF(web.text.fqdnTooltip, { protocol: 'IPv6' });

View file

@ -18,7 +18,7 @@ export const CopyButton = (props: CopyButtonProps): JSX.Element => {
size="sm"
variant="ghost"
onClick={onCopy}
colorScheme="secondary"
colorScheme="primary"
{...rest}
>
<DynamicIcon icon={{ fi: hasCopied ? 'FiCheck' : 'FiCopy' }} boxSize="16px" />

View file

@ -1,8 +1,8 @@
import { AccordionIcon, Box, HStack, Spinner, Text, Tooltip } from '@chakra-ui/react';
import { useMemo } from 'react';
import { AccordionIcon, Box, Spinner, HStack, Text, Tooltip } from '@chakra-ui/react';
import { useConfig } from '~/context';
import { DynamicIcon } from '~/elements';
import { useColorValue, useOpposingColor, useStrf } from '~/hooks';
import { useOpposingColor, useStrf } from '~/hooks';
import type { ErrorLevels } from '~/types';
@ -26,9 +26,9 @@ const runtimeText = (runtime: number, text: string): string => {
export const ResultHeader = (props: ResultHeaderProps): JSX.Element => {
const { title, loading, isError, errorMsg, errorLevel, runtime } = props;
const status = useColorValue('primary.500', 'primary.300');
const warning = useColorValue(`${errorLevel}.500`, `${errorLevel}.300`);
const defaultStatus = useColorValue('success.500', 'success.300');
const status = 'primary.500';
const warning = `${errorLevel}.500`;
const defaultStatus = 'success.500';
const { web } = useConfig();
const strF = useStrf();
@ -52,7 +52,7 @@ export const ResultHeader = (props: ResultHeaderProps): JSX.Element => {
<Spinner size="sm" mr={4} color={status} />
) : (
<DynamicIcon
icon={isError ? { bi: 'BisError' } : { fa: 'FaCheckCircle' }}
icon={isError ? { bi: 'BiError' } : { fa: 'FaCheckCircle' }}
color={isError ? warning : defaultStatus}
mr={4}
boxSize="100%"

View file

@ -13,7 +13,7 @@ import {
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
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 { Else, If, Then } from 'react-if';
import { BGPTable, Path, TextOutput } from '~/components';
@ -48,6 +48,7 @@ const AccordionHeaderWrapper = chakra('div', {
baseStyle: {
display: 'flex',
justifyContent: 'space-between',
bg: 'white',
_hover: { bg: 'blackAlpha.50' },
_focus: { boxShadow: 'outline' },
},
@ -72,24 +73,44 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
const addResponse = useFormState(s => s.addResponse);
const form = useFormState(s => s.form);
const [errorLevel, _setErrorLevel] = useState<ErrorLevels>('error');
const { data, error, isError, isLoading, refetch, isFetchedAfterMount } = useLGQuery(
{
queryLocation,
queryTarget: form.queryTarget,
queryType: form.queryType,
},
const setErrorLevel = (level: ResponseLevel): void => {
let e: ErrorLevels = 'error';
switch (level) {
case 'success':
e = level;
break;
case 'warning' || 'error':
e = 'warning';
break;
}
_setErrorLevel(e);
};
const { data, error, isLoading, refetch, isFetchedAfterMount } = useLGQuery(
{ queryLocation, queryTarget: form.queryTarget, queryType: form.queryType },
{
onSuccess(data) {
if (device !== null) {
addResponse(device.id, data);
}
if (isLGOutputOrError(data)) {
console.error(data);
setErrorLevel(data.level);
}
},
onError(error) {
console.error(error);
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 strF = useStrf();
@ -123,23 +144,6 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
return messages.general;
}, [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>(() => {
let result = false;
if (data?.format === 'application/json') {
@ -220,6 +224,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
</AccordionHeaderWrapper>
<AccordionPanel
pb={4}
bg="white"
overflowX="auto"
css={{
WebkitOverflowScrolling: 'touch',

View file

@ -25,7 +25,7 @@ const _RequeryButton: React.ForwardRefRenderFunction<HTMLButtonElement, RequeryB
zIndex="1"
variant="ghost"
onClick={requery as Get<RequeryButtonProps, 'onClick'>}
colorScheme="secondary"
colorScheme="primary"
{...rest}
>
<DynamicIcon icon={{ fi: 'FiRepeat' }} boxSize="16px" />

View file

@ -19,7 +19,7 @@ export const Option = <Opt extends SingleOption, IsMulti extends boolean>(
display={{ base: 'flex', lg: 'inline-flex' }}
>
{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}
</Badge>
))}

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