forked from mirrors/thatmattlove-hyperglass
Compare commits
34 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d21da01a1 | |||
| ee7d8752f8 | |||
| 18e0b3e7e7 | |||
|
|
0b2fbb1b4d | ||
|
|
c2142ee76f | ||
|
|
ec3c55aa81 | ||
|
|
a5bc1ca8a0 | ||
|
|
0f52bc438f | ||
|
|
e0751311ba | ||
|
|
f340e65082 | ||
|
|
872c3ec654 | ||
|
|
41248231ae | ||
|
|
b617df24d1 | ||
|
|
3b0abd5ba8 | ||
|
|
0fd5bc7997 | ||
|
|
e8997b981c | ||
|
|
bbba29546c | ||
|
|
7eb8d8e925 | ||
|
|
4733dd1893 | ||
|
|
08fd310b44 | ||
|
|
6d06b9809d | ||
|
|
30fda91bc8 | ||
|
|
e19dd675e5 | ||
|
|
8a9766b99f | ||
|
|
4d5e259f6e | ||
|
|
f324d34323 | ||
|
|
3d82a21ba4 | ||
|
|
0137b042dc | ||
|
|
6b37ce96f6 | ||
|
|
aab4ada723 | ||
|
|
bfcae89bf0 | ||
|
|
8a3d704eca | ||
|
|
b796149a26 | ||
|
|
0c643c6abd |
86 changed files with 1103 additions and 953 deletions
30
.github/ISSUE_TEMPLATE/1-feature-request.md
vendored
30
.github/ISSUE_TEMPLATE/1-feature-request.md
vendored
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for hyperglass
|
||||
labels:
|
||||
- feature
|
||||
---
|
||||
|
||||
<!--
|
||||
If the answer to any of these questions is "no", your feature request will most likely be rejected (but will still be considered).
|
||||
- Is the new feature _only_ applicable to one platform (https://hyperglass.dev/platforms)?
|
||||
- Would the new feature work only on mobile, or only on desktop?
|
||||
- Would the new feature only support IPv4, or IPv6?
|
||||
- Is the new feature something that can be reasonably customized by hyperglass end-users?
|
||||
-->
|
||||
|
||||
# Feature Description
|
||||
|
||||
<!-- Describe the solution or change you would like. -->
|
||||
|
||||
### Is your feature request related to a problem? Please describe.
|
||||
|
||||
<!-- If yes, a clear and concise description of what the problem is. -->
|
||||
|
||||
# Environment & Use Case
|
||||
|
||||
<!-- Describe your use case for hyperglass, the environment on which it runs, the predominant network device type, and any other relevant details. -->
|
||||
|
||||
# Additional Context
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
49
.github/ISSUE_TEMPLATE/1-feature-request.yaml
vendored
Normal file
49
.github/ISSUE_TEMPLATE/1-feature-request.yaml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
name: Feature Request
|
||||
description: Suggest an idea for hyperglass
|
||||
labels:
|
||||
- feature
|
||||
assignees:
|
||||
- thatmattlove
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
If the answer to any of these questions is "no", your feature request will most likely be rejected (but may still be considered).
|
||||
- Is the new feature _only_ applicable to one [platform](https://hyperglass.dev/platforms)?
|
||||
- Would the new feature work only on mobile, or only on desktop?
|
||||
- Would the new feature only support IPv4, or IPv6?
|
||||
- Is the new feature something that can be reasonably customized by hyperglass users?
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of hyperglass are you currently running?
|
||||
placeholder: v2.0.4
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-details
|
||||
attributes:
|
||||
label: Feature Details
|
||||
description: Describe the solution or change you would like in detail.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: feature-type
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Feature Type
|
||||
multiple: true
|
||||
options:
|
||||
- New Platform
|
||||
- Web UI
|
||||
- New Functionality
|
||||
- Change to Existing Functionality
|
||||
- type: textarea
|
||||
id: use-case
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Use Case
|
||||
description: How will this feature benefit hyperglass users (providers, end-users, or both)?
|
||||
64
.github/ISSUE_TEMPLATE/2-bug-report.md
vendored
64
.github/ISSUE_TEMPLATE/2-bug-report.md
vendored
|
|
@ -1,64 +0,0 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Report a problem or unexpected behavior
|
||||
labels:
|
||||
- possible-bug
|
||||
assignees: thatmattlove
|
||||
---
|
||||
|
||||
<!-- Please provide a general summary of the issue in the Title. -->
|
||||
|
||||
# Bug Description
|
||||
|
||||
<!-- A clear and concise description of the bug. -->
|
||||
|
||||
# Expected behavior
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
# Steps to Reproduce
|
||||
|
||||
<!-- Provide steps necessary to reproduce this issue. -->
|
||||
|
||||
## Local Configurations
|
||||
|
||||
<!-- If possible, please a link to a live example and the relevant sections of your hyperglass.yaml, commands.yaml, or devices.yaml in a code block. -->
|
||||
|
||||
```
|
||||
<configs>
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
<!-- If an error occurred, please paste the relevant error message(s) in the below code block. -->
|
||||
|
||||
```
|
||||
<logs>
|
||||
```
|
||||
|
||||
# Possible Solution
|
||||
|
||||
<!-- If you think you know what would fix this, please share your ideas. -->
|
||||
|
||||
# Environment
|
||||
|
||||
## Server
|
||||
|
||||
<!-- Please paste the output from `hyperglass system-info` below: -->
|
||||
|
||||
<!-- If, for some reason, that doesn't work, please include the following:
|
||||
- OS:
|
||||
- Python Version:
|
||||
- hyperglass Version:
|
||||
-->
|
||||
|
||||
## Client
|
||||
|
||||
- OS & Version: <!-- (e.g. Windows 10, macOS 10.15, Ubuntu Linux 18.04) -->
|
||||
- Browser: <!-- (e.g. Chrome, Safari, Firefox, etc.) -->
|
||||
|
||||
### Smartphone Details (if applicable)
|
||||
|
||||
- Device: <!-- (e.g. iPhone, Samsung) -->
|
||||
- OS: <!-- (e.g. iOS 13.1, Android 11) -->
|
||||
- Browser: <!-- (e.g. Safari, Chrome) -->
|
||||
98
.github/ISSUE_TEMPLATE/2-bug-report.yaml
vendored
Normal file
98
.github/ISSUE_TEMPLATE/2-bug-report.yaml
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
name: Bug Report
|
||||
description: Report a problem or unexpected behavior
|
||||
labels:
|
||||
- possible-bug
|
||||
assignees:
|
||||
- thatmattlove
|
||||
body:
|
||||
- type: dropdown
|
||||
id: deployment-type
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Deployment Type
|
||||
description: How are you running hyperglass?
|
||||
multiple: false
|
||||
options:
|
||||
- Docker
|
||||
- Manual
|
||||
- Other (please explain)
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of hyperglass are you currently running?
|
||||
placeholder: v2.0.4
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: >
|
||||
Describe in detail the exact steps one can take to reproduce this bug.
|
||||
If reporting a UI bug, be sure to include screenshots, browser version, and operating system and platform.
|
||||
If you've deployed hyperglass manually, be sure to include Python and NodeJS versions.
|
||||
placeholder: |
|
||||
1. Click the thing
|
||||
2. Type the stuff
|
||||
3. See the error
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What did you expect to happen?
|
||||
placeholder: A thing should have happened.
|
||||
- type: textarea
|
||||
id: observed-behavior
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Observed Behavior
|
||||
description: What actually happened?
|
||||
placeholder: An error was shown.
|
||||
- type: textarea
|
||||
id: configuration
|
||||
attributes:
|
||||
label: Configuration
|
||||
description: hyperglass [config](https://hyperglass.dev/configuration/config) file in YAML format.
|
||||
placeholder: |
|
||||
org_name: Beloved Hyperglass User
|
||||
plugins: []
|
||||
primary_asn: 65000
|
||||
request_timeout: 90
|
||||
site_description: Beloved Hyperglass User Network Looking Glass
|
||||
site_title: Beloved Hyperglass User
|
||||
render: yaml
|
||||
- type: textarea
|
||||
id: devices
|
||||
attributes:
|
||||
label: Devices
|
||||
description: >
|
||||
hyperglass [devices](https://hyperglass.dev/configuration/devices) file in YAML format
|
||||
**with passwords obfuscated or removed**.
|
||||
placeholder: |
|
||||
devices:
|
||||
- name: New York, NY
|
||||
address: 192.0.2.1
|
||||
platform: cisco_ios
|
||||
credential:
|
||||
username: ***
|
||||
password: ***
|
||||
- name: San Francisco, CA
|
||||
address: 192.0.2.2
|
||||
platform: juniper
|
||||
credential:
|
||||
username: ***
|
||||
password: ***
|
||||
render: yaml
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Include any relevant log messages related to the bug.
|
||||
render: console
|
||||
27
.github/ISSUE_TEMPLATE/3-new-nos.yaml
vendored
27
.github/ISSUE_TEMPLATE/3-new-nos.yaml
vendored
|
|
@ -1,27 +0,0 @@
|
|||
---
|
||||
name: New Platform
|
||||
description: Request native support for a network operating system/platform
|
||||
labels:
|
||||
- feature
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
In order to natively support a new platform for hyperglass, please make sure it is supported by Netmiko ([see here](https://hyperglass.dev/platforms)).
|
||||
- type: input
|
||||
attributes:
|
||||
label: Manufacturer
|
||||
description: What is the network vendor? For example, for Juniper Junos, this would be Juniper.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Name
|
||||
description: What is the name of this platform? For example, for Juniper Junos, this would be Junos
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Commands
|
||||
description: >
|
||||
Please provide the commands required to execute the default hyperglass commands (IPv4 BGP Route, IPv6 BGP Route, BGP AS Path, BGP Community, IPv4 ping, IPv6 ping, IPv4 traceroute, and IPv6 traceroute). If you do not know the commands, it is likely that this request will be either denied or may take a long time to implement.
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Contributing Policy
|
||||
url: https://github.com/thatmattlove/hyperglass/blob/main/CONTRIBUTING.md
|
||||
about: Please read through the contributing policy before opening an issue or pull request.
|
||||
|
|
|
|||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
|
|
@ -1,4 +1,4 @@
|
|||
<!-- PLEASE CONSULT CONTRIBUTING.MD PRIOR TO WORKING ON HYPERGLASS -->
|
||||
<!-- PLEASE CONSULT CONTRIBUTING POLICY PRIOR TO WORKING ON HYPERGLASS -->
|
||||
|
||||
<!-- Provide a general summary of your changes in the Title. -->
|
||||
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
|||
# Project
|
||||
hyperglass/hyperglass/static
|
||||
static/
|
||||
TODO*
|
||||
.env
|
||||
|
||||
|
|
|
|||
41
CHANGELOG.md
41
CHANGELOG.md
|
|
@ -4,6 +4,47 @@ 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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
The Clear BSD License
|
||||
|
||||
Copyright (c) 2021 Matthew Love
|
||||
Copyright (c) 2024 Matthew Love
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
87
biome.json
87
biome.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ export default {
|
|||
title: "Changelog",
|
||||
href: "/changelog",
|
||||
},
|
||||
license: {
|
||||
title: "License",
|
||||
href: "/license",
|
||||
},
|
||||
},
|
||||
},
|
||||
demo: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
20
docs/pages/license.mdx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# The Clear BSD License
|
||||
|
||||
export const Year = () => new Date().getFullYear();
|
||||
|
||||
**Copyright © <Year/> Matthew Love**
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
- Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
> NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from datetime import datetime
|
||||
|
||||
__name__ = "hyperglass"
|
||||
__version__ = "2.0.1"
|
||||
__version__ = "2.0.4"
|
||||
__author__ = "Matt Love"
|
||||
__copyright__ = f"Copyright {datetime.now().year} Matthew Love"
|
||||
__license__ = "BSD 3-Clause Clear License"
|
||||
|
|
|
|||
|
|
@ -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 = """
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
76
hyperglass/defaults/directives/sixwind_os.py
Normal file
76
hyperglass/defaults/directives/sixwind_os.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"""Default 6WIND Directives."""
|
||||
|
||||
# Project
|
||||
from hyperglass.models.directive import (
|
||||
Text,
|
||||
RuleWithIPv4,
|
||||
RuleWithIPv6,
|
||||
RuleWithPattern,
|
||||
BuiltinDirective,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"SixWind_BGPRoute",
|
||||
"SixWind_Ping",
|
||||
"SixWind_Traceroute",
|
||||
)
|
||||
|
||||
NAME = "6WIND"
|
||||
PLATFORMS = ["sixwind_os"]
|
||||
|
||||
SixWind_BGPRoute = BuiltinDirective(
|
||||
id="__hyperglass_sixwind_os_bgp_route__",
|
||||
name="BGP Route",
|
||||
rules=[
|
||||
RuleWithIPv4(
|
||||
condition="0.0.0.0/0",
|
||||
action="permit",
|
||||
command="show bgp ipv4 unicast prefix {target}",
|
||||
),
|
||||
RuleWithIPv6(
|
||||
condition="::/0",
|
||||
action="permit",
|
||||
command="show bgp ipv6 unicast prefix {target}",
|
||||
),
|
||||
],
|
||||
field=Text(description="IP Address, Prefix, or Hostname"),
|
||||
platforms=PLATFORMS,
|
||||
)
|
||||
|
||||
SixWind_Ping = BuiltinDirective(
|
||||
id="__hyperglass_sixwind_os_ping__",
|
||||
name="Ping",
|
||||
rules=[
|
||||
RuleWithIPv4(
|
||||
condition="0.0.0.0/0",
|
||||
action="permit",
|
||||
command="cmd ping count 8 packetsize 256 source {source4} {target}",
|
||||
),
|
||||
RuleWithIPv6(
|
||||
condition="::/0",
|
||||
action="permit",
|
||||
command="cmd ping count 8 packetsize 256 ipv6 source {source6} {target}",
|
||||
),
|
||||
],
|
||||
field=Text(description="IP Address, Prefix, or Hostname"),
|
||||
platforms=PLATFORMS,
|
||||
)
|
||||
|
||||
SixWind_Traceroute = BuiltinDirective(
|
||||
id="__hyperglass_sixwind_os_traceroute__",
|
||||
name="Traceroute",
|
||||
rules=[
|
||||
RuleWithIPv4(
|
||||
condition="0.0.0.0/0",
|
||||
action="permit",
|
||||
command="cmd traceroute source {source4} {target}",
|
||||
),
|
||||
RuleWithIPv6(
|
||||
condition="::/0",
|
||||
action="permit",
|
||||
command="cmd traceroute ipv6 source {source6} {target}",
|
||||
),
|
||||
],
|
||||
field=Text(description="IP Address, Prefix, or Hostname"),
|
||||
platforms=PLATFORMS,
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Standard Library
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
1
hyperglass/external/_base.py
vendored
1
hyperglass/external/_base.py
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -205,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,)
|
||||
|
|
@ -328,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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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) and str(Settings.original_app_path) in value:
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import { forwardRef } from 'react';
|
||||
import { Button, Tooltip } from '@chakra-ui/react';
|
||||
import { Switch, Case } from 'react-if';
|
||||
import { DynamicIcon } from '~/elements';
|
||||
import { useOpposingColor, useColorMode, useColorValue, useBreakpointValue } from '~/hooks';
|
||||
|
||||
import type { ButtonProps } from '@chakra-ui/react';
|
||||
|
||||
interface ColorModeToggleProps extends Omit<ButtonProps, 'size'> {
|
||||
size?: string | number;
|
||||
}
|
||||
|
||||
export const ColorModeToggle = forwardRef<HTMLButtonElement, ColorModeToggleProps>(
|
||||
(props: ColorModeToggleProps, ref) => {
|
||||
const { size = '1.5rem', ...rest } = props;
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
|
||||
const bg = useColorValue('primary.500', 'yellow.300');
|
||||
const color = useOpposingColor(bg);
|
||||
const label = useColorValue('Switch to dark mode', 'Switch to light mode');
|
||||
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
|
||||
|
||||
return (
|
||||
<Tooltip hasArrow placement="top-end" label={label} bg={bg} color={color}>
|
||||
<Button
|
||||
ref={ref}
|
||||
size={btnSize}
|
||||
title={label}
|
||||
variant="ghost"
|
||||
aria-label={label}
|
||||
_hover={{ color: bg }}
|
||||
color="currentColor"
|
||||
onClick={toggleColorMode}
|
||||
{...rest}
|
||||
>
|
||||
<Switch>
|
||||
<Case condition={colorMode === 'light'}>
|
||||
<DynamicIcon icon={{ hi: 'HiMoon' }} boxSize={size} />
|
||||
</Case>
|
||||
<Case condition={colorMode === 'dark'}>
|
||||
<DynamicIcon icon={{ hi: 'HiSun' }} boxSize={size} />
|
||||
</Case>
|
||||
</Switch>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { Flex, HStack, useToken } from '@chakra-ui/react';
|
||||
import { Box, Container, Flex, Grid, GridItem } from '@chakra-ui/react';
|
||||
import { useMemo } from 'react';
|
||||
import { 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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
37
hyperglass/ui/components/footer/logo.tsx
Normal file
37
hyperglass/ui/components/footer/logo.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Image, Skeleton } from '@chakra-ui/react';
|
||||
import { useConfig } from '~/context';
|
||||
|
||||
import type { ImageProps } from '@chakra-ui/react';
|
||||
|
||||
export const Logo = (props: ImageProps): JSX.Element => {
|
||||
const { web } = useConfig();
|
||||
const { footerFormat } = web.logo;
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={`/images/footer${footerFormat}`}
|
||||
alt={web.text.title}
|
||||
width="100%"
|
||||
css={{
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'top',
|
||||
userDrag: 'none',
|
||||
userSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
WebkitUserDrag: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
}}
|
||||
fallback={
|
||||
<Skeleton
|
||||
isLoaded={false}
|
||||
borderRadius="md"
|
||||
endColor="light.500"
|
||||
startColor="whiteSolid.100"
|
||||
width="100%"
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@ import { Flex, FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react
|
|||
import { useMemo } from 'react';
|
||||
import { 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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -46,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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -144,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' }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -173,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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%' }}
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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%"
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { mergeWith } from '@chakra-ui/utils';
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
useColorToken,
|
||||
useColorValue,
|
||||
useMobile,
|
||||
useOpposingColor,
|
||||
useOpposingColorCallback,
|
||||
|
|
@ -30,26 +28,20 @@ export const useControlStyle = <Opt extends SingleOption, IsMulti extends boolea
|
|||
|
||||
const { isError } = useSelectContext();
|
||||
|
||||
const minHeight = useToken('space', 12);
|
||||
const borderRadius = useToken('radii', 'md');
|
||||
const color = useColorToken('colors', 'black', 'whiteAlpha.800');
|
||||
const focusBorder = useColorToken('colors', 'blue.500', 'blue.300');
|
||||
const invalidBorder = useColorToken('colors', 'red.500', 'red.300');
|
||||
// const borderColor = useColorToken('colors', 'gray.200', 'whiteAlpha.300');
|
||||
const borderColor = useColorToken('colors', 'gray.100', 'whiteAlpha.50');
|
||||
const borderHover = useColorToken('colors', 'gray.300', 'whiteAlpha.400');
|
||||
const backgroundColor = useColorToken('colors', 'white', 'blackSolid.800');
|
||||
const focusBorder = useToken('colors', 'brand.500');
|
||||
const invalidBorder = useToken('colors', 'red.500');
|
||||
const borderHover = useToken('colors', 'gray.300');
|
||||
|
||||
return useCallback(
|
||||
(base, state) => {
|
||||
const { isFocused } = state;
|
||||
const styles = {
|
||||
backgroundColor,
|
||||
borderRadius,
|
||||
color,
|
||||
minHeight,
|
||||
backgroundColor: useToken('colors', 'white'),
|
||||
borderRadius: useToken('radii', 'md'),
|
||||
color: useToken('colors', 'black'),
|
||||
minHeight: useToken('space', 12),
|
||||
transition: 'all 0.2s',
|
||||
borderColor: isError ? invalidBorder : isFocused ? focusBorder : borderColor,
|
||||
borderColor: isError ? invalidBorder : isFocused ? focusBorder : useToken('gray.100'),
|
||||
boxShadow: isError
|
||||
? `0 0 0 1px ${invalidBorder}`
|
||||
: isFocused
|
||||
|
|
@ -72,9 +64,7 @@ export const useMenuStyle = <Opt extends SingleOption, IsMulti extends boolean>(
|
|||
const { colorMode } = props;
|
||||
|
||||
const { isOpen } = useSelectContext();
|
||||
|
||||
const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700');
|
||||
const styles = { backgroundColor, zIndex: 1500 };
|
||||
const styles = { backgroundColor: useToken('white'), zIndex: 1500 };
|
||||
|
||||
return useCallback(base => mergeWith({}, base, styles), [colorMode, isOpen]);
|
||||
};
|
||||
|
|
@ -86,14 +76,12 @@ export const useMenuListStyle = <Opt extends SingleOption, IsMulti extends boole
|
|||
|
||||
const { isOpen } = useSelectContext();
|
||||
|
||||
const borderRadius = useToken('radii', 'md');
|
||||
const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700');
|
||||
const scrollbarTrack = useColorToken('colors', 'blackAlpha.50', 'whiteAlpha.50');
|
||||
const scrollbarThumb = useColorToken('colors', 'blackAlpha.300', 'whiteAlpha.300');
|
||||
const scrollbarThumbHover = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400');
|
||||
const scrollbarTrack = useToken('colors', 'blackAlpha.50');
|
||||
const scrollbarThumb = useToken('colors', 'blackAlpha.300');
|
||||
const scrollbarThumbHover = useToken('colors', 'blackAlpha.400');
|
||||
const styles = {
|
||||
borderRadius,
|
||||
backgroundColor,
|
||||
borderRadius: useToken('radii', 'md'),
|
||||
backgroundColor: useToken('colors', 'white'),
|
||||
'&::-webkit-scrollbar': { width: '5px' },
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: scrollbarTrack },
|
||||
'&::-webkit-scrollbar-thumb': { backgroundColor: scrollbarThumb },
|
||||
|
|
@ -113,12 +101,11 @@ export const useOptionStyle = <Opt extends SingleOption, IsMulti extends boolean
|
|||
|
||||
const fontSize = useToken('fontSizes', 'lg');
|
||||
const disabled = useToken('colors', 'whiteAlpha.400');
|
||||
const active = useColorToken('colors', 'primary.600', 'primary.400');
|
||||
const focused = useColorToken('colors', 'primary.500', 'primary.300');
|
||||
const selected = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400');
|
||||
const active = useToken('colors', 'brand.600');
|
||||
const focused = useToken('colors', 'brand.500');
|
||||
const selected = useToken('colors', 'brand.600');
|
||||
|
||||
const activeColor = useOpposingColor(active);
|
||||
const getColor = useOpposingColorCallback();
|
||||
const color = useToken('colors', 'white');
|
||||
|
||||
return useCallback(
|
||||
(base, state) => {
|
||||
|
|
@ -136,12 +123,11 @@ export const useOptionStyle = <Opt extends SingleOption, IsMulti extends boolean
|
|||
backgroundColor = focused;
|
||||
break;
|
||||
}
|
||||
const color = getColor(backgroundColor);
|
||||
|
||||
const styles = {
|
||||
color: backgroundColor === 'transparent' ? 'currentColor' : color,
|
||||
'&:active': { backgroundColor: active, color: activeColor },
|
||||
'&:focus': { backgroundColor: active, color: activeColor },
|
||||
'&:active': { backgroundColor: active, color: color },
|
||||
'&:focus': { backgroundColor: active, color: color },
|
||||
backgroundColor,
|
||||
fontSize,
|
||||
};
|
||||
|
|
@ -156,8 +142,7 @@ export const useIndicatorSeparatorStyle = <Opt extends SingleOption, IsMulti ext
|
|||
props: RSStyleCallbackProps,
|
||||
): RSStyleFunction<'indicatorSeparator', Opt, IsMulti> => {
|
||||
const { colorMode } = props;
|
||||
const backgroundColor = useColorToken('colors', 'gray.200', 'whiteAlpha.300');
|
||||
const styles = { backgroundColor };
|
||||
const styles = { backgroundColor: useToken('colors', 'gray.200') };
|
||||
|
||||
return useCallback(base => mergeWith({}, base, styles), [colorMode]);
|
||||
};
|
||||
|
|
@ -167,10 +152,12 @@ export const usePlaceholderStyle = <Opt extends SingleOption, IsMulti extends bo
|
|||
): RSStyleFunction<'placeholder', Opt, IsMulti> => {
|
||||
const { colorMode } = props;
|
||||
|
||||
const color = useColorToken('colors', 'gray.600', 'whiteAlpha.700');
|
||||
const fontSize = useToken('fontSizes', 'lg');
|
||||
const styles = {
|
||||
color: useToken('colors', 'gray.600'),
|
||||
fontSize: useToken('fontSizes', 'lg')
|
||||
}
|
||||
|
||||
return useCallback(base => mergeWith({}, base, { color, fontSize }), [colorMode]);
|
||||
return useCallback(base => mergeWith({}, base, styles), [colorMode]);
|
||||
};
|
||||
|
||||
export const useSingleValueStyle = <Opt extends SingleOption, IsMulti extends boolean>(
|
||||
|
|
@ -178,9 +165,11 @@ export const useSingleValueStyle = <Opt extends SingleOption, IsMulti extends bo
|
|||
): RSStyleFunction<'singleValue', Opt, IsMulti> => {
|
||||
const { colorMode } = props;
|
||||
|
||||
const color = useColorValue('black', 'whiteAlpha.800');
|
||||
const fontSize = useToken('fontSizes', 'lg');
|
||||
const styles = { color, fontSize };
|
||||
const color = useToken('colors', 'black');
|
||||
const styles = {
|
||||
color,
|
||||
fontSize: useToken('fontSizes', 'lg')
|
||||
};
|
||||
|
||||
return useCallback(base => mergeWith({}, base, styles), [color, colorMode]);
|
||||
};
|
||||
|
|
@ -190,10 +179,12 @@ export const useMultiValueStyle = <Opt extends SingleOption, IsMulti extends boo
|
|||
): RSStyleFunction<'multiValue', Opt, IsMulti> => {
|
||||
const { colorMode } = props;
|
||||
|
||||
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
|
||||
const color = useOpposingColor(backgroundColor);
|
||||
const borderRadius = useToken('radii', 'md');
|
||||
const styles = { backgroundColor, color, borderRadius };
|
||||
const backgroundColor = useToken('colors', 'brand.500');
|
||||
const styles = {
|
||||
backgroundColor: backgroundColor,
|
||||
color: useToken('colors', 'white'),
|
||||
borderRadius: useToken('radii', 'md')
|
||||
};
|
||||
|
||||
return useCallback(base => mergeWith({}, base, styles), [backgroundColor, colorMode]);
|
||||
};
|
||||
|
|
@ -203,9 +194,7 @@ export const useMultiValueLabelStyle = <Opt extends SingleOption, IsMulti extend
|
|||
): RSStyleFunction<'multiValueLabel', Opt, IsMulti> => {
|
||||
const { colorMode } = props;
|
||||
|
||||
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
|
||||
const color = useOpposingColor(backgroundColor);
|
||||
const styles = { color };
|
||||
const styles = { color: useToken('colors', 'white') };
|
||||
|
||||
return useCallback(base => mergeWith({}, base, styles), [colorMode]);
|
||||
};
|
||||
|
|
@ -215,10 +204,9 @@ export const useMultiValueRemoveStyle = <Opt extends SingleOption, IsMulti exten
|
|||
): RSStyleFunction<'multiValueRemove', Opt, IsMulti> => {
|
||||
const { colorMode } = props;
|
||||
|
||||
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
|
||||
const color = useOpposingColor(backgroundColor);
|
||||
const color = useToken('colors', 'white');
|
||||
const styles = {
|
||||
color,
|
||||
color: color,
|
||||
'&:hover': { backgroundColor: 'transparent', color, opacity: 0.8 },
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
import { forwardRef } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Popover,
|
||||
ModalBody,
|
||||
IconButton,
|
||||
PopoverBody,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
PopoverArrow,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { If, Then, Else } from 'react-if';
|
||||
import { Else, If, Then } from 'react-if';
|
||||
import { ResolvedTarget } from '~/components';
|
||||
import { DynamicIcon } from '~/elements';
|
||||
import { useFormState, useMobile, useColorValue } from '~/hooks';
|
||||
import { useFormState, useMobile } from '~/hooks';
|
||||
|
||||
import type { IconButtonProps } from '@chakra-ui/react';
|
||||
|
||||
|
|
@ -42,7 +42,8 @@ const _SubmitIcon: React.ForwardRefRenderFunction<
|
|||
type="submit"
|
||||
icon={<DynamicIcon icon={{ fi: 'FiSearch' }} />}
|
||||
title="Submit Query"
|
||||
colorScheme="primary"
|
||||
variant="solid"
|
||||
colorScheme="brand"
|
||||
isLoading={isLoading}
|
||||
aria-label="Submit Query"
|
||||
{...rest}
|
||||
|
|
@ -56,7 +57,6 @@ const SubmitIcon = forwardRef<HTMLButtonElement, Omit<IconButtonProps, 'aria-lab
|
|||
*/
|
||||
const MSubmitButton = (props: ResponsiveSubmitButtonProps): JSX.Element => {
|
||||
const { children, isOpen, onClose } = props;
|
||||
const bg = useColorValue('white', 'gray.900');
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
|
|
@ -70,7 +70,7 @@ const MSubmitButton = (props: ResponsiveSubmitButtonProps): JSX.Element => {
|
|||
motionPreset="slideInBottom"
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent bg={bg}>
|
||||
<ModalContent bg="white">
|
||||
<ModalCloseButton />
|
||||
<ModalBody px={4} py={10}>
|
||||
{isOpen && <ResolvedTarget errorClose={onClose} />}
|
||||
|
|
@ -86,12 +86,11 @@ const MSubmitButton = (props: ResponsiveSubmitButtonProps): JSX.Element => {
|
|||
*/
|
||||
const DSubmitButton = (props: ResponsiveSubmitButtonProps): JSX.Element => {
|
||||
const { children, isOpen, onClose } = props;
|
||||
const bg = useColorValue('white', 'gray.900');
|
||||
return (
|
||||
<Popover isOpen={isOpen} onClose={onClose} closeOnBlur={false}>
|
||||
<PopoverTrigger>{children}</PopoverTrigger>
|
||||
<PopoverContent bg={bg}>
|
||||
<PopoverArrow bg={bg} />
|
||||
<PopoverContent bg="white">
|
||||
<PopoverArrow bg="white" />
|
||||
<PopoverCloseButton />
|
||||
<PopoverBody p={6}>{isOpen && <ResolvedTarget errorClose={onClose} />}</PopoverBody>
|
||||
</PopoverContent>
|
||||
|
|
@ -114,7 +113,7 @@ export const SubmitButton = (props: SubmitButtonProps): JSX.Element => {
|
|||
|
||||
const { reset } = useFormContext();
|
||||
|
||||
function handleClose(): void {
|
||||
async function handleClose() {
|
||||
reset();
|
||||
resetForm();
|
||||
resolvedClose();
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const UserIP = (props: UserIPProps): JSX.Element => {
|
|||
return (
|
||||
<Prompt
|
||||
trigger={
|
||||
<Button size="sm" onClick={handleOpen}>
|
||||
<Button size="sm" onClick={handleOpen} colorScheme="brand">
|
||||
{web.text.ipButton}
|
||||
</Button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { ChakraProvider, localStorageManager } from '@chakra-ui/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { makeTheme } from '~/util';
|
||||
|
||||
import type { Config } from '~/types';
|
||||
|
|
@ -12,7 +12,7 @@ interface HyperglassProviderProps {
|
|||
|
||||
export const HyperglassContext = createContext<Config>({} as Config);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
export const queryClient = new QueryClient();
|
||||
|
||||
export const HyperglassProvider = (props: HyperglassProviderProps): JSX.Element => {
|
||||
const { config, children } = props;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Flex } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/hooks';
|
||||
|
||||
import type { FlexProps } from '@chakra-ui/react';
|
||||
|
||||
|
|
@ -9,14 +8,12 @@ interface CardBodyProps extends Omit<FlexProps, 'onClick'> {
|
|||
|
||||
export const CardBody = (props: CardBodyProps): JSX.Element => {
|
||||
const { onClick, ...rest } = props;
|
||||
const bg = useColorValue('white', 'dark.500');
|
||||
const color = useColorValue('dark.500', 'white');
|
||||
return (
|
||||
<Flex
|
||||
bg={bg}
|
||||
bg="white"
|
||||
w="100%"
|
||||
rounded="md"
|
||||
color={color}
|
||||
color="dark.500"
|
||||
onClick={onClick}
|
||||
overflow="hidden"
|
||||
borderWidth="1px"
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
import { Box } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/hooks';
|
||||
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
export const CodeBlock = (props: BoxProps): JSX.Element => {
|
||||
const bg = useColorValue('blackAlpha.100', 'gray.800');
|
||||
const color = useColorValue('black', 'white');
|
||||
return (
|
||||
<Box
|
||||
p={3}
|
||||
mt={5}
|
||||
bg={bg}
|
||||
bg="blackAlpha.100"
|
||||
as="pre"
|
||||
border="1px"
|
||||
rounded="md"
|
||||
color={color}
|
||||
color="black"
|
||||
fontSize="sm"
|
||||
fontFamily="mono"
|
||||
borderColor="inherit"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ const Renderer = (props: RendererProps): JSX.Element => {
|
|||
const time = [zeroPad(seconds)];
|
||||
minutes !== 0 && time.unshift(zeroPad(minutes));
|
||||
hours !== 0 && time.unshift(zeroPad(hours));
|
||||
const bg = useColorValue('black', 'white');
|
||||
return (
|
||||
<If condition={completed}>
|
||||
<Then>
|
||||
|
|
@ -28,7 +27,7 @@ const Renderer = (props: RendererProps): JSX.Element => {
|
|||
<Else>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{text}
|
||||
<chakra.span fontSize="xs" color={bg}>
|
||||
<chakra.span fontSize="xs">
|
||||
{time.join(':')}
|
||||
</chakra.span>
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { useMemo } from 'react';
|
||||
import create from 'zustand';
|
||||
import intersectionWith from 'lodash/intersectionWith';
|
||||
import plur from 'plur';
|
||||
import { useMemo } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import create from 'zustand';
|
||||
import { queryClient } from '~/context';
|
||||
import { all, andJoin, dedupObjectArray, withDev } from '~/util';
|
||||
|
||||
import type { UseFormClearErrors, UseFormSetError } from 'react-hook-form';
|
||||
import type { MultiValue, SingleValue } from 'react-select';
|
||||
import type { StateCreator } from 'zustand';
|
||||
import type { UseFormSetError, UseFormClearErrors } from 'react-hook-form';
|
||||
import type { SingleValue, MultiValue } from 'react-select';
|
||||
import type { SingleOption, Directive, FormData, Text, Device } from '~/types';
|
||||
import type { Device, Directive, FormData, SingleOption, Text } from '~/types';
|
||||
import type { UseDeviceReturn } from './use-device';
|
||||
|
||||
type FormStatus = 'form' | 'results';
|
||||
|
|
@ -64,7 +65,7 @@ interface FormStateType<Opt extends SingleOption = SingleOption> {
|
|||
>(field: K, value: FormSelections[K]): void;
|
||||
setTarget(update: Partial<Target>): void;
|
||||
getDirective(): Directive | null;
|
||||
reset(): void;
|
||||
reset(): Promise<void>;
|
||||
setFormValue<K extends keyof FormValues>(field: K, value: FormValues[K]): void;
|
||||
locationChange(
|
||||
locations: string[],
|
||||
|
|
@ -198,7 +199,8 @@ const formState: StateCreator<FormStateType> = (set, get) => ({
|
|||
return null;
|
||||
},
|
||||
|
||||
reset(): void {
|
||||
async reset(): Promise<void> {
|
||||
const { form } = get();
|
||||
set({
|
||||
filtered: { types: [], groups: [] },
|
||||
form: { queryLocation: [], queryTarget: [], queryType: '' },
|
||||
|
|
@ -209,6 +211,10 @@ const formState: StateCreator<FormStateType> = (set, get) => ({
|
|||
target: { display: '' },
|
||||
resolvedIsOpen: false,
|
||||
});
|
||||
for (const queryLocation of form.queryLocation) {
|
||||
const query = { queryLocation, queryTarget: form.queryTarget, queryType: form.queryType };
|
||||
queryClient.removeQueries({ queryKey: ['/api/query', query] });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -55,9 +55,7 @@ export function useLGQuery(
|
|||
);
|
||||
try {
|
||||
const data = await res.json();
|
||||
console.dir(data, { depth: null });
|
||||
return data;
|
||||
// return await res.json();
|
||||
} catch (err) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
|
|
@ -74,8 +72,6 @@ export function useLGQuery(
|
|||
return useQuery<QueryResponse, Response | QueryResponse | Error, QueryResponse, LGQueryKey>({
|
||||
queryKey: ['/api/query', query],
|
||||
queryFn: runQuery,
|
||||
// Invalidate react-query's cache just shy of the configured cache timeout.
|
||||
cacheTime: cache.timeout * 1000 * 0.95,
|
||||
// Don't refetch when window refocuses.
|
||||
refetchOnWindowFocus: false,
|
||||
// Don't automatically refetch query data (queries should be on-off).
|
||||
|
|
|
|||
4
hyperglass/ui/package.json
vendored
4
hyperglass/ui/package.json
vendored
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.4",
|
||||
"name": "ui",
|
||||
"description": "UI for hyperglass, the modern network looking glass",
|
||||
"author": "Matt Love",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"@tanstack/react-query": "^4.22.0",
|
||||
"dagre": "^0.8.5",
|
||||
"dayjs": "^1.10.4",
|
||||
"framer-motion": "^10.11.6",
|
||||
"framer-motion": "^11.2.10",
|
||||
"lodash": "^4.17.21",
|
||||
"merge-anything": "^4.0.1",
|
||||
"next": "13.5.6",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import fs from 'fs';
|
||||
import { ColorModeScript } from '@chakra-ui/react';
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
import { CustomHtml, CustomJavascript, Favicon } from '~/elements';
|
||||
import { googleFontUrl } from '~/util';
|
||||
|
|
@ -42,7 +41,7 @@ class MyDocument extends Document<DocumentExtra> {
|
|||
// } = await getHyperglassConfig(hyperglassUrl);
|
||||
|
||||
fonts = {
|
||||
body: googleFontUrl(config.web.theme.fonts.body),
|
||||
body: "https://assets.witine.com/fonts/inter.css",
|
||||
mono: googleFontUrl(config.web.theme.fonts.mono),
|
||||
};
|
||||
defaultColorMode = config.web.theme.defaultColorMode;
|
||||
|
|
@ -61,9 +60,6 @@ class MyDocument extends Document<DocumentExtra> {
|
|||
<meta name="og:image" content="/images/opengraph.jpg" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
||||
<link href={this.props.fonts.mono} rel="stylesheet" />
|
||||
<link href={this.props.fonts.body} rel="stylesheet" />
|
||||
{favicons.map(favicon => (
|
||||
|
|
@ -72,7 +68,6 @@ class MyDocument extends Document<DocumentExtra> {
|
|||
<CustomJavascript>{this.props.customJs}</CustomJavascript>
|
||||
</Head>
|
||||
<body>
|
||||
<ColorModeScript initialColorMode={this.props.defaultColorMode ?? 'system'} />
|
||||
<Main />
|
||||
<CustomHtml>{this.props.customHtml}</CustomHtml>
|
||||
<NextScript />
|
||||
|
|
|
|||
574
hyperglass/ui/pnpm-lock.yaml
generated
574
hyperglass/ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -59,6 +59,7 @@ interface _Logo {
|
|||
height: string | null;
|
||||
light_format: string;
|
||||
dark_format: string;
|
||||
footer_format: string;
|
||||
}
|
||||
|
||||
interface _Link {
|
||||
|
|
@ -87,6 +88,7 @@ interface _Highlight {
|
|||
}
|
||||
|
||||
interface _Web {
|
||||
copyright: string;
|
||||
credit: _Credit;
|
||||
dns_provider: { name: string; url: string };
|
||||
links: _Link[];
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function importFonts(userFonts: Theme.Fonts): ChakraTheme['fonts'] {
|
|||
|
||||
function importColors(userColors: ThemeConfig['colors']): Theme.Colors {
|
||||
const initial: Pick<Theme.Colors, 'blackSolid' | 'whiteSolid' | 'transparent' | 'current'> = {
|
||||
brand: generatePalette('#00BCA9'),
|
||||
blackSolid: {
|
||||
50: '#444444',
|
||||
100: '#3c3c3c',
|
||||
|
|
@ -92,24 +93,14 @@ export function makeTheme(
|
|||
colors,
|
||||
config,
|
||||
fontWeights,
|
||||
semanticTokens: {
|
||||
colors: {
|
||||
'body-bg': {
|
||||
default: 'light.500',
|
||||
_dark: 'dark.500',
|
||||
},
|
||||
'body-fg': {
|
||||
default: 'dark.500',
|
||||
_dark: 'light.500',
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
global: {
|
||||
html: { scrollBehavior: 'smooth', height: '-webkit-fill-available' },
|
||||
body: {
|
||||
background: 'body-bg',
|
||||
color: 'body-fg',
|
||||
background: 'repeating-linear-gradient(225deg,#f5fffe,#effdff,#eff9ff,#f5f4ff,#ffeefe,#ffeefe)',
|
||||
color: '#2D3635',
|
||||
fontSize: '1.05rem',
|
||||
lineHeight: '1.5',
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "hyperglass"
|
||||
version = "2.0.1"
|
||||
version = "2.0.4"
|
||||
description = "hyperglass is the modern network looking glass that tries to make the internet better."
|
||||
authors = [
|
||||
{ name = "thatmattlove", email = "matt@hyperglass.dev" }
|
||||
|
|
|
|||
13
version.py
13
version.py
|
|
@ -18,10 +18,23 @@ PYPROJECT_PATTERN = re.compile(r"^version\s\=\s\"(.+)\"$")
|
|||
CONSTANTS = Path(__file__).parent / "hyperglass" / "constants.py"
|
||||
CONSTANT_PATTERN = re.compile(r"^__version__\s\=\s\"(.+)\"$")
|
||||
|
||||
UPGRADE_DOC = Path(__file__).parent / "docs" / "pages" / "installation" / "upgrading.mdx"
|
||||
UPGRADE_DOC_PATTERN = re.compile(r"^git\scheckout\sv(.+)$")
|
||||
|
||||
UPGRADE_GH_FEATURE = Path(__file__).parent / ".github" / "ISSUE_TEMPLATE" / "1-feature-request.yaml"
|
||||
UPGRADE_GH_FEATURE_PATTERN = re.compile(r"^[\s\t]+placeholder\:\sv(.+)$")
|
||||
|
||||
UPGRADE_GH_BUG = Path(__file__).parent / ".github" / "ISSUE_TEMPLATE" / "2-bug-report.yaml"
|
||||
|
||||
UPGRADE_GH_BUG_PATTERN = re.compile(r"^[\s\t]+placeholder\:\sv(.+)$")
|
||||
|
||||
UPGRADES = (
|
||||
("package.json", PACKAGE_JSON, PACKAGE_JSON_PATTERN),
|
||||
("pyproject.toml", PYPROJECT_TOML, PYPROJECT_PATTERN),
|
||||
("constants.py", CONSTANTS, CONSTANT_PATTERN),
|
||||
("upgrading.mdx", UPGRADE_DOC, UPGRADE_DOC_PATTERN),
|
||||
("1-feature-request.yaml", UPGRADE_GH_FEATURE, UPGRADE_GH_FEATURE_PATTERN),
|
||||
("2-bug-report.yaml", UPGRADE_GH_BUG, UPGRADE_GH_BUG_PATTERN),
|
||||
)
|
||||
|
||||
cli = typer.Typer(name="version", no_args_is_help=True)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue