forked from mirrors/thatmattlove-hyperglass
Merge UI Refactor
**Minor Changes** - Migration to TypeScript - Migration to Chakra UI 1.1 - Migration to Next.JS 10 - Migration to React 17 - Addition of BGP map, closes #72 **Breaking Changes** - `web.text.title` & `web.text.subtitle` now carry a 32 character limit for styling/layout reasons.
This commit is contained in:
commit
5e045686a8
225 changed files with 8238 additions and 6639 deletions
|
|
@ -55,7 +55,7 @@ hyperglass is intended to make implementing a looking glass too easy not to do,
|
|||
- VRF support
|
||||
- Access List/prefix-list style query control to whitelist or blacklist query targets on a per-VRF basis
|
||||
- REST API with automatic, configurable OpenAPI documentation
|
||||
- Modern, responsive UI built on [ReactJS](https://reactjs.org/), with [NextJS](https://nextjs.org/) & [Chakra UI](https://chakra-ui.com/)
|
||||
- Modern, responsive UI built on [ReactJS](https://reactjs.org/), with [NextJS](https://nextjs.org/) & [Chakra UI](https://chakra-ui.com/), written in [TypeScript](https://www.typescriptlang.org/)
|
||||
- Query multiple devices simultaneously
|
||||
- Browser-based DNS-over-HTTPS resolution of FQDN queries
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ description: Customize the Logo & Favicon
|
|||
| `light` | String | hyperglass logo | Path to logo that will be used in light mode |
|
||||
| `dark` | String | hyperglass logo | Path to logo that will be used in dark mode |
|
||||
| `favicon` | String | hyperglass icon | Path to logo that will be used as the favicon |
|
||||
| `width` | Integer | `75%` | Maximum logo width as a percentage |
|
||||
| `width` | String | `100%` | Maximum logo width as a percentage |
|
||||
| `height` | Integer | | Maximum logo height as a percentage |
|
||||
|
||||
### Favicon
|
||||
|
|
|
|||
|
|
@ -8,19 +8,21 @@ description: Customize the text used in the web UI
|
|||
|
||||
## `text`
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| :--------------- | :----: | :------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `cache_prefix` | String | `'Results cached for '` | Text displayed with the cache timeout countdown. |
|
||||
| `cache_icon` | String | `'Cached Response from {time}'` | Text displayed when a user hovers over the lightning bolt icon, which is displayed when a response from the server was a cached response. `{time}` is replaced with the _original_ query's timestamp. |
|
||||
| `completed_time` | String | `'Completed in {seconds}'` | Text displayed when a user hovers over the success icon for a query result. `{seconds}` will be replaced with 'n seconds' where n is the time a query took to complete. |
|
||||
| `fqdn_tooltip` | String | `'Use {protocol}'` | Text displayed when a user hovers over the IPv4 or IPv6 button on an FQDN target resolved by DNS. `{protocol}` is replaced with the relevant IP protocol. |
|
||||
| `query_location` | String | `'Location'` | Query Location (router) form label. |
|
||||
| `query_target` | String | `'Target'` | Query Target (IP/hostname/community/AS Path) form label. |
|
||||
| `query_type` | String | `'Query Type'` | Query Type (BGP Route, Ping, Traceroute, etc.) form label. |
|
||||
| `query_vrf` | String | `'Routing Table'` | Query VRF form label. |
|
||||
| `subtitle` | String | `'Network Looking Glass'` | Subtitle text. value. |
|
||||
| `title` | String | `'hyperglass'` | Title text. |
|
||||
| `title_mode` | String | `'text_only'` | Set the title mode. <MiniNote>Must be <Code>text_only</Code>, <Code>logo_only</Code>, <Code>logo_subtitle</Code>, or <Code>all</Code></MiniNote> |
|
||||
| Parameter | Type | Default | Description |
|
||||
| :--------------- | :----: | :-------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `cache_prefix` | String | `'Results cached for '` | Text displayed with the cache timeout countdown. |
|
||||
| `cache_icon` | String | `'Cached Response from {time}'` | Text displayed when a user hovers over the lightning bolt icon, which is displayed when a response from the server was a cached response. `{time}` is replaced with the _original_ query's timestamp. |
|
||||
| `completed_time` | String | `'Completed in {seconds}'` | Text displayed when a user hovers over the success icon for a query result. `{seconds}` will be replaced with 'n seconds' where n is the time a query took to complete. |
|
||||
| `fqdn_message` | String | `'Your browser has resolved {fqdn} to'` | Text displayed when prompting a user to select a resolve IPv4 or IPv6 address for an FQDN query. |
|
||||
| `fqdn_error` | String | `'Unable to resolve {fqdn}'` | Text displayed when an FQDN is not resolvable. |
|
||||
| `fqdn_button` | String | `'Try Again'` | Button text used when an FQDN is not resolvable. |
|
||||
| `query_location` | String | `'Location'` | Query Location (router) form label. |
|
||||
| `query_target` | String | `'Target'` | Query Target (IP/hostname/community/AS Path) form label. |
|
||||
| `query_type` | String | `'Query Type'` | Query Type (BGP Route, Ping, Traceroute, etc.) form label. |
|
||||
| `query_vrf` | String | `'Routing Table'` | Query VRF form label. |
|
||||
| `subtitle` | String | `'Network Looking Glass'` | Subtitle text. value. |
|
||||
| `title` | String | `'hyperglass'` | Title text. |
|
||||
| `title_mode` | String | `'text_only'` | Set the title mode. <MiniNote>Must be <Code>text_only</Code>, <Code>logo_only</Code>, <Code>logo_subtitle</Code>, or <Code>all</Code></MiniNote> |
|
||||
|
||||
### Title Mode
|
||||
|
||||
|
|
|
|||
164
hyperglass/api/fake_output.py
Normal file
164
hyperglass/api/fake_output.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
"""Return fake, static data for development purposes."""
|
||||
|
||||
# Standard Library
|
||||
from typing import Dict, Union
|
||||
|
||||
PLAIN = r"""
|
||||
_ _
|
||||
| |__ _ _ _ __ ___ _ __ __ _ | | __ _ ___ ___
|
||||
| '_ \ | | | || '_ \ / _ \| '__|/ _` || | / _` |/ __|/ __|
|
||||
| | | || |_| || |_) || __/| | | (_| || || (_| |\__ \\__ \
|
||||
|_| |_| \__, || .__/ \___||_| \__, ||_| \__,_||___/|___/
|
||||
|___/ |_| |___/
|
||||
|
||||
""" # noqa: W291
|
||||
|
||||
ROUTES = [
|
||||
{
|
||||
"prefix": "198.18.1.0/24",
|
||||
"active": True,
|
||||
"age": 240,
|
||||
"weight": 170,
|
||||
"med": 1,
|
||||
"local_preference": 100,
|
||||
"as_path": [1299, 174, 7018, 7922],
|
||||
"communities": ["65000:1", "65000:101", "65000:102", "65000:103"],
|
||||
"next_hop": "198.18.0.1",
|
||||
"source_as": 7922,
|
||||
"source_rid": "198.18.0.1",
|
||||
"peer_rid": "198.18.0.1",
|
||||
"rpki_state": 1,
|
||||
},
|
||||
{
|
||||
"prefix": "2001:db8:1::/64",
|
||||
"active": True,
|
||||
"age": 240,
|
||||
"weight": 170,
|
||||
"med": 1,
|
||||
"local_preference": 100,
|
||||
"as_path": [1299, 174, 7018, 7922],
|
||||
"communities": ["65000:1", "65000:101", "65000:102", "65000:103"],
|
||||
"next_hop": "2001:db8::1",
|
||||
"source_as": 7922,
|
||||
"source_rid": "198.18.0.1",
|
||||
"peer_rid": "198.18.0.1",
|
||||
"rpki_state": 1,
|
||||
},
|
||||
{
|
||||
"prefix": "198.18.2.0/24",
|
||||
"active": False,
|
||||
"age": 480,
|
||||
"weight": 170,
|
||||
"med": 2,
|
||||
"local_preference": 100,
|
||||
"as_path": [6939, 20115, 20115],
|
||||
"communities": [
|
||||
"65000:2",
|
||||
"65000:201",
|
||||
"198.18.0.2:65000",
|
||||
"198.18.0.2:650201",
|
||||
],
|
||||
"next_hop": "198.18.0.2",
|
||||
"source_as": 20115,
|
||||
"source_rid": "198.18.0.2",
|
||||
"peer_rid": "198.18.0.2",
|
||||
"rpki_state": 2,
|
||||
},
|
||||
{
|
||||
"prefix": "2001:db8:2::/64",
|
||||
"active": False,
|
||||
"age": 480,
|
||||
"weight": 170,
|
||||
"med": 2,
|
||||
"local_preference": 100,
|
||||
"as_path": [6939, 20115, 20115],
|
||||
"communities": [
|
||||
"65000:2",
|
||||
"65000:201",
|
||||
"198.18.0.2:65000",
|
||||
"198.18.0.2:650201",
|
||||
],
|
||||
"next_hop": "2001:db8::2",
|
||||
"source_as": 20115,
|
||||
"source_rid": "198.18.0.2",
|
||||
"peer_rid": "198.18.0.2",
|
||||
"rpki_state": 2,
|
||||
},
|
||||
{
|
||||
"prefix": "198.18.3.0/24",
|
||||
"active": False,
|
||||
"age": 480,
|
||||
"weight": 170,
|
||||
"med": 3,
|
||||
"local_preference": 100,
|
||||
"as_path": [22773],
|
||||
"communities": ["65000:3"],
|
||||
"next_hop": "198.18.0.3",
|
||||
"source_as": 22773,
|
||||
"source_rid": "198.18.0.3",
|
||||
"peer_rid": "198.18.0.3",
|
||||
"rpki_state": 3,
|
||||
},
|
||||
{
|
||||
"prefix": "2001:db8:3::/64",
|
||||
"active": False,
|
||||
"age": 480,
|
||||
"weight": 170,
|
||||
"med": 3,
|
||||
"local_preference": 100,
|
||||
"as_path": [22773],
|
||||
"communities": ["65000:3"],
|
||||
"next_hop": "2001:db8::3",
|
||||
"source_as": 22773,
|
||||
"source_rid": "198.18.0.3",
|
||||
"peer_rid": "198.18.0.3",
|
||||
"rpki_state": 3,
|
||||
},
|
||||
{
|
||||
"prefix": "198.18.4.0/24",
|
||||
"active": False,
|
||||
"age": 480,
|
||||
"weight": 170,
|
||||
"med": 4,
|
||||
"local_preference": 100,
|
||||
"as_path": [],
|
||||
"communities": ["65000:4"],
|
||||
"next_hop": "198.18.0.4",
|
||||
"source_as": 209,
|
||||
"source_rid": "198.18.0.4",
|
||||
"peer_rid": "198.18.0.4",
|
||||
"rpki_state": 0,
|
||||
},
|
||||
{
|
||||
"prefix": "2001:db8:4::/64",
|
||||
"active": False,
|
||||
"age": 480,
|
||||
"weight": 170,
|
||||
"med": 4,
|
||||
"local_preference": 100,
|
||||
"as_path": [],
|
||||
"communities": ["65000:4"],
|
||||
"next_hop": "2001:db8::4",
|
||||
"source_as": 209,
|
||||
"source_rid": "198.18.0.4",
|
||||
"peer_rid": "198.18.0.4",
|
||||
"rpki_state": 0,
|
||||
},
|
||||
]
|
||||
|
||||
STRUCTURED = {
|
||||
"vrf": "default",
|
||||
"count": len(ROUTES),
|
||||
"routes": ROUTES,
|
||||
"winning_weight": "high",
|
||||
}
|
||||
|
||||
|
||||
async def fake_output(structured: bool) -> Union[str, Dict]:
|
||||
"""Bypass the standard execution process and return static, fake output."""
|
||||
output = PLAIN
|
||||
|
||||
if structured:
|
||||
output = STRUCTURED
|
||||
|
||||
return output
|
||||
|
|
@ -23,6 +23,9 @@ from hyperglass.models.api import Query, EncodedRequest
|
|||
from hyperglass.configuration import REDIS_CONFIG, params, devices
|
||||
from hyperglass.execution.main import execute
|
||||
|
||||
# Local
|
||||
from .fake_output import fake_output
|
||||
|
||||
APP_PATH = os.environ["hyperglass_directory"]
|
||||
|
||||
|
||||
|
|
@ -115,9 +118,16 @@ async def query(query_data: Query, request: Request, background_tasks: Backgroun
|
|||
)
|
||||
|
||||
timestamp = query_data.timestamp
|
||||
# Pass request to execution module
|
||||
|
||||
starttime = time.time()
|
||||
cache_output = await execute(query_data)
|
||||
|
||||
if params.fake_output:
|
||||
# Return fake, static data for development purposes, if enabled.
|
||||
cache_output = await fake_output(json_output)
|
||||
else:
|
||||
# Pass request to execution module
|
||||
cache_output = await execute(query_data)
|
||||
|
||||
endtime = time.time()
|
||||
elapsedtime = round(endtime - starttime, 4)
|
||||
log.debug("Query {} took {} seconds to run.", cache_key, elapsedtime)
|
||||
|
|
|
|||
|
|
@ -309,7 +309,12 @@ def _build_networks():
|
|||
"display_name": device.display_name,
|
||||
"network": device.network.display_name,
|
||||
"vrfs": [
|
||||
{"id": vrf.name, "display_name": vrf.display_name}
|
||||
{
|
||||
"id": vrf.name,
|
||||
"display_name": vrf.display_name,
|
||||
"ipv4": True if vrf.ipv4 else False, # noqa: IF100
|
||||
"ipv6": True if vrf.ipv6 else False, # noqa: IF100
|
||||
}
|
||||
for vrf in device.vrfs
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from datetime import datetime
|
||||
|
||||
__name__ = "hyperglass"
|
||||
__version__ = "1.0.0-beta.64"
|
||||
__version__ = "1.0.0-beta.65"
|
||||
__author__ = "Matt Love"
|
||||
__copyright__ = f"Copyright {datetime.now().year} Matthew Love"
|
||||
__license__ = "BSD 3-Clause Clear License"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# Standard Library
|
||||
import sys
|
||||
import json as _json
|
||||
from typing import Dict, List, Union, Optional, Sequence
|
||||
from typing import Dict, List, Union, Optional
|
||||
|
||||
# Third Party
|
||||
from rich.console import Console
|
||||
|
|
@ -190,18 +190,13 @@ class ParsingError(_UnformattedHyperglassError):
|
|||
"""Raised when there is a problem parsing a structured response."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unformatted_msg: Union[Sequence[Dict], str],
|
||||
level: str = "danger",
|
||||
**kwargs,
|
||||
self, unformatted_msg: Union[List[Dict], str], level: str = "danger", **kwargs,
|
||||
) -> None:
|
||||
"""Format error message with keyword arguments."""
|
||||
if isinstance(unformatted_msg, Sequence):
|
||||
if isinstance(unformatted_msg, list):
|
||||
self._message = validation_error_message(*unformatted_msg)
|
||||
else:
|
||||
self._message = unformatted_msg.format(**kwargs)
|
||||
self._level = level or self._level
|
||||
self._keywords = list(kwargs.values())
|
||||
super().__init__(
|
||||
message=self._message, level=self._level, keywords=self._keywords
|
||||
)
|
||||
super().__init__(self._message, level=self._level, keywords=self._keywords)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Base Connection Class."""
|
||||
|
||||
# Standard Library
|
||||
from typing import Iterable
|
||||
from typing import Dict, Union, Sequence
|
||||
|
||||
# Project
|
||||
from hyperglass.log import log
|
||||
|
|
@ -27,8 +27,8 @@ class Connection:
|
|||
self.query = self._query.queries()
|
||||
|
||||
async def parsed_response( # noqa: C901 ("too complex")
|
||||
self, output: Iterable
|
||||
) -> str:
|
||||
self, output: Sequence[str]
|
||||
) -> Union[str, Sequence[Dict]]:
|
||||
"""Send output through common parsers."""
|
||||
|
||||
log.debug("Pre-parsed responses:\n{}", output)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ hyperglass-frr API calls, returns the output back to the front end.
|
|||
|
||||
# Standard Library
|
||||
import signal
|
||||
from typing import Any, Dict, Union, Callable
|
||||
from typing import Any, Dict, Union, Callable, Sequence
|
||||
|
||||
# Project
|
||||
from hyperglass.log import log
|
||||
|
|
@ -36,7 +36,7 @@ def handle_timeout(**exc_args: Any) -> Callable:
|
|||
return handler
|
||||
|
||||
|
||||
async def execute(query: Query) -> Union[str, Dict]:
|
||||
async def execute(query: Query) -> Union[str, Sequence[Dict]]:
|
||||
"""Initiate query validation and execution."""
|
||||
|
||||
output = params.messages.general
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ from typing import Union
|
|||
# Third Party
|
||||
from pydantic import BaseModel, StrictStr
|
||||
|
||||
# Project
|
||||
from hyperglass.models.fields import StrictBytes
|
||||
# Local
|
||||
from ..fields import StrictBytes
|
||||
|
||||
|
||||
class EncodedRequest(BaseModel):
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ class Params(HyperglassModel):
|
|||
title="Developer Mode",
|
||||
description='Enable developer mode. If enabled, the hyperglass backend (Python) and frontend (React/Javascript) applications are "unlinked", so that React tools can be used for front end development. A `<Debugger />` convenience component is also displayed in the UI for easier UI development.',
|
||||
)
|
||||
fake_output: StrictBool = Field(
|
||||
False,
|
||||
title="Fake Output",
|
||||
description="If enabled, the hyperglass backend will return static fake output for development/testing purposes.",
|
||||
)
|
||||
primary_asn: Union[StrictInt, StrictStr] = Field(
|
||||
"65001",
|
||||
title="Primary ASN",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ Percentage = constr(regex=r"^([1-9][0-9]?|100)\%$")
|
|||
TitleMode = constr(regex=("logo_only|text_only|logo_title|logo_subtitle|all"))
|
||||
ColorMode = constr(regex=r"light|dark")
|
||||
DOHProvider = constr(regex="|".join(DNS_OVER_HTTPS.keys()))
|
||||
Title = constr(max_length=32)
|
||||
|
||||
|
||||
class Analytics(HyperglassModel):
|
||||
|
|
@ -102,7 +103,7 @@ class Logo(HyperglassModel):
|
|||
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
|
||||
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
|
||||
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
|
||||
width: Optional[Union[StrictInt, Percentage]] = "75%"
|
||||
width: Optional[Union[StrictInt, Percentage]] = "100%"
|
||||
height: Optional[Union[StrictInt, Percentage]]
|
||||
|
||||
|
||||
|
|
@ -118,13 +119,16 @@ class Text(HyperglassModel):
|
|||
"""Validation model for params.branding.text."""
|
||||
|
||||
title_mode: TitleMode = "logo_only"
|
||||
title: StrictStr = "hyperglass"
|
||||
subtitle: StrictStr = "Network Looking Glass"
|
||||
title: Title = "hyperglass"
|
||||
subtitle: Title = "Network Looking Glass"
|
||||
query_location: StrictStr = "Location"
|
||||
query_type: StrictStr = "Query Type"
|
||||
query_target: StrictStr = "Target"
|
||||
query_vrf: StrictStr = "Routing Table"
|
||||
fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript
|
||||
fqdn_message: StrictStr = "Your browser has resolved {fqdn} to" # Formatted by Javascript
|
||||
fqdn_error: StrictStr = "Unable to resolve {fqdn}" # Formatted by Javascript
|
||||
fqdn_error_button: StrictStr = "Try Again"
|
||||
cache_prefix: StrictStr = "Results cached for "
|
||||
cache_icon: StrictStr = "Cached from {time} UTC" # Formatted by Javascript
|
||||
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ from pydantic import StrictInt, StrictStr, StrictBool, constr, root_validator
|
|||
|
||||
# Project
|
||||
from hyperglass.log import log
|
||||
from hyperglass.models import HyperglassModel
|
||||
from hyperglass.parsing.models.serialized import ParsedRoutes
|
||||
|
||||
# Local
|
||||
from ..main import HyperglassModel
|
||||
from .serialized import ParsedRoutes
|
||||
|
||||
FRRPeerType = constr(regex=r"(internal|external)")
|
||||
|
||||
|
|
@ -8,8 +8,10 @@ from pydantic import StrictInt, StrictStr, StrictBool, validator, root_validator
|
|||
|
||||
# Project
|
||||
from hyperglass.log import log
|
||||
from hyperglass.models import HyperglassModel
|
||||
from hyperglass.parsing.models.serialized import ParsedRoutes
|
||||
|
||||
# Local
|
||||
from ..main import HyperglassModel
|
||||
from .serialized import ParsedRoutes
|
||||
|
||||
RPKI_STATE_MAP = {
|
||||
"invalid": 0,
|
||||
|
|
@ -8,10 +8,12 @@ from typing import List
|
|||
from pydantic import StrictInt, StrictStr, StrictBool, constr, validator
|
||||
|
||||
# Project
|
||||
from hyperglass.models import HyperglassModel
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.external.rpki import rpki_state
|
||||
|
||||
# Local
|
||||
from ..main import HyperglassModel
|
||||
|
||||
WinningWeight = constr(regex=r"(low|high)")
|
||||
|
||||
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"""Parse Juniper XML Response to Structured Data."""
|
||||
|
||||
# Standard Library
|
||||
import re
|
||||
from typing import Dict, Iterable
|
||||
|
||||
# Third Party
|
||||
|
|
@ -11,7 +12,34 @@ from pydantic import ValidationError
|
|||
from hyperglass.log import log
|
||||
from hyperglass.exceptions import ParsingError, ResponseEmpty
|
||||
from hyperglass.configuration import params
|
||||
from hyperglass.parsing.models.juniper import JuniperRoute
|
||||
from hyperglass.models.parsing.juniper import JuniperRoute
|
||||
|
||||
|
||||
def clean_xml_output(output: str) -> str:
|
||||
"""Remove Juniper-specific patterns from output."""
|
||||
patterns = (
|
||||
"""
|
||||
The XML response can a CLI banner appended to the end of the XML
|
||||
string. For example:
|
||||
|
||||
```
|
||||
<rpc-reply>
|
||||
...
|
||||
<cli>
|
||||
<banner>{master}</banner>
|
||||
</cli>
|
||||
</rpc-reply>
|
||||
|
||||
{master}
|
||||
```
|
||||
|
||||
This pattern will remove anything inside braces, including the braces.
|
||||
"""
|
||||
r"\{.+\}",
|
||||
)
|
||||
lines = (line.strip() for line in output.split())
|
||||
scrubbed_lines = (re.sub(pat, "", line) for pat in patterns for line in lines)
|
||||
return "\n".join(scrubbed_lines)
|
||||
|
||||
|
||||
def parse_juniper(output: Iterable) -> Dict: # noqa: C901
|
||||
|
|
@ -19,9 +47,10 @@ def parse_juniper(output: Iterable) -> Dict: # noqa: C901
|
|||
data = {}
|
||||
|
||||
for i, response in enumerate(output):
|
||||
cleaned = clean_xml_output(response)
|
||||
try:
|
||||
parsed = xmltodict.parse(
|
||||
response, force_list=("rt", "rt-entry", "community")
|
||||
cleaned, force_list=("rt", "rt-entry", "community")
|
||||
)
|
||||
|
||||
log.debug("Initially Parsed Response: \n{}", parsed)
|
||||
|
|
@ -49,7 +78,7 @@ def parse_juniper(output: Iterable) -> Dict: # noqa: C901
|
|||
|
||||
except xmltodict.expat.ExpatError as err:
|
||||
log.critical(str(err))
|
||||
raise ParsingError("Error parsing response data")
|
||||
raise ParsingError("Error parsing response data") from err
|
||||
|
||||
except KeyError as err:
|
||||
log.critical("{} was not found in the response", str(err))
|
||||
|
|
|
|||
1
hyperglass/ui/.gitignore
vendored
1
hyperglass/ui/.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
.DS_Store
|
||||
# dev/test files
|
||||
TODO.txt
|
||||
*.tmp*
|
||||
test*
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# hyperglass-ui
|
||||
|
||||
Temporary repo for the permanent [hyperglass](https://github.com/checktheroads/hyperglass) UI, written in [React](https://reactjs.org/), on [Next.js](https://nextjs.org/), with [Chakra UI](https://chakra-ui.com/).
|
||||
[hyperglass](https://github.com/checktheroads/hyperglass) UI, written in [React](https://reactjs.org/), on [Next.js](https://nextjs.org/), with [Chakra UI](https://chakra-ui.com/).
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Icon,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
Tooltip,
|
||||
useColorMode,
|
||||
} from '@chakra-ui/core';
|
||||
import { MdLastPage } from '@meronex/icons/md';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
|
||||
import utcPlugin from 'dayjs/plugin/utc';
|
||||
import { useConfig } from 'app/context';
|
||||
import { Table } from 'app/components';
|
||||
|
||||
dayjs.extend(relativeTimePlugin);
|
||||
dayjs.extend(utcPlugin);
|
||||
|
||||
const isActiveColor = {
|
||||
true: { dark: 'green.300', light: 'green.500' },
|
||||
false: { dark: 'gray.300', light: 'gray.500' },
|
||||
};
|
||||
|
||||
const arrowColor = {
|
||||
true: { dark: 'blackAlpha.500', light: 'blackAlpha.500' },
|
||||
false: { dark: 'whiteAlpha.300', light: 'blackAlpha.500' },
|
||||
};
|
||||
|
||||
const rpkiIcon = ['not-allowed', 'check-circle', 'warning', 'question'];
|
||||
|
||||
const rpkiColor = {
|
||||
true: {
|
||||
dark: ['red.500', 'green.600', 'yellow.500', 'gray.800'],
|
||||
light: ['red.500', 'green.500', 'yellow.500', 'gray.600'],
|
||||
},
|
||||
false: {
|
||||
dark: ['red.300', 'green.300', 'yellow.300', 'gray.300'],
|
||||
light: ['red.400', 'green.500', 'yellow.400', 'gray.500'],
|
||||
},
|
||||
};
|
||||
|
||||
const makeColumns = fields => {
|
||||
return fields.map(pair => {
|
||||
const [header, accessor, align] = pair;
|
||||
let columnConfig = {
|
||||
Header: header,
|
||||
accessor: accessor,
|
||||
align: align,
|
||||
hidden: false,
|
||||
};
|
||||
if (align === null) {
|
||||
columnConfig.hidden = true;
|
||||
}
|
||||
return columnConfig;
|
||||
});
|
||||
};
|
||||
|
||||
const MonoField = ({ v, ...props }) => (
|
||||
<Text fontSize="sm" fontFamily="mono" {...props}>
|
||||
{v}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const Active = ({ isActive }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
return (
|
||||
<Icon name={isActive ? 'check-circle' : 'warning'} color={isActiveColor[isActive][colorMode]} />
|
||||
);
|
||||
};
|
||||
|
||||
const Age = ({ inSeconds }) => {
|
||||
const now = dayjs.utc();
|
||||
const then = now.subtract(inSeconds, 'seconds');
|
||||
return (
|
||||
<Tooltip hasArrow label={then.toString().replace('GMT', 'UTC')} placement="right">
|
||||
<Text fontSize="sm">{now.to(then, true)}</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const Weight = ({ weight, winningWeight }) => {
|
||||
const fixMeText =
|
||||
winningWeight === 'low' ? 'Lower Weight is Preferred' : 'Higher Weight is Preferred';
|
||||
return (
|
||||
<Tooltip hasArrow label={fixMeText} placement="right">
|
||||
<Text fontSize="sm" fontFamily="mono">
|
||||
{weight}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const ASPath = ({ path, active }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
if (path.length === 0) {
|
||||
return <Icon as={MdLastPage} />;
|
||||
}
|
||||
let paths = [];
|
||||
path.map((asn, i) => {
|
||||
const asnStr = String(asn);
|
||||
i !== 0 &&
|
||||
paths.push(
|
||||
<Icon name="chevron-right" key={`separator-${i}`} color={arrowColor[active][colorMode]} />,
|
||||
);
|
||||
paths.push(
|
||||
<Text fontSize="sm" as="span" whiteSpace="pre" fontFamily="mono" key={`as-${asnStr}-${i}`}>
|
||||
{asnStr}
|
||||
</Text>,
|
||||
);
|
||||
});
|
||||
return paths;
|
||||
};
|
||||
|
||||
const Communities = ({ communities }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
let component;
|
||||
communities.length === 0
|
||||
? (component = (
|
||||
<Tooltip placement="right" hasArrow label="No Communities">
|
||||
<Icon name="question-outline" />
|
||||
</Tooltip>
|
||||
))
|
||||
: (component = (
|
||||
<Popover trigger="hover" placement="right">
|
||||
<PopoverTrigger>
|
||||
<Icon name="view" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
textAlign="left"
|
||||
p={4}
|
||||
width="unset"
|
||||
color={colorMode === 'dark' ? 'white' : 'black'}
|
||||
fontFamily="mono"
|
||||
fontWeight="normal"
|
||||
whiteSpace="pre-wrap">
|
||||
<PopoverArrow />
|
||||
{communities.join('\n')}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
));
|
||||
return component;
|
||||
};
|
||||
|
||||
const RPKIState = ({ state, active }) => {
|
||||
const { web } = useConfig();
|
||||
const { colorMode } = useColorMode();
|
||||
const stateText = [
|
||||
web.text.rpki_invalid,
|
||||
web.text.rpki_valid,
|
||||
web.text.rpki_unknown,
|
||||
web.text.rpki_unverified,
|
||||
];
|
||||
return (
|
||||
<Tooltip hasArrow placement="right" label={stateText[state] ?? stateText[3]}>
|
||||
<Icon name={rpkiIcon[state]} color={rpkiColor[active][colorMode][state]} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const Cell = ({ data, rawData, longestASN }) => {
|
||||
const component = {
|
||||
prefix: <MonoField v={data.value} />,
|
||||
active: <Active isActive={data.value} />,
|
||||
age: <Age inSeconds={data.value} />,
|
||||
weight: <Weight weight={data.value} winningWeight={rawData.winning_weight} />,
|
||||
med: <MonoField v={data.value} />,
|
||||
local_preference: <MonoField v={data.value} />,
|
||||
as_path: <ASPath path={data.value} active={data.row.values.active} longestASN={longestASN} />,
|
||||
communities: <Communities communities={data.value} />,
|
||||
next_hop: <MonoField v={data.value} />,
|
||||
source_as: <MonoField v={data.value} />,
|
||||
source_rid: <MonoField v={data.value} />,
|
||||
peer_rid: <MonoField v={data.value} />,
|
||||
rpki_state: <RPKIState state={data.value} active={data.row.values.active} />,
|
||||
};
|
||||
return component[data.column.id] ?? <> </>;
|
||||
};
|
||||
|
||||
export const BGPTable = ({ children: data, ...props }) => {
|
||||
const config = useConfig();
|
||||
const columns = makeColumns(config.parsed_data_fields);
|
||||
|
||||
return (
|
||||
<Flex my={8} maxW={['100%', '100%', '100%', '100%']} w="100%" {...props}>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data.routes}
|
||||
rowHighlightProp="active"
|
||||
cellRender={d => <Cell data={d} rawData={data} />}
|
||||
bordersHorizontal
|
||||
rowHighlightBg="green"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import Countdown, { zeroPad } from 'react-countdown';
|
||||
import { Text, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const bg = { dark: 'white', light: 'black' };
|
||||
|
||||
const Renderer = ({ hours, minutes, seconds, completed, props }) => {
|
||||
if (completed) {
|
||||
return <Text fontSize="xs" />;
|
||||
} else {
|
||||
let time = [zeroPad(seconds)];
|
||||
minutes !== 0 && time.unshift(zeroPad(minutes));
|
||||
hours !== 0 && time.unshift(zeroPad(hours));
|
||||
return (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{props.text}
|
||||
<Text as="span" fontSize="xs" color={bg[props.colorMode]}>
|
||||
{time.join(':')}
|
||||
</Text>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const CacheTimeout = ({ timeout, text }) => {
|
||||
const then = timeout * 1000;
|
||||
const { colorMode } = useColorMode();
|
||||
return (
|
||||
<Countdown
|
||||
date={Date.now() + then}
|
||||
renderer={Renderer}
|
||||
daysInHours
|
||||
text={text}
|
||||
colorMode={colorMode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Flex, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const bg = { light: 'white', dark: 'original.dark' };
|
||||
const color = { light: 'original.dark', dark: 'white' };
|
||||
|
||||
export const CardBody = ({ onClick = () => false, children, ...props }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
return (
|
||||
<Flex
|
||||
w="100%"
|
||||
maxW="100%"
|
||||
rounded="md"
|
||||
borderWidth="1px"
|
||||
direction="column"
|
||||
onClick={onClick}
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}
|
||||
overflow="hidden"
|
||||
{...props}>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Flex, Text, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const bg = { light: 'blackAlpha.50', dark: 'whiteAlpha.100' };
|
||||
|
||||
export const CardHeader = ({ children, ...props }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
return (
|
||||
<Flex
|
||||
bg={bg[colorMode]}
|
||||
p={4}
|
||||
direction="column"
|
||||
roundedTopLeft={4}
|
||||
roundedTopRight={4}
|
||||
borderBottomWidth="1px"
|
||||
{...props}>
|
||||
<Text fontWeight="bold">{children}</Text>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
24
hyperglass/ui/components/Card/body.tsx
Normal file
24
hyperglass/ui/components/Card/body.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { Flex } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/context';
|
||||
|
||||
import type { ICardBody } from './types';
|
||||
|
||||
export const CardBody = (props: ICardBody) => {
|
||||
const { onClick, ...rest } = props;
|
||||
const bg = useColorValue('white', 'dark.500');
|
||||
const color = useColorValue('dark.500', 'white');
|
||||
return (
|
||||
<Flex
|
||||
bg={bg}
|
||||
w="100%"
|
||||
maxW="95%"
|
||||
rounded="md"
|
||||
color={color}
|
||||
onClick={onClick}
|
||||
overflow="hidden"
|
||||
borderWidth="1px"
|
||||
direction="column"
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import * as React from 'react';
|
||||
import { Flex } from '@chakra-ui/core';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
export const CardFooter = ({ children, ...props }) => (
|
||||
import type { ICardFooter } from './types';
|
||||
|
||||
export const CardFooter = (props: ICardFooter) => (
|
||||
<Flex
|
||||
p={4}
|
||||
roundedBottomLeft={4}
|
||||
roundedBottomRight={4}
|
||||
direction="column"
|
||||
borderTopWidth="1px"
|
||||
overflowX="hidden"
|
||||
overflowY="hidden"
|
||||
flexDirection="row"
|
||||
borderTopWidth="1px"
|
||||
roundedBottomLeft={4}
|
||||
roundedBottomRight={4}
|
||||
justifyContent="space-between"
|
||||
{...props}>
|
||||
{children}
|
||||
</Flex>
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
21
hyperglass/ui/components/Card/header.tsx
Normal file
21
hyperglass/ui/components/Card/header.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/context';
|
||||
|
||||
import type { ICardHeader } from './types';
|
||||
|
||||
export const CardHeader = (props: ICardHeader) => {
|
||||
const { children, ...rest } = props;
|
||||
const bg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
|
||||
return (
|
||||
<Flex
|
||||
p={4}
|
||||
bg={bg}
|
||||
direction="column"
|
||||
roundedTopLeft={4}
|
||||
roundedTopRight={4}
|
||||
borderBottomWidth="1px"
|
||||
{...rest}>
|
||||
<Text fontWeight="bold">{children}</Text>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './CardBody';
|
||||
export * from './CardFooter';
|
||||
export * from './CardHeader';
|
||||
3
hyperglass/ui/components/Card/index.ts
Normal file
3
hyperglass/ui/components/Card/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './body';
|
||||
export * from './footer';
|
||||
export * from './header';
|
||||
9
hyperglass/ui/components/Card/types.ts
Normal file
9
hyperglass/ui/components/Card/types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import type { FlexProps } from '@chakra-ui/react';
|
||||
|
||||
export interface ICardBody extends Omit<FlexProps, 'onClick'> {
|
||||
onClick?: () => boolean;
|
||||
}
|
||||
|
||||
export interface ICardFooter extends FlexProps {}
|
||||
|
||||
export interface ICardHeader extends FlexProps {}
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Text, useColorMode, useTheme } from '@chakra-ui/core';
|
||||
import Select from 'react-select';
|
||||
import { opposingColor } from 'app/util';
|
||||
|
||||
export const ChakraSelect = React.forwardRef(
|
||||
({ placeholder = 'Select...', isFullWidth, size, children, ...props }, ref) => {
|
||||
const theme = useTheme();
|
||||
const { colorMode } = useColorMode();
|
||||
const sizeMap = {
|
||||
lg: { height: theme.space[12] },
|
||||
md: { height: theme.space[10] },
|
||||
sm: { height: theme.space[8] },
|
||||
};
|
||||
const colorSetPrimaryBg = {
|
||||
dark: theme.colors.primary[300],
|
||||
light: theme.colors.primary[500],
|
||||
};
|
||||
const colorSetPrimaryColor = opposingColor(theme, colorSetPrimaryBg[colorMode]);
|
||||
const bg = {
|
||||
dark: theme.colors.whiteAlpha[100],
|
||||
light: theme.colors.white,
|
||||
};
|
||||
const color = {
|
||||
dark: theme.colors.whiteAlpha[800],
|
||||
light: theme.colors.black,
|
||||
};
|
||||
const borderFocused = theme.colors.secondary[500];
|
||||
const borderDisabled = theme.colors.whiteAlpha[100];
|
||||
const border = {
|
||||
dark: theme.colors.whiteAlpha[50],
|
||||
light: theme.colors.gray[100],
|
||||
};
|
||||
const borderRadius = theme.space[1];
|
||||
const hoverColor = {
|
||||
dark: theme.colors.whiteAlpha[200],
|
||||
light: theme.colors.gray[300],
|
||||
};
|
||||
const { height } = sizeMap[size];
|
||||
const optionBgActive = {
|
||||
dark: theme.colors.primary[400],
|
||||
light: theme.colors.primary[600],
|
||||
};
|
||||
const optionBgColor = opposingColor(theme, optionBgActive[colorMode]);
|
||||
const optionSelectedBg = {
|
||||
dark: theme.colors.whiteAlpha[400],
|
||||
light: theme.colors.blackAlpha[400],
|
||||
};
|
||||
const optionSelectedColor = opposingColor(theme, optionSelectedBg[colorMode]);
|
||||
const selectedDisabled = theme.colors.whiteAlpha[400];
|
||||
const placeholderColor = {
|
||||
dark: theme.colors.whiteAlpha[700],
|
||||
light: theme.colors.gray[600],
|
||||
};
|
||||
const menuBg = {
|
||||
dark: theme.colors.blackFaded[800],
|
||||
light: theme.colors.whiteFaded[50],
|
||||
};
|
||||
const menuColor = {
|
||||
dark: theme.colors.white,
|
||||
light: theme.colors.blackAlpha[800],
|
||||
};
|
||||
const scrollbar = {
|
||||
dark: theme.colors.whiteAlpha[300],
|
||||
light: theme.colors.blackAlpha[300],
|
||||
};
|
||||
const scrollbarHover = {
|
||||
dark: theme.colors.whiteAlpha[400],
|
||||
light: theme.colors.blackAlpha[400],
|
||||
};
|
||||
const scrollbarBg = {
|
||||
dark: theme.colors.whiteAlpha[50],
|
||||
light: theme.colors.blackAlpha[50],
|
||||
};
|
||||
return (
|
||||
<Select
|
||||
ref={ref}
|
||||
styles={{
|
||||
container: base => ({
|
||||
...base,
|
||||
minHeight: height,
|
||||
borderRadius: borderRadius,
|
||||
width: '100%',
|
||||
}),
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
minHeight: height,
|
||||
backgroundColor: bg[colorMode],
|
||||
color: color[colorMode],
|
||||
borderColor: state.isDisabled
|
||||
? borderDisabled
|
||||
: state.isFocused
|
||||
? borderFocused
|
||||
: border[colorMode],
|
||||
borderRadius: borderRadius,
|
||||
'&:hover': {
|
||||
borderColor: hoverColor[colorMode],
|
||||
},
|
||||
}),
|
||||
menu: base => ({
|
||||
...base,
|
||||
backgroundColor: menuBg[colorMode],
|
||||
borderRadius: borderRadius,
|
||||
}),
|
||||
menuList: base => ({
|
||||
...base,
|
||||
'&::-webkit-scrollbar': { width: '5px' },
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: scrollbarBg[colorMode],
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: scrollbar[colorMode],
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
backgroundColor: scrollbarHover[colorMode],
|
||||
},
|
||||
|
||||
'-ms-overflow-style': { display: 'none' },
|
||||
}),
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
backgroundColor: state.isDisabled
|
||||
? selectedDisabled
|
||||
: state.isSelected
|
||||
? optionSelectedBg[colorMode]
|
||||
: state.isFocused
|
||||
? colorSetPrimaryBg[colorMode]
|
||||
: 'transparent',
|
||||
color: state.isDisabled
|
||||
? selectedDisabled
|
||||
: state.isFocused
|
||||
? colorSetPrimaryColor
|
||||
: state.isSelected
|
||||
? optionSelectedColor
|
||||
: menuColor[colorMode],
|
||||
fontSize: theme.fontSizes[size],
|
||||
'&:active': {
|
||||
backgroundColor: optionBgActive[colorMode],
|
||||
color: optionBgColor,
|
||||
},
|
||||
}),
|
||||
indicatorSeparator: base => ({
|
||||
...base,
|
||||
backgroundColor: placeholderColor[colorMode],
|
||||
}),
|
||||
dropdownIndicator: base => ({
|
||||
...base,
|
||||
color: placeholderColor[colorMode],
|
||||
'&:hover': {
|
||||
color: color[colorMode],
|
||||
},
|
||||
}),
|
||||
valueContainer: base => ({
|
||||
...base,
|
||||
paddingLeft: theme.space[4],
|
||||
paddingRight: theme.space[4],
|
||||
}),
|
||||
multiValue: base => ({
|
||||
...base,
|
||||
backgroundColor: colorSetPrimaryBg[colorMode],
|
||||
}),
|
||||
multiValueLabel: base => ({
|
||||
...base,
|
||||
color: colorSetPrimaryColor,
|
||||
}),
|
||||
multiValueRemove: base => ({
|
||||
...base,
|
||||
color: colorSetPrimaryColor,
|
||||
'&:hover': {
|
||||
color: colorSetPrimaryColor,
|
||||
backgroundColor: 'inherit',
|
||||
},
|
||||
}),
|
||||
singleValue: base => ({
|
||||
...base,
|
||||
color: color[colorMode],
|
||||
fontSize: theme.fontSizes[size],
|
||||
}),
|
||||
}}
|
||||
placeholder={
|
||||
<Text color={placeholderColor[colorMode]} fontSize={size} fontFamily={theme.fonts.body}>
|
||||
{placeholder}
|
||||
</Text>
|
||||
}
|
||||
{...props}>
|
||||
{children}
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Box, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
export const CodeBlock = ({ children }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const bg = { dark: 'gray.800', light: 'blackAlpha.100' };
|
||||
const color = { dark: 'white', light: 'black' };
|
||||
return (
|
||||
<Box
|
||||
fontFamily="mono"
|
||||
mt={5}
|
||||
p={3}
|
||||
border="1px"
|
||||
borderColor="inherit"
|
||||
rounded="md"
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}
|
||||
fontSize="sm"
|
||||
whiteSpace="pre-wrap"
|
||||
as="pre">
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { Button, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const Sun = ({ color, size = '1.5rem', ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
style={{
|
||||
height: size,
|
||||
width: size,
|
||||
}}
|
||||
strokeWidth={0}
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
{...props}>
|
||||
<path
|
||||
d="M256 32a224 224 0 00-161.393 69.035h323.045A224 224 0 00256 32zM79.148 118.965a224 224 0 00-16.976 25.16H449.74a224 224 0 00-16.699-25.16H79.148zm-27.222 45.16A224 224 0 0043.3 186.25h425.271a224 224 0 00-8.586-22.125H51.926zM36.783 210.25a224 224 0 00-3.02 19.125h444.368a224 224 0 00-3.113-19.125H36.783zm-4.752 45.125A224 224 0 0032 256a224 224 0 00.64 16.5h446.534A224 224 0 00480 256a224 224 0 00-.021-.625H32.03zm4.67 45.125a224 224 0 003.395 15.125h431.578a224 224 0 003.861-15.125H36.701zm14.307 45.125a224 224 0 006.017 13.125H454.82a224 224 0 006.342-13.125H51.008zm26.316 45.125a224 224 0 009.04 11.125H425.86a224 224 0 008.727-11.125H77.324zm45.62 45.125A224 224 0 00136.247 445h239.89a224 224 0 0012.936-9.125h-266.13z"
|
||||
fill={color || 'currentColor'}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Moon = ({ color, size = '1.5rem', ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
style={{
|
||||
height: size,
|
||||
width: size,
|
||||
}}
|
||||
strokeWidth={0}
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
{...props}>
|
||||
<path
|
||||
d="M14.53 10.53a7 7 0 01-9.058-9.058A7.003 7.003 0 008 15a7.002 7.002 0 006.53-4.47z"
|
||||
fill={color || 'currentColor'}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const iconMap = { dark: Moon, light: Sun };
|
||||
const outlineColor = { dark: 'primary.300', light: 'primary.600' };
|
||||
|
||||
export const ColorModeToggle = forwardRef((props, ref) => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const Icon = iconMap[colorMode];
|
||||
|
||||
const label = `Switch to ${colorMode === 'light' ? 'dark' : 'light'} mode`;
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
borderWidth="1px"
|
||||
borderColor="transparent"
|
||||
_hover={{
|
||||
backgroundColor: 'unset',
|
||||
borderColor: outlineColor[colorMode],
|
||||
}}
|
||||
color="current"
|
||||
px={4}
|
||||
{...props}>
|
||||
<Icon />
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Text } from '@chakra-ui/core';
|
||||
import { components } from 'react-select';
|
||||
import { ChakraSelect } from 'app/components';
|
||||
|
||||
export const CommunitySelect = ({ name, communities, onChange, register, unregister }) => {
|
||||
const communitySelections = communities.map(c => {
|
||||
return {
|
||||
value: c.community,
|
||||
label: c.display_name,
|
||||
description: c.description,
|
||||
};
|
||||
});
|
||||
const Option = ({ label, data, ...props }) => {
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<Text>{label}</Text>
|
||||
<Text fontSize="xs" as="span">
|
||||
{data.description}
|
||||
</Text>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
register({ name });
|
||||
return () => unregister(name);
|
||||
}, [name, register, unregister]);
|
||||
return (
|
||||
<ChakraSelect
|
||||
innerRef={register}
|
||||
size="lg"
|
||||
name={name}
|
||||
onChange={e => {
|
||||
onChange({ field: name, value: e.value || '' });
|
||||
}}
|
||||
options={communitySelections}
|
||||
components={{ Option }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Button, Icon, Tooltip, useClipboard } from '@chakra-ui/core';
|
||||
|
||||
export const CopyButton = ({ bg = 'secondary', copyValue, ...props }) => {
|
||||
const { onCopy, hasCopied } = useClipboard(copyValue);
|
||||
return (
|
||||
<Tooltip hasArrow label="Copy Output" placement="top">
|
||||
<Button
|
||||
as="a"
|
||||
size="sm"
|
||||
variantColor={bg}
|
||||
zIndex="dropdown"
|
||||
onClick={onCopy}
|
||||
mx={1}
|
||||
{...props}>
|
||||
{hasCopied ? <Icon name="check" size="16px" /> : <Icon name="copy" size="16px" />}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Stack,
|
||||
Tag,
|
||||
useDisclosure,
|
||||
useColorMode,
|
||||
useTheme,
|
||||
} from '@chakra-ui/core';
|
||||
import { useConfig, useMedia } from 'app/context';
|
||||
import { CodeBlock } from 'app/components';
|
||||
|
||||
const prettyMediaSize = {
|
||||
sm: 'SMALL',
|
||||
md: 'MEDIUM',
|
||||
lg: 'LARGE',
|
||||
xl: 'X-LARGE',
|
||||
};
|
||||
|
||||
export const Debugger = () => {
|
||||
const { isOpen: configOpen, onOpen: onConfigOpen, onClose: configClose } = useDisclosure();
|
||||
const { isOpen: themeOpen, onOpen: onThemeOpen, onClose: themeClose } = useDisclosure();
|
||||
const config = useConfig();
|
||||
const theme = useTheme();
|
||||
const bg = { light: 'white', dark: 'black' };
|
||||
const color = { light: 'black', dark: 'white' };
|
||||
const { colorMode } = useColorMode();
|
||||
const { mediaSize } = useMedia();
|
||||
const borderColor = { light: 'gray.100', dark: 'gray.600' };
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor[colorMode]}
|
||||
py={4}
|
||||
px={4}
|
||||
isInline
|
||||
position="relative"
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
justifyContent="center"
|
||||
zIndex={1000}
|
||||
maxW="100%">
|
||||
<Tag variantColor="gray">{colorMode.toUpperCase()}</Tag>
|
||||
<Tag variantColor="teal">{prettyMediaSize[mediaSize]}</Tag>
|
||||
<Button size="sm" variantColor="cyan" onClick={onConfigOpen}>
|
||||
View Config
|
||||
</Button>
|
||||
<Button size="sm" variantColor="purple" onClick={onThemeOpen}>
|
||||
View Theme
|
||||
</Button>
|
||||
</Stack>
|
||||
<Modal isOpen={configOpen} onClose={configClose} size="full">
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}
|
||||
py={4}
|
||||
borderRadius="md"
|
||||
maxW="90%">
|
||||
<ModalHeader>Loaded Configuration</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<CodeBlock>{JSON.stringify(config, null, 4)}</CodeBlock>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal isOpen={themeOpen} onClose={themeClose} size="full">
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}
|
||||
py={4}
|
||||
borderRadius="md"
|
||||
maxW="90%">
|
||||
<ModalHeader>Loaded Theme</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<CodeBlock>{JSON.stringify(theme, null, 4)}</CodeBlock>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Flex, useColorMode } from '@chakra-ui/core';
|
||||
import { FiCode } from '@meronex/icons/fi';
|
||||
import { GoLinkExternal } from '@meronex/icons/go';
|
||||
import format from 'string-format';
|
||||
import { useConfig } from 'app/context';
|
||||
import { FooterButton } from './FooterButton';
|
||||
import { FooterContent } from './FooterContent';
|
||||
|
||||
format.extend(String.prototype, {});
|
||||
|
||||
const footerBg = { light: 'blackAlpha.50', dark: 'whiteAlpha.100' };
|
||||
const footerColor = { light: 'black', dark: 'white' };
|
||||
const contentBorder = { light: 'blackAlpha.100', dark: 'whiteAlpha.200' };
|
||||
|
||||
export const Footer = () => {
|
||||
const config = useConfig();
|
||||
const { colorMode } = useColorMode();
|
||||
const [helpVisible, showHelp] = useState(false);
|
||||
const [termsVisible, showTerms] = useState(false);
|
||||
const [creditVisible, showCredit] = useState(false);
|
||||
const handleCollapse = i => {
|
||||
if (i === 'help') {
|
||||
showTerms(false);
|
||||
showCredit(false);
|
||||
showHelp(!helpVisible);
|
||||
} else if (i === 'credit') {
|
||||
showTerms(false);
|
||||
showHelp(false);
|
||||
showCredit(!creditVisible);
|
||||
} else if (i === 'terms') {
|
||||
showHelp(false);
|
||||
showCredit(false);
|
||||
showTerms(!termsVisible);
|
||||
}
|
||||
};
|
||||
const extUrl = config.web.external_link.url.includes('{primary_asn}')
|
||||
? config.web.external_link.url.format({ primary_asn: config.primary_asn })
|
||||
: config.web.external_link.url || '/';
|
||||
return (
|
||||
<>
|
||||
{config.web.help_menu.enable && (
|
||||
<FooterContent
|
||||
isOpen={helpVisible}
|
||||
content={config.content.help_menu}
|
||||
title={config.web.help_menu.title}
|
||||
bg={footerBg[colorMode]}
|
||||
borderColor={contentBorder[colorMode]}
|
||||
side="left"
|
||||
/>
|
||||
)}
|
||||
{config.web.terms.enable && (
|
||||
<FooterContent
|
||||
isOpen={termsVisible}
|
||||
content={config.content.terms}
|
||||
title={config.web.terms.title}
|
||||
bg={footerBg[colorMode]}
|
||||
borderColor={contentBorder[colorMode]}
|
||||
side="left"
|
||||
/>
|
||||
)}
|
||||
{config.web.credit.enable && (
|
||||
<FooterContent
|
||||
isOpen={creditVisible}
|
||||
content={config.content.credit}
|
||||
title={config.web.credit.title}
|
||||
bg={footerBg[colorMode]}
|
||||
borderColor={contentBorder[colorMode]}
|
||||
side="right"
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
py={[4, 4, 2, 2]}
|
||||
px={6}
|
||||
w="100%"
|
||||
as="footer"
|
||||
flexWrap="wrap"
|
||||
textAlign="center"
|
||||
alignItems="center"
|
||||
bg={footerBg[colorMode]}
|
||||
color={footerColor[colorMode]}
|
||||
justifyContent="space-between">
|
||||
{config.web.terms.enable && (
|
||||
<FooterButton
|
||||
side="left"
|
||||
onClick={() => handleCollapse('terms')}
|
||||
aria-label={config.web.terms.title}>
|
||||
{config.web.terms.title}
|
||||
</FooterButton>
|
||||
)}
|
||||
{config.web.help_menu.enable && (
|
||||
<FooterButton
|
||||
side="left"
|
||||
onClick={() => handleCollapse('help')}
|
||||
aria-label={config.web.help_menu.title}>
|
||||
{config.web.help_menu.title}
|
||||
</FooterButton>
|
||||
)}
|
||||
<Flex
|
||||
flexBasis="auto"
|
||||
flexGrow={0}
|
||||
flexShrink={0}
|
||||
maxWidth="100%"
|
||||
marginRight="auto"
|
||||
p={0}
|
||||
/>
|
||||
{config.web.credit.enable && (
|
||||
<FooterButton
|
||||
side="right"
|
||||
onClick={() => handleCollapse('credit')}
|
||||
aria-label="Powered by hyperglass">
|
||||
<FiCode />
|
||||
</FooterButton>
|
||||
)}
|
||||
{config.web.external_link.enable && (
|
||||
<FooterButton
|
||||
as="a"
|
||||
href={extUrl}
|
||||
aria-label={config.web.external_link.title}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="ghost"
|
||||
rightIcon={GoLinkExternal}
|
||||
size="xs">
|
||||
{config.web.external_link.title}
|
||||
</FooterButton>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Button, Flex } from '@chakra-ui/core';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const AnimatedFlex = motion.custom(Flex);
|
||||
|
||||
export const FooterButton = React.forwardRef(({ onClick, side, children, ...props }, ref) => {
|
||||
return (
|
||||
<AnimatedFlex
|
||||
p={0}
|
||||
w="auto"
|
||||
ref={ref}
|
||||
flexGrow={0}
|
||||
float={side}
|
||||
flexShrink={0}
|
||||
maxWidth="100%"
|
||||
flexBasis="auto"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6 }}>
|
||||
<Button size="xs" variant="ghost" onClick={onClick} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
</AnimatedFlex>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { Box, Collapse } from '@chakra-ui/core';
|
||||
import { Markdown } from 'app/components/Markdown';
|
||||
|
||||
export const FooterContent = forwardRef(
|
||||
({ isOpen = false, content, side = 'left', title, ...props }, ref) => {
|
||||
return (
|
||||
<Collapse
|
||||
px={6}
|
||||
py={4}
|
||||
w="auto"
|
||||
ref={ref}
|
||||
borderBottom="1px"
|
||||
display="flex"
|
||||
maxWidth="100%"
|
||||
isOpen={isOpen}
|
||||
flexBasis="auto"
|
||||
justifyContent={side === 'left' ? 'flex-start' : 'flex-end'}
|
||||
{...props}>
|
||||
<Box textAlign={side}>
|
||||
<Markdown content={content} />
|
||||
</Box>
|
||||
</Collapse>
|
||||
);
|
||||
},
|
||||
);
|
||||
37
hyperglass/ui/components/Footer/button.tsx
Normal file
37
hyperglass/ui/components/Footer/button.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Button, Menu, MenuButton, MenuList } from '@chakra-ui/react';
|
||||
import { Markdown } from '~/components';
|
||||
import { useColorValue, useBreakpointValue } from '~/context';
|
||||
import { useOpposingColor } from '~/hooks';
|
||||
|
||||
import type { TFooterButton } from './types';
|
||||
|
||||
export const FooterButton = (props: TFooterButton) => {
|
||||
const { content, title, side, ...rest } = props;
|
||||
const placement = side === 'left' ? 'top' : side === 'right' ? 'top-start' : undefined;
|
||||
const bg = useColorValue('white', 'gray.900');
|
||||
const color = useOpposingColor(bg);
|
||||
const size = useBreakpointValue({ base: 'xs', lg: 'sm' });
|
||||
return (
|
||||
<Menu placement={placement}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size={size}
|
||||
variant="ghost"
|
||||
aria-label={typeof title === 'string' ? title : undefined}>
|
||||
{title}
|
||||
</MenuButton>
|
||||
<MenuList
|
||||
bg={bg}
|
||||
boxShadow="2xl"
|
||||
color={color}
|
||||
px={6}
|
||||
py={4}
|
||||
textAlign="left"
|
||||
mx={{ base: 1, lg: 2 }}
|
||||
maxW={{ base: '100vw', lg: '50vw' }}
|
||||
{...rest}>
|
||||
<Markdown content={content} />
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
43
hyperglass/ui/components/Footer/colorMode.tsx
Normal file
43
hyperglass/ui/components/Footer/colorMode.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { forwardRef } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Button, Icon, Tooltip } from '@chakra-ui/react';
|
||||
import { If } from '~/components';
|
||||
import { useColorMode, useColorValue, useBreakpointValue } from '~/context';
|
||||
import { useOpposingColor } from '~/hooks';
|
||||
|
||||
import type { TColorModeToggle } from './types';
|
||||
|
||||
const Sun = dynamic<MeronexIcon>(() => import('@meronex/icons/hi').then(i => i.HiSun));
|
||||
const Moon = dynamic<MeronexIcon>(() => import('@meronex/icons/hi').then(i => i.HiMoon));
|
||||
|
||||
export const ColorModeToggle = forwardRef<HTMLButtonElement, TColorModeToggle>((props, ref) => {
|
||||
const { size = '1.5rem', ...rest } = props;
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
|
||||
const bg = useColorValue('primary.500', 'yellow.300');
|
||||
const color = useOpposingColor(bg);
|
||||
const label = useColorValue('Switch to dark mode', 'Switch to light mode');
|
||||
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
|
||||
|
||||
return (
|
||||
<Tooltip hasArrow placement="top-end" label={label} bg={bg} color={color}>
|
||||
<Button
|
||||
ref={ref}
|
||||
size={btnSize}
|
||||
title={label}
|
||||
variant="ghost"
|
||||
aria-label={label}
|
||||
_hover={{ color: bg }}
|
||||
color="currentColor"
|
||||
onClick={toggleColorMode}
|
||||
{...rest}>
|
||||
<If c={colorMode === 'light'}>
|
||||
<Icon as={Moon} boxSize={size} />
|
||||
</If>
|
||||
<If c={colorMode === 'dark'}>
|
||||
<Icon as={Sun} boxSize={size} />
|
||||
</If>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
65
hyperglass/ui/components/Footer/footer.tsx
Normal file
65
hyperglass/ui/components/Footer/footer.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import dynamic from 'next/dynamic';
|
||||
import { Button, Flex, Link, Icon, HStack, useToken } from '@chakra-ui/react';
|
||||
import { If } from '~/components';
|
||||
import { useConfig, useMobile, useColorValue, useBreakpointValue } from '~/context';
|
||||
import { useStrf } from '~/hooks';
|
||||
import { FooterButton } from './button';
|
||||
import { ColorModeToggle } from './colorMode';
|
||||
|
||||
const CodeIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCode));
|
||||
const ExtIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/go').then(i => i.GoLinkExternal));
|
||||
|
||||
export const Footer = () => {
|
||||
const { web, content, primary_asn } = useConfig();
|
||||
|
||||
const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
|
||||
const footerColor = useColorValue('black', 'white');
|
||||
|
||||
const extUrl = useStrf(web.external_link.url, { primary_asn }) ?? '/';
|
||||
|
||||
const size = useBreakpointValue({ base: useToken('sizes', 4), lg: useToken('sizes', 6) });
|
||||
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
|
||||
|
||||
const isMobile = useMobile();
|
||||
|
||||
return (
|
||||
<HStack
|
||||
px={6}
|
||||
py={4}
|
||||
w="100%"
|
||||
zIndex={1}
|
||||
as="footer"
|
||||
bg={footerBg}
|
||||
color={footerColor}
|
||||
spacing={{ base: 8, lg: 6 }}
|
||||
justifyContent={{ base: 'center', lg: 'space-between' }}>
|
||||
<If c={web.terms.enable}>
|
||||
<FooterButton side="left" content={content.terms} title={web.terms.title} />
|
||||
</If>
|
||||
<If c={web.help_menu.enable}>
|
||||
<FooterButton side="left" content={content.help_menu} title={web.help_menu.title} />
|
||||
</If>
|
||||
<If c={web.external_link.enable}>
|
||||
<Button
|
||||
as={Link}
|
||||
isExternal
|
||||
href={extUrl}
|
||||
size={btnSize}
|
||||
variant="ghost"
|
||||
rightIcon={<ExtIcon />}
|
||||
aria-label={web.external_link.title}>
|
||||
{web.external_link.title}
|
||||
</Button>
|
||||
</If>
|
||||
{!isMobile && <Flex p={0} flex="0 0 auto" maxWidth="100%" mr="auto" />}
|
||||
<If c={web.credit.enable}>
|
||||
<FooterButton
|
||||
side="right"
|
||||
content={content.credit}
|
||||
title={<Icon as={CodeIcon} boxSize={size} />}
|
||||
/>
|
||||
</If>
|
||||
<ColorModeToggle size={size} />
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './Footer';
|
||||
1
hyperglass/ui/components/Footer/index.ts
Normal file
1
hyperglass/ui/components/Footer/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './footer';
|
||||
15
hyperglass/ui/components/Footer/types.ts
Normal file
15
hyperglass/ui/components/Footer/types.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { ButtonProps, MenuListProps } from '@chakra-ui/react';
|
||||
|
||||
type TFooterSide = 'left' | 'right';
|
||||
|
||||
export interface TFooterButton extends Omit<MenuListProps, 'title'> {
|
||||
side: TFooterSide;
|
||||
title?: MenuListProps['children'];
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type TFooterItems = 'help' | 'credit' | 'terms';
|
||||
|
||||
export interface TColorModeToggle extends ButtonProps {
|
||||
size?: string;
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Flex, FormControl, FormLabel, FormErrorMessage, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const labelColor = { dark: 'whiteAlpha.700', light: 'blackAlpha.700' };
|
||||
|
||||
export const FormField = ({
|
||||
label,
|
||||
name,
|
||||
error,
|
||||
hiddenLabels,
|
||||
helpIcon,
|
||||
targetInfo,
|
||||
setTarget,
|
||||
labelAddOn,
|
||||
fieldAddOn,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
as={Flex}
|
||||
flexDirection="column"
|
||||
flex={['1 0 100%', '1 0 100%', '1 0 33.33%', '1 0 33.33%']}
|
||||
w="100%"
|
||||
maxW="100%"
|
||||
mx={2}
|
||||
my={[2, 2, 4, 4]}
|
||||
isInvalid={error && error.message}
|
||||
{...props}>
|
||||
<FormLabel
|
||||
htmlFor={name}
|
||||
color={labelColor[colorMode]}
|
||||
pl={1}
|
||||
opacity={hiddenLabels ? 0 : null}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
pr={0}>
|
||||
{label}
|
||||
{labelAddOn || null}
|
||||
</FormLabel>
|
||||
{children}
|
||||
{fieldAddOn && (
|
||||
<Flex justifyContent="flex-end" pt={3}>
|
||||
{fieldAddOn}
|
||||
</Flex>
|
||||
)}
|
||||
<FormErrorMessage opacity={hiddenLabels ? 0 : null}>
|
||||
{error && error.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useColorMode,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/core';
|
||||
import { Markdown } from 'app/components';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const bg = { light: 'white', dark: 'black' };
|
||||
const color = { light: 'black', dark: 'white' };
|
||||
|
||||
const AnimatedModalContent = motion.custom(ModalContent);
|
||||
const AnimatedModalOverlay = motion.custom(ModalOverlay);
|
||||
|
||||
export const Greeting = ({ greetingConfig, content, onClickThrough }) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure(true);
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const handleClick = () => {
|
||||
onClickThrough(true);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClick}
|
||||
isOpen={isOpen}
|
||||
size="full"
|
||||
isCentered
|
||||
closeOnOverlayClick={!greetingConfig.required}
|
||||
closeOnEsc={!greetingConfig.required}>
|
||||
<AnimatedModalOverlay
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.7 }}
|
||||
/>
|
||||
<AnimatedModalContent
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.7 }}
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}
|
||||
py={4}
|
||||
borderRadius="md"
|
||||
maxW={['95%', '75%', '75%', '75%']}>
|
||||
<ModalHeader>{greetingConfig.title}</ModalHeader>
|
||||
{!greetingConfig.required && <ModalCloseButton />}
|
||||
<ModalBody>
|
||||
<Markdown content={content} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variantColor="primary" onClick={handleClick}>
|
||||
{greetingConfig.button}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</AnimatedModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Flex, useColorMode } from '@chakra-ui/core';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useConfig, useHyperglassState, useMedia } from 'app/context';
|
||||
import { Title, ResetButton, ColorModeToggle } from 'app/components';
|
||||
|
||||
const titleVariants = {
|
||||
sm: {
|
||||
fullSize: { scale: 1, marginLeft: 0 },
|
||||
smallLogo: { marginLeft: 'auto' },
|
||||
smallText: { marginLeft: 'auto' },
|
||||
},
|
||||
md: {
|
||||
fullSize: { scale: 1 },
|
||||
smallLogo: { scale: 0.5 },
|
||||
smallText: { scale: 0.8 },
|
||||
},
|
||||
lg: {
|
||||
fullSize: { scale: 1 },
|
||||
smallLogo: { scale: 0.5 },
|
||||
smallText: { scale: 0.8 },
|
||||
},
|
||||
xl: {
|
||||
fullSize: { scale: 1 },
|
||||
smallLogo: { scale: 0.5 },
|
||||
smallText: { scale: 0.8 },
|
||||
},
|
||||
};
|
||||
|
||||
const bg = { light: 'white', dark: 'black' };
|
||||
const headerTransition = {
|
||||
type: 'spring',
|
||||
ease: 'anticipate',
|
||||
damping: 15,
|
||||
stiffness: 100,
|
||||
};
|
||||
const titleJustify = {
|
||||
true: ['flex-end', 'flex-end', 'center', 'center'],
|
||||
false: ['flex-start', 'flex-start', 'center', 'center'],
|
||||
};
|
||||
const titleHeight = {
|
||||
true: null,
|
||||
false: [null, '20vh', '20vh', '20vh'],
|
||||
};
|
||||
const resetButtonMl = { true: [null, 2, 2, 2], false: null };
|
||||
|
||||
const widthMap = {
|
||||
text_only: '100%',
|
||||
logo_only: ['90%', '90%', '50%', '50%'],
|
||||
logo_subtitle: ['90%', '90%', '50%', '50%'],
|
||||
all: ['90%', '90%', '50%', '50%'],
|
||||
};
|
||||
|
||||
export const Header = ({ layoutRef, ...props }) => {
|
||||
const AnimatedFlex = motion.custom(Flex);
|
||||
const AnimatedResetButton = motion.custom(ResetButton);
|
||||
const { colorMode } = useColorMode();
|
||||
const { web } = useConfig();
|
||||
const { mediaSize } = useMedia();
|
||||
const { isSubmitting, resetForm } = useHyperglassState();
|
||||
const handleFormReset = () => {
|
||||
resetForm(layoutRef);
|
||||
};
|
||||
const resetButton = (
|
||||
<AnimatePresence key="resetButton">
|
||||
<AnimatedFlex
|
||||
layoutTransition={headerTransition}
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0, width: 'unset' }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
alignItems="center"
|
||||
mb={[null, 'auto']}
|
||||
ml={resetButtonMl[isSubmitting]}
|
||||
display={isSubmitting ? 'flex' : 'none'}>
|
||||
<AnimatedResetButton isSubmitting={isSubmitting} onClick={handleFormReset} />
|
||||
</AnimatedFlex>
|
||||
</AnimatePresence>
|
||||
);
|
||||
const title = (
|
||||
<AnimatedFlex
|
||||
key="title"
|
||||
px={1}
|
||||
alignItems={isSubmitting ? 'center' : ['center', 'center', 'flex-end', 'flex-end']}
|
||||
positionTransition={headerTransition}
|
||||
initial={{ scale: 0.5 }}
|
||||
animate={
|
||||
isSubmitting && web.text.title_mode === 'text_only'
|
||||
? 'smallText'
|
||||
: isSubmitting && web.text.title_mode !== 'text_only'
|
||||
? 'smallLogo'
|
||||
: 'fullSize'
|
||||
}
|
||||
variants={titleVariants[mediaSize]}
|
||||
justifyContent={titleJustify[isSubmitting]}
|
||||
mt={[null, isSubmitting ? null : 'auto']}
|
||||
maxW={widthMap[web.text.title_mode]}
|
||||
flex="1 0 0"
|
||||
minH={titleHeight[isSubmitting]}>
|
||||
<Title isSubmitting={isSubmitting} onClick={handleFormReset} />
|
||||
</AnimatedFlex>
|
||||
);
|
||||
const colorModeToggle = (
|
||||
<AnimatedFlex
|
||||
layoutTransition={headerTransition}
|
||||
key="colorModeToggle"
|
||||
alignItems="center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
mb={[null, 'auto']}
|
||||
mr={isSubmitting ? null : 2}>
|
||||
<ColorModeToggle />
|
||||
</AnimatedFlex>
|
||||
);
|
||||
const layout = {
|
||||
false: {
|
||||
sm: [title, resetButton, colorModeToggle],
|
||||
md: [resetButton, title, colorModeToggle],
|
||||
lg: [resetButton, title, colorModeToggle],
|
||||
xl: [resetButton, title, colorModeToggle],
|
||||
},
|
||||
true: {
|
||||
sm: [resetButton, colorModeToggle, title],
|
||||
md: [resetButton, title, colorModeToggle],
|
||||
lg: [resetButton, title, colorModeToggle],
|
||||
xl: [resetButton, title, colorModeToggle],
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
px={2}
|
||||
zIndex="4"
|
||||
as="header"
|
||||
width="full"
|
||||
flex="0 1 auto"
|
||||
bg={bg[colorMode]}
|
||||
color="gray.500"
|
||||
{...props}>
|
||||
<Flex
|
||||
w="100%"
|
||||
mx="auto"
|
||||
pt={6}
|
||||
justify="space-between"
|
||||
flex="1 0 auto"
|
||||
alignItems={isSubmitting ? 'center' : 'flex-start'}>
|
||||
{layout[isSubmitting][mediaSize]}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure,
|
||||
useColorMode,
|
||||
useTheme,
|
||||
} from '@chakra-ui/core';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Markdown } from 'app/components';
|
||||
|
||||
const AnimatedIcon = motion.custom(IconButton);
|
||||
|
||||
export const HelpModal = ({ item, name }) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { colors } = useTheme();
|
||||
const { colorMode } = useColorMode();
|
||||
const bg = { light: 'whiteFaded.50', dark: 'blackFaded.800' };
|
||||
const color = { light: 'black', dark: 'white' };
|
||||
const iconColor = {
|
||||
light: colors.primary[500],
|
||||
dark: colors.primary[300],
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
<AnimatedIcon
|
||||
initial={{ opacity: 0, scale: 0.3, color: colors.gray[500] }}
|
||||
animate={{ opacity: 1, scale: 1, color: iconColor[colorMode] }}
|
||||
transition={{ duration: 0.2 }}
|
||||
exit={{ opacity: 0, scale: 0.3 }}
|
||||
variantColor="primary"
|
||||
aria-label={`${name}_help`}
|
||||
icon="info-outline"
|
||||
variant="link"
|
||||
size="sm"
|
||||
h="unset"
|
||||
w={3}
|
||||
minW={3}
|
||||
maxW={3}
|
||||
h={3}
|
||||
minH={3}
|
||||
maxH={3}
|
||||
ml={1}
|
||||
mb={1}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent bg={bg[colorMode]} color={color[colorMode]} py={4} borderRadius="md">
|
||||
<ModalHeader>{item.params.title}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Markdown content={item.content} />
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,251 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { forwardRef, useState, useEffect } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/core';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { intersectionWith, isEqual } from 'lodash';
|
||||
import * as yup from 'yup';
|
||||
import format from 'string-format';
|
||||
import {
|
||||
FormField,
|
||||
HelpModal,
|
||||
QueryLocation,
|
||||
QueryType,
|
||||
QueryTarget,
|
||||
CommunitySelect,
|
||||
QueryVrf,
|
||||
ResolvedTarget,
|
||||
SubmitButton,
|
||||
} from 'app/components';
|
||||
import { useConfig } from 'app/context';
|
||||
|
||||
format.extend(String.prototype, {});
|
||||
|
||||
const formSchema = config =>
|
||||
yup.object().shape({
|
||||
query_location: yup
|
||||
.array()
|
||||
.of(yup.string())
|
||||
.required(
|
||||
config.messages.no_input.format({
|
||||
field: config.web.text.query_location,
|
||||
}),
|
||||
),
|
||||
query_type: yup
|
||||
.string()
|
||||
.required(config.messages.no_input.format({ field: config.web.text.query_type })),
|
||||
query_vrf: yup.string(),
|
||||
query_target: yup
|
||||
.string()
|
||||
.required(config.messages.no_input.format({ field: config.web.text.query_target })),
|
||||
});
|
||||
|
||||
const FormRow = ({ children, ...props }) => (
|
||||
<Flex
|
||||
flexDirection="row"
|
||||
flexWrap="wrap"
|
||||
w="100%"
|
||||
justifyContent={['center', 'center', 'space-between', 'space-between']}
|
||||
{...props}>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
export const HyperglassForm = forwardRef(
|
||||
({ isSubmitting, setSubmitting, setFormData, greetingAck, setGreetingAck, ...props }, ref) => {
|
||||
const config = useConfig();
|
||||
const { handleSubmit, register, unregister, setValue, errors } = useForm({
|
||||
validationSchema: formSchema(config),
|
||||
defaultValues: { query_vrf: 'default', query_target: '' },
|
||||
});
|
||||
|
||||
const [queryLocation, setQueryLocation] = useState([]);
|
||||
const [queryType, setQueryType] = useState('');
|
||||
const [queryVrf, setQueryVrf] = useState('');
|
||||
const [queryTarget, setQueryTarget] = useState('');
|
||||
const [availVrfs, setAvailVrfs] = useState([]);
|
||||
const [fqdnTarget, setFqdnTarget] = useState('');
|
||||
const [displayTarget, setDisplayTarget] = useState('');
|
||||
const [families, setFamilies] = useState([]);
|
||||
const onSubmit = values => {
|
||||
if (!greetingAck && config.web.greeting.required) {
|
||||
window.location.reload(false);
|
||||
setGreetingAck(false);
|
||||
} else {
|
||||
setFormData(values);
|
||||
setSubmitting(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLocChange = locObj => {
|
||||
setQueryLocation(locObj.value);
|
||||
const allVrfs = [];
|
||||
const deviceVrfs = [];
|
||||
locObj.value.map(loc => {
|
||||
const locVrfs = [];
|
||||
config.devices[loc].vrfs.map(vrf => {
|
||||
locVrfs.push({
|
||||
label: vrf.display_name,
|
||||
value: vrf.id,
|
||||
});
|
||||
deviceVrfs.push([{ id: vrf.id, ipv4: vrf.ipv4, ipv6: vrf.ipv6 }]);
|
||||
});
|
||||
allVrfs.push(locVrfs);
|
||||
});
|
||||
|
||||
const intersecting = intersectionWith(...allVrfs, isEqual);
|
||||
setAvailVrfs(intersecting);
|
||||
!intersecting.includes(queryVrf) && queryVrf !== 'default' && setQueryVrf('default');
|
||||
|
||||
let ipv4 = 0;
|
||||
let ipv6 = 0;
|
||||
deviceVrfs.length !== 0 &&
|
||||
intersecting.length !== 0 &&
|
||||
deviceVrfs
|
||||
.filter(v => intersecting.every(i => i.id === v.id))
|
||||
.reduce((a, b) => a.concat(b))
|
||||
.filter(v => v.id === 'default')
|
||||
.map(v => {
|
||||
v.ipv4 === true && ipv4++;
|
||||
v.ipv6 === true && ipv6++;
|
||||
});
|
||||
if (ipv4 !== 0 && ipv4 === ipv6) {
|
||||
setFamilies([4, 6]);
|
||||
} else if (ipv4 > ipv6) {
|
||||
setFamilies([4]);
|
||||
} else if (ipv4 < ipv6) {
|
||||
setFamilies([6]);
|
||||
} else {
|
||||
setFamilies([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
setValue(e.field, e.value);
|
||||
e.field === 'query_location'
|
||||
? handleLocChange(e)
|
||||
: e.field === 'query_type'
|
||||
? setQueryType(e.value)
|
||||
: e.field === 'query_vrf'
|
||||
? setQueryVrf(e.value)
|
||||
: e.field === 'query_target'
|
||||
? setQueryTarget(e.value)
|
||||
: null;
|
||||
};
|
||||
|
||||
const vrfContent = config.content.vrf[queryVrf]?.[queryType];
|
||||
|
||||
const validFqdnQueryType =
|
||||
['ping', 'traceroute', 'bgp_route'].includes(queryType) &&
|
||||
fqdnTarget &&
|
||||
queryVrf === 'default'
|
||||
? fqdnTarget
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
register({ name: 'query_location' });
|
||||
register({ name: 'query_type' });
|
||||
register({ name: 'query_vrf' });
|
||||
}, [register]);
|
||||
Object.keys(errors).length >= 1 && console.error(errors);
|
||||
return (
|
||||
<Box
|
||||
as="form"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
maxW={['100%', '100%', '75%', '75%']}
|
||||
w="100%"
|
||||
p={0}
|
||||
mx="auto"
|
||||
my={4}
|
||||
textAlign="left"
|
||||
ref={ref}
|
||||
{...props}>
|
||||
<FormRow>
|
||||
<FormField
|
||||
label={config.web.text.query_location}
|
||||
name="query_location"
|
||||
error={errors.query_location}>
|
||||
<QueryLocation
|
||||
onChange={handleChange}
|
||||
locations={config.networks}
|
||||
label={config.web.text.query_location}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label={config.web.text.query_type}
|
||||
name="query_type"
|
||||
error={errors.query_type}
|
||||
labelAddOn={vrfContent && <HelpModal item={vrfContent} name="query_type" />}>
|
||||
<QueryType
|
||||
onChange={handleChange}
|
||||
queryTypes={config.queries.list}
|
||||
label={config.web.text.query_type}
|
||||
/>
|
||||
</FormField>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
{availVrfs.length > 1 && (
|
||||
<FormField label={config.web.text.query_vrf} name="query_vrf" error={errors.query_vrf}>
|
||||
<QueryVrf
|
||||
label={config.web.text.query_vrf}
|
||||
vrfs={availVrfs}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField
|
||||
label={config.web.text.query_target}
|
||||
name="query_target"
|
||||
error={errors.query_target}
|
||||
fieldAddOn={
|
||||
queryLocation.length !== 0 &&
|
||||
validFqdnQueryType && (
|
||||
<ResolvedTarget
|
||||
queryTarget={queryTarget}
|
||||
fqdnTarget={validFqdnQueryType}
|
||||
setTarget={handleChange}
|
||||
families={families}
|
||||
availVrfs={availVrfs}
|
||||
/>
|
||||
)
|
||||
}>
|
||||
{queryType === 'bgp_community' && config.queries.bgp_community.mode === 'select' ? (
|
||||
<CommunitySelect
|
||||
label={config.queries.bgp_community.display_name}
|
||||
name="query_target"
|
||||
register={register}
|
||||
unregister={unregister}
|
||||
onChange={handleChange}
|
||||
communities={config.queries.bgp_community.communities}
|
||||
/>
|
||||
) : (
|
||||
<QueryTarget
|
||||
name="query_target"
|
||||
placeholder={config.web.text.query_target}
|
||||
register={register}
|
||||
unregister={unregister}
|
||||
resolveTarget={['ping', 'traceroute', 'bgp_route'].includes(queryType)}
|
||||
value={queryTarget}
|
||||
setFqdn={setFqdnTarget}
|
||||
setTarget={handleChange}
|
||||
displayValue={displayTarget}
|
||||
setDisplayValue={setDisplayTarget}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
</FormRow>
|
||||
<FormRow mt={0} justifyContent="flex-end">
|
||||
<Flex
|
||||
w="100%"
|
||||
maxW="100%"
|
||||
ml="auto"
|
||||
my={2}
|
||||
mr={[0, 0, 2, 2]}
|
||||
flexDirection="column"
|
||||
flex="0 0 0">
|
||||
<SubmitButton isLoading={isSubmitting} />
|
||||
</Flex>
|
||||
</FormRow>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { Flex, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
export const Label = forwardRef(
|
||||
({ value, label, labelColor, valueBg, valueColor, ...props }, ref) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const _labelColor = { dark: 'whiteAlpha.700', light: 'blackAlpha.700' };
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
flexWrap="nowrap"
|
||||
alignItems="center"
|
||||
justifyContent="flex-start"
|
||||
mx={[1, 2, 2, 2]}
|
||||
my={2}
|
||||
{...props}>
|
||||
<Flex
|
||||
display="inline-flex"
|
||||
justifyContent="center"
|
||||
lineHeight="1.5"
|
||||
px={[1, 3, 3, 3]}
|
||||
whiteSpace="nowrap"
|
||||
mb={2}
|
||||
mr={0}
|
||||
bg={valueBg || 'primary.600'}
|
||||
color={valueColor || 'white'}
|
||||
borderBottomLeftRadius={4}
|
||||
borderTopLeftRadius={4}
|
||||
borderBottomRightRadius={0}
|
||||
borderTopRightRadius={0}
|
||||
fontWeight="bold"
|
||||
fontSize={['xs', 'sm', 'sm', 'sm']}>
|
||||
{value}
|
||||
</Flex>
|
||||
<Flex
|
||||
display="inline-flex"
|
||||
justifyContent="center"
|
||||
lineHeight="1.5"
|
||||
px={3}
|
||||
whiteSpace="nowrap"
|
||||
mb={2}
|
||||
ml={0}
|
||||
mr={0}
|
||||
boxShadow={`inset 0px 0px 0px 1px ${valueBg || 'primary.600'}`}
|
||||
color={labelColor || _labelColor[colorMode]}
|
||||
borderBottomRightRadius={4}
|
||||
borderTopRightRadius={4}
|
||||
borderBottomLeftRadius={0}
|
||||
borderTopLeftRadius={0}
|
||||
fontSize={['xs', 'sm', 'sm', 'sm']}>
|
||||
{label}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Flex, useColorMode } from '@chakra-ui/core';
|
||||
import { useConfig, useHyperglassState } from 'app/context';
|
||||
import { Debugger, Greeting, Footer, Header } from 'app/components';
|
||||
|
||||
const bg = { light: 'white', dark: 'black' };
|
||||
const color = { light: 'black', dark: 'white' };
|
||||
|
||||
export const Layout = ({ children }) => {
|
||||
const config = useConfig();
|
||||
const { colorMode } = useColorMode();
|
||||
const { greetingAck, setGreetingAck } = useHyperglassState();
|
||||
const containerRef = useRef(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
w="100%"
|
||||
ref={containerRef}
|
||||
minHeight="100vh"
|
||||
bg={bg[colorMode]}
|
||||
flexDirection="column"
|
||||
color={color[colorMode]}>
|
||||
<Flex px={2} flex="0 1 auto" flexDirection="column">
|
||||
<Header layoutRef={containerRef} />
|
||||
</Flex>
|
||||
<Flex
|
||||
px={2}
|
||||
py={0}
|
||||
w="100%"
|
||||
as="main"
|
||||
flex="1 1 auto"
|
||||
textAlign="center"
|
||||
alignItems="center"
|
||||
justifyContent="start"
|
||||
flexDirection="column">
|
||||
{children}
|
||||
</Flex>
|
||||
<Footer />
|
||||
{config.developer_mode && <Debugger />}
|
||||
</Flex>
|
||||
{config.web.greeting.enable && !greetingAck && (
|
||||
<Greeting
|
||||
greetingConfig={config.web.greeting}
|
||||
content={config.content.greeting}
|
||||
onClickThrough={setGreetingAck}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Flex, Spinner, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
export const Loading = () => {
|
||||
const { colorMode } = useColorMode();
|
||||
const bg = { light: 'white', dark: 'black' };
|
||||
const color = { light: 'black', dark: 'white' };
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
minHeight="100vh"
|
||||
w="100%"
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}>
|
||||
<Flex
|
||||
as="main"
|
||||
w="100%"
|
||||
flexGrow={1}
|
||||
flexShrink={1}
|
||||
flexBasis="auto"
|
||||
alignItems="center"
|
||||
justifyContent="start"
|
||||
textAlign="center"
|
||||
flexDirection="column"
|
||||
px={2}
|
||||
py={0}
|
||||
mt={['50%', '50%', '50%', '25%']}>
|
||||
<Spinner color="primary.500" w="6rem" h="6rem" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Layout, HyperglassForm, Results } from 'app/components';
|
||||
import { useHyperglassState } from 'app/context';
|
||||
|
||||
const AnimatedForm = motion.custom(HyperglassForm);
|
||||
|
||||
export const LookingGlass = () => {
|
||||
const {
|
||||
isSubmitting,
|
||||
setSubmitting,
|
||||
formData,
|
||||
setFormData,
|
||||
greetingAck,
|
||||
setGreetingAck,
|
||||
} = useHyperglassState();
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
{isSubmitting && formData && (
|
||||
<Results
|
||||
queryLocation={formData.query_location}
|
||||
queryType={formData.query_type}
|
||||
queryVrf={formData.query_vrf}
|
||||
queryTarget={formData.query_target}
|
||||
setSubmitting={setSubmitting}
|
||||
/>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{!isSubmitting && (
|
||||
<AnimatedForm
|
||||
initial={{ opacity: 0, y: 300 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
exit={{ opacity: 0, x: -300 }}
|
||||
isSubmitting={isSubmitting}
|
||||
setSubmitting={setSubmitting}
|
||||
setFormData={setFormData}
|
||||
greetingAck={greetingAck}
|
||||
setGreetingAck={setGreetingAck}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Checkbox as ChakraCheckbox,
|
||||
Divider as ChakraDivider,
|
||||
Code as ChakraCode,
|
||||
Heading as ChakraHeading,
|
||||
Link as ChakraLink,
|
||||
List as ChakraList,
|
||||
ListItem as ChakraListItem,
|
||||
Text as ChakraText,
|
||||
} from '@chakra-ui/core';
|
||||
|
||||
import { TableCell, TableHeader, Table as ChakraTable } from './MDTable';
|
||||
|
||||
import { CodeBlock as CustomCodeBlock } from 'app/components';
|
||||
|
||||
export const Checkbox = ({ checked, children }) => (
|
||||
<ChakraCheckbox isChecked={checked}>{children}</ChakraCheckbox>
|
||||
);
|
||||
|
||||
export const List = ({ ordered, children }) => (
|
||||
<ChakraList as={ordered ? 'ol' : 'ul'}>{children}</ChakraList>
|
||||
);
|
||||
|
||||
export const ListItem = ({ checked, children }) =>
|
||||
checked ? (
|
||||
<Checkbox checked={checked}>{children}</Checkbox>
|
||||
) : (
|
||||
<ChakraListItem>{children}</ChakraListItem>
|
||||
);
|
||||
|
||||
export const Heading = ({ level, children }) => {
|
||||
const levelMap = {
|
||||
1: { as: 'h1', size: 'lg', fontWeight: 'bold' },
|
||||
2: { as: 'h2', size: 'lg', fontWeight: 'normal' },
|
||||
3: { as: 'h3', size: 'lg', fontWeight: 'bold' },
|
||||
4: { as: 'h4', size: 'md', fontWeight: 'normal' },
|
||||
5: { as: 'h5', size: 'md', fontWeight: 'bold' },
|
||||
6: { as: 'h6', size: 'sm', fontWeight: 'bold' },
|
||||
};
|
||||
return <ChakraHeading {...levelMap[level]}>{children}</ChakraHeading>;
|
||||
};
|
||||
|
||||
export const Link = ({ children, ...props }) => (
|
||||
<ChakraLink isExternal {...props}>
|
||||
{children}
|
||||
</ChakraLink>
|
||||
);
|
||||
|
||||
export const CodeBlock = ({ value }) => <CustomCodeBlock>{value}</CustomCodeBlock>;
|
||||
|
||||
export const TableData = ({ isHeader, children, ...props }) => {
|
||||
const Component = isHeader ? TableHeader : TableCell;
|
||||
return <Component {...props}>{children}</Component>;
|
||||
};
|
||||
|
||||
export const Paragraph = props => <ChakraText {...props} />;
|
||||
export const InlineCode = props => <ChakraCode {...props} />;
|
||||
export const Divider = props => <ChakraDivider {...props} />;
|
||||
export const Table = props => <ChakraTable {...props} />;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Box, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
export const Table = props => <Box as="table" textAlign="left" mt={4} width="full" {...props} />;
|
||||
|
||||
const bg = { light: 'blackAlpha.50', dark: 'whiteAlpha.50' };
|
||||
|
||||
export const TableHeader = props => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
return <Box as="th" bg={bg[colorMode]} fontWeight="semibold" p={2} fontSize="sm" {...props} />;
|
||||
};
|
||||
|
||||
export const TableCell = ({ isHeader = false, ...props }) => (
|
||||
<Box
|
||||
as={isHeader ? 'th' : 'td'}
|
||||
p={2}
|
||||
borderTopWidth="1px"
|
||||
borderColor="inherit"
|
||||
fontSize="sm"
|
||||
whiteSpace="normal"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import * as React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import {
|
||||
List,
|
||||
|
|
@ -12,9 +10,12 @@ import {
|
|||
InlineCode,
|
||||
Divider,
|
||||
Table,
|
||||
} from './MDComponents';
|
||||
} from './elements';
|
||||
|
||||
const mdComponents = {
|
||||
import type { ReactMarkdownProps } from 'react-markdown';
|
||||
import type { TMarkdown } from './types';
|
||||
|
||||
const renderers = {
|
||||
paragraph: Paragraph,
|
||||
link: Link,
|
||||
heading: Heading,
|
||||
|
|
@ -25,8 +26,8 @@ const mdComponents = {
|
|||
code: CodeBlock,
|
||||
table: Table,
|
||||
tableCell: TableData,
|
||||
};
|
||||
} as ReactMarkdownProps['renderers'];
|
||||
|
||||
export const Markdown = forwardRef(({ content }, ref) => (
|
||||
<ReactMarkdown ref={ref} renderers={mdComponents} source={content} />
|
||||
));
|
||||
export const Markdown = (props: TMarkdown) => (
|
||||
<ReactMarkdown renderers={renderers} source={props.content} />
|
||||
);
|
||||
87
hyperglass/ui/components/Markdown/elements.tsx
Normal file
87
hyperglass/ui/components/Markdown/elements.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import {
|
||||
OrderedList,
|
||||
UnorderedList,
|
||||
Code as ChakraCode,
|
||||
Link as ChakraLink,
|
||||
Text as ChakraText,
|
||||
Divider as ChakraDivider,
|
||||
Heading as ChakraHeading,
|
||||
Checkbox as ChakraCheckbox,
|
||||
ListItem as ChakraListItem,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { TD, TH, Table as ChakraTable } from './table';
|
||||
|
||||
import { CodeBlock as CustomCodeBlock, If } from '~/components';
|
||||
|
||||
import type {
|
||||
BoxProps,
|
||||
TextProps,
|
||||
CodeProps,
|
||||
LinkProps,
|
||||
HeadingProps,
|
||||
DividerProps,
|
||||
} from '@chakra-ui/react';
|
||||
import type { TCheckbox, TList, THeading, TCodeBlock, TTableData, TListItem } from './types';
|
||||
|
||||
export const Checkbox = (props: TCheckbox) => {
|
||||
const { checked, ...rest } = props;
|
||||
return <ChakraCheckbox isChecked={checked} {...rest} />;
|
||||
};
|
||||
|
||||
export const List = (props: TList) => {
|
||||
const { ordered, ...rest } = props;
|
||||
return (
|
||||
<>
|
||||
<If c={ordered}>
|
||||
<OrderedList {...rest} />
|
||||
</If>
|
||||
<If c={!ordered}>
|
||||
<UnorderedList {...rest} />
|
||||
</If>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListItem = (props: TListItem) => {
|
||||
const { checked, ...rest } = props;
|
||||
return checked ? <Checkbox checked={checked} {...rest} /> : <ChakraListItem {...rest} />;
|
||||
};
|
||||
|
||||
export const Heading = (props: THeading) => {
|
||||
const { level, ...rest } = props;
|
||||
|
||||
const levelMap = {
|
||||
1: { as: 'h1', size: 'lg', fontWeight: 'bold' },
|
||||
2: { as: 'h2', size: 'lg', fontWeight: 'normal' },
|
||||
3: { as: 'h3', size: 'lg', fontWeight: 'bold' },
|
||||
4: { as: 'h4', size: 'md', fontWeight: 'normal' },
|
||||
5: { as: 'h5', size: 'md', fontWeight: 'bold' },
|
||||
6: { as: 'h6', size: 'sm', fontWeight: 'bold' },
|
||||
} as { [i: number]: HeadingProps };
|
||||
|
||||
return <ChakraHeading {...levelMap[level]} {...rest} />;
|
||||
};
|
||||
|
||||
export const Link = (props: LinkProps) => <ChakraLink isExternal {...props} />;
|
||||
|
||||
export const CodeBlock = (props: TCodeBlock) => <CustomCodeBlock>{props.value}</CustomCodeBlock>;
|
||||
|
||||
export const TableData = (props: TTableData) => {
|
||||
const { isHeader, ...rest } = props;
|
||||
return (
|
||||
<>
|
||||
<If c={isHeader}>
|
||||
<TH {...rest} />
|
||||
</If>
|
||||
<If c={!isHeader}>
|
||||
<TD {...rest} />
|
||||
</If>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Paragraph = (props: TextProps) => <ChakraText {...props} />;
|
||||
export const InlineCode = (props: CodeProps) => <ChakraCode children={props.children} />;
|
||||
export const Divider = (props: DividerProps) => <ChakraDivider {...props} />;
|
||||
export const Table = (props: BoxProps) => <ChakraTable {...props} />;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './Markdown';
|
||||
1
hyperglass/ui/components/Markdown/index.ts
Normal file
1
hyperglass/ui/components/Markdown/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './markdown';
|
||||
27
hyperglass/ui/components/Markdown/table.tsx
Normal file
27
hyperglass/ui/components/Markdown/table.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Box } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/context';
|
||||
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
export const Table = (props: BoxProps) => (
|
||||
<Box as="table" textAlign="left" mt={4} width="full" {...props} />
|
||||
);
|
||||
|
||||
export const TH = (props: BoxProps) => {
|
||||
const bg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
return <Box as="th" bg={bg} fontWeight="semibold" p={2} fontSize="sm" {...props} />;
|
||||
};
|
||||
|
||||
export const TD = (props: BoxProps) => {
|
||||
return (
|
||||
<Box
|
||||
p={2}
|
||||
as="td"
|
||||
fontSize="sm"
|
||||
whiteSpace="normal"
|
||||
borderTopWidth="1px"
|
||||
borderColor="inherit"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
37
hyperglass/ui/components/Markdown/types.ts
Normal file
37
hyperglass/ui/components/Markdown/types.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type {
|
||||
BoxProps,
|
||||
CheckboxProps,
|
||||
HeadingProps,
|
||||
ListProps,
|
||||
ListItemProps,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
export interface TMarkdown {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface TCheckbox extends CheckboxProps {
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
export interface TListItem {
|
||||
checked: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface TList extends ListProps {
|
||||
ordered: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface THeading extends HeadingProps {
|
||||
level: 1 | 2 | 3 | 4 | 5 | 6;
|
||||
}
|
||||
|
||||
export interface TCodeBlock {
|
||||
value: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface TTableData extends BoxProps {
|
||||
isHeader: boolean;
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useTheme } from '@chakra-ui/core';
|
||||
import { useConfig } from 'app/context';
|
||||
import { googleFontUrl } from 'app/util';
|
||||
|
||||
export const Meta = () => {
|
||||
const config = useConfig();
|
||||
const theme = useTheme();
|
||||
const [location, setLocation] = useState({});
|
||||
const title = config?.site_title || 'hyperglass';
|
||||
const description = config?.site_description || 'Network Looking Glass';
|
||||
const siteName = `${title} - ${description}`;
|
||||
const keywords = config?.site_keywords || [
|
||||
'hyperglass',
|
||||
'looking glass',
|
||||
'lg',
|
||||
'peer',
|
||||
'peering',
|
||||
'ipv4',
|
||||
'ipv6',
|
||||
'transit',
|
||||
'community',
|
||||
'communities',
|
||||
'bgp',
|
||||
'routing',
|
||||
'network',
|
||||
'isp',
|
||||
];
|
||||
const language = config?.language ?? 'en';
|
||||
const primaryFont = googleFontUrl(theme.fonts.body);
|
||||
const monoFont = googleFontUrl(theme.fonts.mono);
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && location === {}) {
|
||||
setLocation(window.location);
|
||||
}
|
||||
}, [location]);
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="hg-version" content={config.hyperglass_version} />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={keywords.join(', ')} />
|
||||
<meta name="language" content={language} />
|
||||
<meta name="url" content={location.href} />
|
||||
<meta name="og:title" content={title} />
|
||||
<meta name="og:url" content={location.href} />
|
||||
<meta name="og:description" content={description} />
|
||||
<meta property="og:image:alt" content={siteName} />
|
||||
<link href={primaryFont} rel="stylesheet" />
|
||||
<link href={monoFont} rel="stylesheet" />
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { ChakraSelect } from 'app/components';
|
||||
|
||||
const buildLocations = networks => {
|
||||
const locations = [];
|
||||
networks.map(net => {
|
||||
const netLocations = [];
|
||||
net.locations.map(loc => {
|
||||
netLocations.push({
|
||||
label: loc.display_name,
|
||||
value: loc.name,
|
||||
group: net.display_name,
|
||||
});
|
||||
});
|
||||
locations.push({ label: net.display_name, options: netLocations });
|
||||
});
|
||||
return locations;
|
||||
};
|
||||
|
||||
export const QueryLocation = ({ locations, onChange, label }) => {
|
||||
const options = buildLocations(locations);
|
||||
const handleChange = e => {
|
||||
const selected = [];
|
||||
e &&
|
||||
e.map(sel => {
|
||||
selected.push(sel.value);
|
||||
});
|
||||
onChange({ field: 'query_location', value: selected });
|
||||
};
|
||||
return (
|
||||
<ChakraSelect
|
||||
isMulti
|
||||
size="lg"
|
||||
options={options}
|
||||
aria-label={label}
|
||||
name="query_location"
|
||||
onChange={handleChange}
|
||||
closeMenuOnSelect={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Input, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const fqdnPattern = /^(?!:\/\/)([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-][a-zA-Z0-9-]+\.[a-zA-Z-]{2,6}?$/gim;
|
||||
|
||||
const bg = { dark: 'whiteAlpha.100', light: 'white' };
|
||||
const color = { dark: 'whiteAlpha.800', light: 'gray.400' };
|
||||
const border = { dark: 'whiteAlpha.50', light: 'gray.100' };
|
||||
const placeholderColor = { dark: 'whiteAlpha.700', light: 'gray.600' };
|
||||
|
||||
export const QueryTarget = ({
|
||||
placeholder,
|
||||
register,
|
||||
unregister,
|
||||
setFqdn,
|
||||
name,
|
||||
value,
|
||||
setTarget,
|
||||
resolveTarget,
|
||||
displayValue,
|
||||
setDisplayValue,
|
||||
}) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const handleBlur = () => {
|
||||
if (resolveTarget && displayValue && fqdnPattern.test(displayValue)) {
|
||||
setFqdn(displayValue);
|
||||
} else if (resolveTarget && !displayValue) {
|
||||
setFqdn(false);
|
||||
}
|
||||
};
|
||||
const handleChange = e => {
|
||||
setDisplayValue(e.target.value);
|
||||
setTarget({ field: name, value: e.target.value });
|
||||
};
|
||||
const handleKeyDown = e => {
|
||||
if ([9, 13].includes(e.keyCode)) {
|
||||
handleBlur();
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
register({ name });
|
||||
return () => unregister(name);
|
||||
}, [register, unregister, name]);
|
||||
return (
|
||||
<>
|
||||
<input hidden readOnly name={name} ref={register} value={value} />
|
||||
<Input
|
||||
size="lg"
|
||||
aria-label={placeholder}
|
||||
name="query_target_display"
|
||||
bg={bg[colorMode]}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={displayValue}
|
||||
borderRadius="0.25rem"
|
||||
onChange={handleChange}
|
||||
color={color[colorMode]}
|
||||
placeholder={placeholder}
|
||||
borderColor={border[colorMode]}
|
||||
_placeholder={{
|
||||
color: placeholderColor[colorMode],
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { ChakraSelect } from 'app/components';
|
||||
|
||||
export const QueryType = ({ queryTypes, onChange, label }) => {
|
||||
const queries = queryTypes
|
||||
.filter(q => q.enable === true)
|
||||
.map(q => {
|
||||
return { value: q.name, label: q.display_name };
|
||||
});
|
||||
return (
|
||||
<ChakraSelect
|
||||
size="lg"
|
||||
name="query_type"
|
||||
onChange={e => onChange({ field: 'query_type', value: e.value })}
|
||||
options={queries}
|
||||
aria-label={label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { ChakraSelect } from 'app/components';
|
||||
|
||||
export const QueryVrf = ({ vrfs, onChange, label }) => (
|
||||
<ChakraSelect
|
||||
size="lg"
|
||||
options={vrfs}
|
||||
name="query_vrf"
|
||||
aria-label={label}
|
||||
onChange={e => onChange({ field: 'query_vrf', value: e.value })}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Button, Icon, Tooltip } from '@chakra-ui/core';
|
||||
|
||||
export const RequeryButton = ({ requery, bg = 'secondary', ...props }) => (
|
||||
<Tooltip hasArrow label="Reload Query" placement="top">
|
||||
<Button mx={1} as="a" size="sm" zIndex="1" variantColor={bg} onClick={requery} {...props}>
|
||||
<Icon size="16px" name="repeat" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Button } from '@chakra-ui/core';
|
||||
import { FiChevronLeft } from '@meronex/icons/fi';
|
||||
|
||||
export const ResetButton = React.forwardRef(({ isSubmitting, onClick }, ref) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
color="current"
|
||||
variant="ghost"
|
||||
onClick={onClick}
|
||||
aria-label="Reset Form"
|
||||
opacity={isSubmitting ? 1 : 0}>
|
||||
<FiChevronLeft size={24} />
|
||||
</Button>
|
||||
));
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { forwardRef, useEffect } from 'react';
|
||||
import { Button, Icon, Spinner, Stack, Tag, Text, Tooltip, useColorMode } from '@chakra-ui/core';
|
||||
import useAxios from 'axios-hooks';
|
||||
import format from 'string-format';
|
||||
import { useConfig } from 'app/context';
|
||||
|
||||
format.extend(String.prototype, {});
|
||||
|
||||
const labelBg = { dark: 'secondary', light: 'secondary' };
|
||||
const labelBgSuccess = { dark: 'success', light: 'success' };
|
||||
|
||||
export const ResolvedTarget = forwardRef(
|
||||
({ fqdnTarget, setTarget, queryTarget, families, availVrfs }, ref) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const config = useConfig();
|
||||
const labelBgStatus = {
|
||||
true: labelBgSuccess[colorMode],
|
||||
false: labelBg[colorMode],
|
||||
};
|
||||
const dnsUrl = config.web.dns_provider.url;
|
||||
const query4 = families.includes(4);
|
||||
const query6 = families.includes(6);
|
||||
const params = {
|
||||
4: {
|
||||
url: dnsUrl,
|
||||
params: { name: fqdnTarget, type: 'A' },
|
||||
headers: { accept: 'application/dns-json' },
|
||||
crossdomain: true,
|
||||
timeout: 1000,
|
||||
},
|
||||
6: {
|
||||
url: dnsUrl,
|
||||
params: { name: fqdnTarget, type: 'AAAA' },
|
||||
headers: { accept: 'application/dns-json' },
|
||||
crossdomain: true,
|
||||
timeout: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
const [{ data: data4, loading: loading4, error: error4 }] = useAxios(params[4]);
|
||||
|
||||
const [{ data: data6, loading: loading6, error: error6 }] = useAxios(params[6]);
|
||||
|
||||
const handleOverride = overridden => {
|
||||
setTarget({ field: 'query_target', value: overridden });
|
||||
};
|
||||
|
||||
const isSelected = value => {
|
||||
return labelBgStatus[value === queryTarget];
|
||||
};
|
||||
|
||||
const findAnswer = data => {
|
||||
return data?.Answer?.filter(answerData => answerData.type === data?.Question[0]?.type)[0]
|
||||
?.data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (query6 && data6?.Answer) {
|
||||
handleOverride(findAnswer(data6));
|
||||
} else if (query4 && data4?.Answer && !query6 && !data6?.Answer) {
|
||||
handleOverride(findAnswer(data4));
|
||||
} else if (query4 && data4?.Answer) {
|
||||
handleOverride(findAnswer(data4));
|
||||
}
|
||||
}, [data4, data6]);
|
||||
return (
|
||||
<Stack
|
||||
ref={ref}
|
||||
isInline
|
||||
w="100%"
|
||||
justifyContent={
|
||||
query4 && data4?.Answer && query6 && data6?.Answer && availVrfs.length > 1
|
||||
? 'space-between'
|
||||
: 'flex-end'
|
||||
}
|
||||
flexWrap="wrap">
|
||||
{loading4 ||
|
||||
error4 ||
|
||||
(query4 && findAnswer(data4) && (
|
||||
<Tag my={2}>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={config.web.text.fqdn_tooltip.format({
|
||||
protocol: 'IPv4',
|
||||
})}
|
||||
placement="bottom">
|
||||
<Button
|
||||
height="unset"
|
||||
minW="unset"
|
||||
fontSize="xs"
|
||||
py="0.1rem"
|
||||
px={2}
|
||||
mr={2}
|
||||
variantColor={labelBgStatus[findAnswer(data4) === queryTarget]}
|
||||
borderRadius="md"
|
||||
onClick={() => handleOverride(findAnswer(data4))}>
|
||||
IPv4
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{loading4 && <Spinner />}
|
||||
{error4 && <Icon name="warning" />}
|
||||
{findAnswer(data4) && (
|
||||
<Text fontSize="xs" fontFamily="mono" as="span" fontWeight={400}>
|
||||
{findAnswer(data4)}
|
||||
</Text>
|
||||
)}
|
||||
</Tag>
|
||||
))}
|
||||
{loading6 ||
|
||||
error6 ||
|
||||
(query6 && findAnswer(data6) && (
|
||||
<Tag my={2}>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={config.web.text.fqdn_tooltip.format({
|
||||
protocol: 'IPv6',
|
||||
})}
|
||||
placement="bottom">
|
||||
<Button
|
||||
height="unset"
|
||||
minW="unset"
|
||||
fontSize="xs"
|
||||
py="0.1rem"
|
||||
px={2}
|
||||
mr={2}
|
||||
variantColor={isSelected(findAnswer(data6))}
|
||||
borderRadius="md"
|
||||
onClick={() => handleOverride(findAnswer(data6))}>
|
||||
IPv6
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{loading6 && <Spinner />}
|
||||
{error6 && <Icon name="warning" />}
|
||||
{findAnswer(data6) && (
|
||||
<Text fontSize="xs" fontFamily="mono" as="span" fontWeight={400}>
|
||||
{findAnswer(data6)}
|
||||
</Text>
|
||||
)}
|
||||
</Tag>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
/** @jsx jsx */
|
||||
import { jsx } from '@emotion/core';
|
||||
import { forwardRef, useEffect, useState } from 'react';
|
||||
import {
|
||||
AccordionItem,
|
||||
AccordionHeader,
|
||||
AccordionPanel,
|
||||
Alert,
|
||||
Box,
|
||||
ButtonGroup,
|
||||
css,
|
||||
Flex,
|
||||
Tooltip,
|
||||
Text,
|
||||
useColorMode,
|
||||
useTheme,
|
||||
} from '@chakra-ui/core';
|
||||
import { BsLightningFill } from '@meronex/icons/bs';
|
||||
import styled from '@emotion/styled';
|
||||
import useAxios from 'axios-hooks';
|
||||
import strReplace from 'react-string-replace';
|
||||
import format from 'string-format';
|
||||
import { startCase } from 'lodash';
|
||||
import { useConfig, useMedia } from 'app/context';
|
||||
import {
|
||||
BGPTable,
|
||||
CacheTimeout,
|
||||
CopyButton,
|
||||
RequeryButton,
|
||||
ResultHeader,
|
||||
TextOutput,
|
||||
} from 'app/components';
|
||||
import { tableToString } from 'app/util';
|
||||
|
||||
format.extend(String.prototype, {});
|
||||
|
||||
const FormattedError = ({ keywords, message }) => {
|
||||
const patternStr = keywords.map(kw => `(${kw})`).join('|');
|
||||
const pattern = new RegExp(patternStr, 'gi');
|
||||
let errorFmt;
|
||||
try {
|
||||
errorFmt = strReplace(message, pattern, match => (
|
||||
<Text key={match} as="strong">
|
||||
{match}
|
||||
</Text>
|
||||
));
|
||||
} catch (err) {
|
||||
errorFmt = <Text as="span">{message}</Text>;
|
||||
}
|
||||
return <Text as="span">{keywords.length !== 0 ? errorFmt : message}</Text>;
|
||||
};
|
||||
|
||||
const AccordionHeaderWrapper = styled(Flex)`
|
||||
justify-content: space-between;
|
||||
&:hover {
|
||||
background-color: ${props => props.hoverBg};
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 'outline';
|
||||
}
|
||||
`;
|
||||
|
||||
const statusMap = {
|
||||
success: 'success',
|
||||
warning: 'warning',
|
||||
error: 'warning',
|
||||
danger: 'error',
|
||||
};
|
||||
|
||||
const color = { dark: 'white', light: 'black' };
|
||||
const scrollbar = { dark: 'whiteAlpha.300', light: 'blackAlpha.300' };
|
||||
const scrollbarHover = { dark: 'whiteAlpha.400', light: 'blackAlpha.400' };
|
||||
const scrollbarBg = { dark: 'whiteAlpha.50', light: 'blackAlpha.50' };
|
||||
|
||||
export const Result = forwardRef(
|
||||
(
|
||||
{
|
||||
device,
|
||||
timeout,
|
||||
queryLocation,
|
||||
queryType,
|
||||
queryVrf,
|
||||
queryTarget,
|
||||
index,
|
||||
resultsComplete,
|
||||
setComplete,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const config = useConfig();
|
||||
const theme = useTheme();
|
||||
const { isSm } = useMedia();
|
||||
const { colorMode } = useColorMode();
|
||||
let [{ data, loading, error }, refetch] = useAxios({
|
||||
url: '/api/query/',
|
||||
method: 'post',
|
||||
data: {
|
||||
query_location: queryLocation,
|
||||
query_type: queryType,
|
||||
query_vrf: queryVrf,
|
||||
query_target: queryTarget,
|
||||
},
|
||||
timeout: timeout,
|
||||
useCache: false,
|
||||
});
|
||||
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [hasOverride, setOverride] = useState(false);
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen(!isOpen);
|
||||
setOverride(true);
|
||||
};
|
||||
|
||||
const errorKw = (error && error.response?.data?.keywords) || [];
|
||||
|
||||
let errorMsg;
|
||||
if (error && error.response?.data?.output) {
|
||||
errorMsg = error.response.data.output;
|
||||
} else if (error && error.message.startsWith('timeout')) {
|
||||
errorMsg = config.messages.request_timeout;
|
||||
} else if (error?.response?.statusText) {
|
||||
errorMsg = startCase(error.response.statusText);
|
||||
} else if (error && error.message) {
|
||||
errorMsg = startCase(error.message);
|
||||
} else {
|
||||
errorMsg = config.messages.general;
|
||||
}
|
||||
|
||||
error && console.error(error);
|
||||
|
||||
const errorLevel =
|
||||
(error?.response?.data?.level && statusMap[error.response?.data?.level]) ?? 'error';
|
||||
|
||||
const structuredDataComponent = {
|
||||
bgp_route: BGPTable,
|
||||
bgp_aspath: BGPTable,
|
||||
bgp_community: BGPTable,
|
||||
ping: TextOutput,
|
||||
traceroute: TextOutput,
|
||||
};
|
||||
|
||||
let Output = TextOutput;
|
||||
let copyValue = data?.output;
|
||||
|
||||
if (data?.format === 'application/json') {
|
||||
Output = structuredDataComponent[queryType];
|
||||
copyValue = tableToString(queryTarget, data, config);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
copyValue = errorMsg;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
!loading && resultsComplete === null && setComplete(index);
|
||||
}, [loading, resultsComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
resultsComplete === index && !hasOverride && setOpen(true);
|
||||
}, [resultsComplete, index]);
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
isOpen={isOpen}
|
||||
isDisabled={loading}
|
||||
ref={ref}
|
||||
css={css({
|
||||
'&:last-of-type': { borderBottom: 'none' },
|
||||
'&:first-of-type': { borderTop: 'none' },
|
||||
})(theme)}>
|
||||
<AccordionHeaderWrapper hoverBg="blackAlpha.50">
|
||||
<AccordionHeader
|
||||
flex="1 0 auto"
|
||||
py={2}
|
||||
_hover={{}}
|
||||
_focus={{}}
|
||||
w="unset"
|
||||
onClick={handleToggle}>
|
||||
<ResultHeader
|
||||
title={device.display_name}
|
||||
loading={loading}
|
||||
error={error}
|
||||
errorMsg={errorMsg}
|
||||
errorLevel={errorLevel}
|
||||
runtime={data?.runtime}
|
||||
/>
|
||||
</AccordionHeader>
|
||||
<ButtonGroup px={[1, 1, 3, 3]} py={2}>
|
||||
<CopyButton copyValue={copyValue} variant="ghost" isDisabled={loading} />
|
||||
<RequeryButton requery={refetch} variant="ghost" isDisabled={loading} />
|
||||
</ButtonGroup>
|
||||
</AccordionHeaderWrapper>
|
||||
<AccordionPanel
|
||||
pb={4}
|
||||
overflowX="auto"
|
||||
css={css({
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
'&::-webkit-scrollbar': { height: '5px' },
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: scrollbarBg[colorMode],
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: scrollbar[colorMode],
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
backgroundColor: scrollbarHover[colorMode],
|
||||
},
|
||||
|
||||
'-ms-overflow-style': { display: 'none' },
|
||||
})(theme)}>
|
||||
<Flex direction="column" flexWrap="wrap">
|
||||
<Flex direction="column" flex="1 0 auto" maxW={error ? '100%' : null}>
|
||||
{!error && data && <Output>{data?.output}</Output>}
|
||||
{error && (
|
||||
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
|
||||
<FormattedError keywords={errorKw} message={errorMsg} />
|
||||
</Alert>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex direction="row" flexWrap="wrap">
|
||||
<Flex
|
||||
px={3}
|
||||
mt={2}
|
||||
justifyContent={['flex-start', 'flex-start', 'flex-end', 'flex-end']}
|
||||
flex="1 0 auto">
|
||||
{config.cache.show_text && data && !error && (
|
||||
<>
|
||||
{!isSm && (
|
||||
<CacheTimeout
|
||||
timeout={config.cache.timeout}
|
||||
text={config.web.text.cache_prefix}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
display={data?.cached ? null : 'none'}
|
||||
hasArrow
|
||||
label={config.web.text.cache_icon.format({
|
||||
time: data?.timestamp,
|
||||
})}
|
||||
placement="top">
|
||||
<Box ml={1} display={data?.cached ? 'block' : 'none'}>
|
||||
<BsLightningFill color={color[colorMode]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{isSm && (
|
||||
<CacheTimeout
|
||||
timeout={config.cache.timeout}
|
||||
text={config.web.text.cache_prefix}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { AccordionIcon, Icon, Spinner, Stack, Text, Tooltip, useColorMode } from '@chakra-ui/core';
|
||||
import format from 'string-format';
|
||||
import { useConfig } from 'app/context';
|
||||
|
||||
format.extend(String.prototype, {});
|
||||
|
||||
const runtimeText = (runtime, text) => {
|
||||
let unit;
|
||||
if (runtime === 1) {
|
||||
unit = 'second';
|
||||
} else {
|
||||
unit = 'seconds';
|
||||
}
|
||||
const fmt = text.format({ seconds: runtime });
|
||||
return `${fmt} ${unit}`;
|
||||
};
|
||||
|
||||
const statusColor = { dark: 'primary.300', light: 'primary.500' };
|
||||
const warningColor = { dark: 300, light: 500 };
|
||||
const defaultStatusColor = {
|
||||
dark: 'success.300',
|
||||
light: 'success.500',
|
||||
};
|
||||
|
||||
export const ResultHeader = forwardRef(
|
||||
({ title, loading, error, errorMsg, errorLevel, runtime }, ref) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const config = useConfig();
|
||||
return (
|
||||
<Stack ref={ref} isInline alignItems="center" w="100%">
|
||||
{loading ? (
|
||||
<Spinner size="sm" mr={4} color={statusColor[colorMode]} />
|
||||
) : error ? (
|
||||
<Tooltip hasArrow label={errorMsg} placement="top">
|
||||
<Icon
|
||||
name="warning"
|
||||
color={`${errorLevel}.${warningColor[colorMode]}`}
|
||||
mr={4}
|
||||
size={6}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={runtimeText(runtime, config.web.text.complete_time)}
|
||||
placement="top">
|
||||
<Icon name="check" color={defaultStatusColor[colorMode]} mr={4} size={6} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Text fontSize="lg">{title}</Text>
|
||||
<AccordionIcon ml="auto" />
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Accordion, Box, Stack, useTheme } from '@chakra-ui/core';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Label, Result } from 'app/components';
|
||||
import { useConfig, useMedia } from 'app/context';
|
||||
|
||||
const AnimatedResult = motion.custom(Result);
|
||||
const AnimatedLabel = motion.custom(Label);
|
||||
|
||||
const labelInitial = {
|
||||
left: {
|
||||
sm: { opacity: 0, x: -100 },
|
||||
md: { opacity: 0, x: -100 },
|
||||
lg: { opacity: 0, x: -100 },
|
||||
xl: { opacity: 0, x: -100 },
|
||||
},
|
||||
center: {
|
||||
sm: { opacity: 0 },
|
||||
md: { opacity: 0 },
|
||||
lg: { opacity: 0 },
|
||||
xl: { opacity: 0 },
|
||||
},
|
||||
right: {
|
||||
sm: { opacity: 0, x: 100 },
|
||||
md: { opacity: 0, x: 100 },
|
||||
lg: { opacity: 0, x: 100 },
|
||||
xl: { opacity: 0, x: 100 },
|
||||
},
|
||||
};
|
||||
const labelAnimate = {
|
||||
left: {
|
||||
sm: { opacity: 1, x: 0 },
|
||||
md: { opacity: 1, x: 0 },
|
||||
lg: { opacity: 1, x: 0 },
|
||||
xl: { opacity: 1, x: 0 },
|
||||
},
|
||||
center: {
|
||||
sm: { opacity: 1 },
|
||||
md: { opacity: 1 },
|
||||
lg: { opacity: 1 },
|
||||
xl: { opacity: 1 },
|
||||
},
|
||||
right: {
|
||||
sm: { opacity: 1, x: 0 },
|
||||
md: { opacity: 1, x: 0 },
|
||||
lg: { opacity: 1, x: 0 },
|
||||
xl: { opacity: 1, x: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
export const Results = ({
|
||||
queryLocation,
|
||||
queryType,
|
||||
queryVrf,
|
||||
queryTarget,
|
||||
setSubmitting,
|
||||
...props
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const theme = useTheme();
|
||||
const { mediaSize } = useMedia();
|
||||
const matchedVrf =
|
||||
config.vrfs.filter(v => v.id === queryVrf)[0] ?? config.vrfs.filter(v => v.id === 'default')[0];
|
||||
const [resultsComplete, setComplete] = useState(null);
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
maxW={['100%', '100%', '75%', '50%']}
|
||||
w="100%"
|
||||
p={0}
|
||||
mx="auto"
|
||||
my={4}
|
||||
textAlign="left"
|
||||
{...props}>
|
||||
<Stack isInline align="center" justify="center" mt={4} flexWrap="wrap">
|
||||
<AnimatePresence>
|
||||
{queryLocation && (
|
||||
<>
|
||||
<AnimatedLabel
|
||||
initial={labelInitial.left[mediaSize]}
|
||||
animate={labelAnimate.left[mediaSize]}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
exit={{ opacity: 0, x: -100 }}
|
||||
label={config.web.text.query_type}
|
||||
value={config.queries[queryType].display_name}
|
||||
valueBg={theme.colors.cyan[500]}
|
||||
fontSize={['xs', 'sm', 'sm', 'sm']}
|
||||
/>
|
||||
<AnimatedLabel
|
||||
initial={labelInitial.center[mediaSize]}
|
||||
animate={labelAnimate.center[mediaSize]}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
label={config.web.text.query_target}
|
||||
value={queryTarget}
|
||||
valueBg={theme.colors.teal[600]}
|
||||
fontSize={['xs', 'sm', 'sm', 'sm']}
|
||||
/>
|
||||
<AnimatedLabel
|
||||
initial={labelInitial.right[mediaSize]}
|
||||
animate={labelAnimate.right[mediaSize]}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
exit={{ opacity: 0, x: 100 }}
|
||||
label={config.web.text.query_vrf}
|
||||
value={matchedVrf.display_name}
|
||||
valueBg={theme.colors.blue[500]}
|
||||
fontSize={['xs', 'sm', 'sm', 'sm']}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box
|
||||
maxW={['100%', '100%', '75%', '75%']}
|
||||
w="100%"
|
||||
p={0}
|
||||
mx="auto"
|
||||
my={4}
|
||||
textAlign="left"
|
||||
borderWidth="1px"
|
||||
rounded="lg"
|
||||
overflow="hidden">
|
||||
<Accordion
|
||||
allowMultiple
|
||||
initial={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 300 }}>
|
||||
<AnimatePresence>
|
||||
{queryLocation &&
|
||||
queryLocation.map((loc, i) => (
|
||||
<AnimatedResult
|
||||
initial={{ opacity: 0, y: 300 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.3 }}
|
||||
exit={{ opacity: 0, y: 300 }}
|
||||
key={loc}
|
||||
timeout={config.request_timeout * 1000}
|
||||
device={config.devices[loc]}
|
||||
queryLocation={loc}
|
||||
queryType={queryType}
|
||||
queryVrf={queryVrf}
|
||||
queryTarget={queryTarget}
|
||||
setSubmitting={setSubmitting}
|
||||
index={i}
|
||||
resultsComplete={resultsComplete}
|
||||
setComplete={setComplete}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</Accordion>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { Box, PseudoBox, Spinner, useColorMode, useTheme } from '@chakra-ui/core';
|
||||
import { FiSearch } from '@meronex/icons/fi';
|
||||
import { opposingColor } from 'app/util';
|
||||
|
||||
const btnProps = {
|
||||
display: 'inline-flex',
|
||||
appearance: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 250ms',
|
||||
userSelect: 'none',
|
||||
position: 'relative',
|
||||
whiteSpace: 'nowrap',
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '1.2',
|
||||
outline: 'none',
|
||||
as: 'button',
|
||||
type: 'submit',
|
||||
borderRadius: 'md',
|
||||
fontWeight: 'semibold',
|
||||
};
|
||||
|
||||
const btnSizeMap = {
|
||||
lg: {
|
||||
height: 12,
|
||||
minWidth: 12,
|
||||
fontSize: 'lg',
|
||||
px: 6,
|
||||
},
|
||||
md: {
|
||||
height: 10,
|
||||
minWidth: 10,
|
||||
fontSize: 'md',
|
||||
px: 4,
|
||||
},
|
||||
sm: {
|
||||
height: 8,
|
||||
minWidth: 8,
|
||||
fontSize: 'sm',
|
||||
px: 3,
|
||||
},
|
||||
xs: {
|
||||
height: 6,
|
||||
minWidth: 6,
|
||||
fontSize: 'xs',
|
||||
px: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const btnBg = { dark: 'primary.300', light: 'primary.500' };
|
||||
const btnBgActive = { dark: 'primary.400', light: 'primary.600' };
|
||||
const btnBgHover = { dark: 'primary.200', light: 'primary.400' };
|
||||
|
||||
export const SubmitButton = forwardRef(
|
||||
(
|
||||
{
|
||||
isLoading = false,
|
||||
isDisabled = false,
|
||||
isActive = false,
|
||||
isFullWidth = false,
|
||||
size = 'lg',
|
||||
loadingText,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const _isDisabled = isDisabled || isLoading;
|
||||
const { colorMode } = useColorMode();
|
||||
const theme = useTheme();
|
||||
const btnColor = opposingColor(theme, btnBg[colorMode]);
|
||||
const btnColorActive = opposingColor(theme, btnBgActive[colorMode]);
|
||||
const btnColorHover = opposingColor(theme, btnBgHover[colorMode]);
|
||||
const btnSize = btnSizeMap[size];
|
||||
return (
|
||||
<PseudoBox
|
||||
ref={ref}
|
||||
disabled={_isDisabled}
|
||||
aria-disabled={_isDisabled}
|
||||
aria-label="Submit Query"
|
||||
width={isFullWidth ? 'full' : undefined}
|
||||
data-active={isActive ? 'true' : undefined}
|
||||
bg={btnBg[colorMode]}
|
||||
color={btnColor}
|
||||
_active={{ bg: btnBgActive[colorMode], color: btnColorActive }}
|
||||
_hover={{ bg: btnBgHover[colorMode], color: btnColorHover }}
|
||||
_focus={{ boxShadow: theme.shadows.outline }}
|
||||
{...btnProps}
|
||||
{...btnSize}
|
||||
{...props}>
|
||||
{isLoading ? (
|
||||
<Spinner
|
||||
position={loadingText ? 'relative' : 'absolute'}
|
||||
mr={loadingText ? 2 : 0}
|
||||
color="currentColor"
|
||||
size="1em"
|
||||
/>
|
||||
) : (
|
||||
<FiSearch color={btnColor} />
|
||||
)}
|
||||
{isLoading
|
||||
? loadingText || (
|
||||
<Box as="span" opacity="0">
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
: children}
|
||||
</PseudoBox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Flex, Icon, Text } from '@chakra-ui/core';
|
||||
import { usePagination, useSortBy, useTable } from 'react-table';
|
||||
import { useMedia } from 'app/context';
|
||||
import { CardBody, CardFooter, CardHeader } from 'app/components';
|
||||
import { TableMain } from './TableMain';
|
||||
import { TableCell } from './TableCell';
|
||||
import { TableHead } from './TableHead';
|
||||
import { TableRow } from './TableRow';
|
||||
import { TableBody } from './TableBody';
|
||||
import { TableIconButton } from './TableIconButton';
|
||||
import { TableSelectShow } from './TableSelectShow';
|
||||
|
||||
export const Table = ({
|
||||
columns,
|
||||
data,
|
||||
tableHeading,
|
||||
initialPageSize = 10,
|
||||
onRowClick,
|
||||
striped = false,
|
||||
bordersVertical = false,
|
||||
bordersHorizontal = false,
|
||||
cellRender = null,
|
||||
rowHighlightProp,
|
||||
rowHighlightBg,
|
||||
rowHighlightColor,
|
||||
}) => {
|
||||
const tableColumns = useMemo(() => columns, [columns]);
|
||||
|
||||
const { isSm, isMd } = useMedia();
|
||||
|
||||
const defaultColumn = useMemo(
|
||||
() => ({
|
||||
minWidth: 100,
|
||||
width: 150,
|
||||
maxWidth: 300,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
let hiddenColumns = [];
|
||||
|
||||
tableColumns.map(col => {
|
||||
if (col.hidden === true) {
|
||||
hiddenColumns.push(col.accessor);
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page,
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable(
|
||||
{
|
||||
columns: tableColumns,
|
||||
defaultColumn,
|
||||
data,
|
||||
initialState: {
|
||||
pageIndex: 0,
|
||||
pageSize: initialPageSize,
|
||||
hiddenColumns: hiddenColumns,
|
||||
},
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
);
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
{!!tableHeading && <CardHeader>{tableHeading}</CardHeader>}
|
||||
<TableMain {...getTableProps()}>
|
||||
<TableHead>
|
||||
{headerGroups.map(headerGroup => (
|
||||
<TableRow key={headerGroup.id} {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map(column => (
|
||||
<TableCell
|
||||
as="th"
|
||||
align={column.align}
|
||||
key={column.id}
|
||||
{...column.getHeaderProps()}
|
||||
{...column.getSortByToggleProps()}>
|
||||
<Text fontSize="sm" fontWeight="bold" display="inline-block">
|
||||
{column.render('Header')}
|
||||
</Text>
|
||||
{column.isSorted ? (
|
||||
column.isSortedDesc ? (
|
||||
<Icon name="chevron-down" size={4} ml={1} />
|
||||
) : (
|
||||
<Icon name="chevron-up" size={4} ml={1} />
|
||||
)
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{page.map(
|
||||
(row, key) =>
|
||||
prepareRow(row) || (
|
||||
<TableRow
|
||||
index={key}
|
||||
doStripe={striped}
|
||||
doHorizontalBorders={bordersHorizontal}
|
||||
onClick={() => onRowClick && onRowClick(row)}
|
||||
key={key}
|
||||
highlight={row.values[rowHighlightProp] ?? false}
|
||||
highlightBg={rowHighlightBg}
|
||||
highlightColor={rowHighlightColor}
|
||||
{...row.getRowProps()}>
|
||||
{row.cells.map((cell, i) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={cell.column.align}
|
||||
cell={cell}
|
||||
bordersVertical={[bordersVertical, i]}
|
||||
key={cell.row.index}
|
||||
{...cell.getCellProps()}>
|
||||
{cell.render(cellRender ?? 'Cell')}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</TableMain>
|
||||
<CardFooter>
|
||||
<Flex direction="row">
|
||||
<TableIconButton
|
||||
mr={2}
|
||||
onClick={() => gotoPage(0)}
|
||||
isDisabled={!canPreviousPage}
|
||||
icon={() => <Icon name="arrow-left" size={3} />}
|
||||
/>
|
||||
<TableIconButton
|
||||
mr={2}
|
||||
onClick={() => previousPage()}
|
||||
isDisabled={!canPreviousPage}
|
||||
icon={() => <Icon name="chevron-left" size={6} />}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justifyContent="center" alignItems="center">
|
||||
<Text fontSize="sm" mr={4} whiteSpace="nowrap">
|
||||
Page{' '}
|
||||
<strong>
|
||||
{pageIndex + 1} of {pageOptions.length}
|
||||
</strong>{' '}
|
||||
</Text>
|
||||
{!(isSm || isMd) && (
|
||||
<TableSelectShow
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex direction="row">
|
||||
<TableIconButton
|
||||
ml={2}
|
||||
isDisabled={!canNextPage}
|
||||
onClick={() => nextPage()}
|
||||
icon={() => <Icon name="chevron-right" size={6} />}
|
||||
/>
|
||||
<TableIconButton
|
||||
ml={2}
|
||||
onClick={() => gotoPage(pageCount ? pageCount - 1 : 1)}
|
||||
isDisabled={!canNextPage}
|
||||
icon={() => <Icon name="arrow-right" size={3} />}
|
||||
/>
|
||||
</Flex>
|
||||
</CardFooter>
|
||||
</CardBody>
|
||||
);
|
||||
};
|
||||
34
hyperglass/ui/components/Table/Table.tsx
Normal file
34
hyperglass/ui/components/Table/Table.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Box } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/context';
|
||||
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
export const TableMain = (props: BoxProps) => {
|
||||
const scrollbar = useColorValue('blackAlpha.300', 'whiteAlpha.300');
|
||||
const scrollbarHover = useColorValue('blackAlpha.400', 'whiteAlpha.400');
|
||||
const scrollbarBg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
return (
|
||||
<Box
|
||||
as="table"
|
||||
display="block"
|
||||
overflowX="auto"
|
||||
borderRadius="md"
|
||||
boxSizing="border-box"
|
||||
css={{
|
||||
'&::-webkit-scrollbar': { height: '5px' },
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: scrollbarBg,
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: scrollbar,
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
backgroundColor: scrollbarHover,
|
||||
},
|
||||
|
||||
'-ms-overflow-style': { display: 'none' },
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
/** @jsx jsx */
|
||||
import { jsx } from '@emotion/core';
|
||||
import { Box, css } from '@chakra-ui/core';
|
||||
|
||||
export const TableBody = ({ children, ...props }) => (
|
||||
<Box
|
||||
as="tbody"
|
||||
overflowY="scroll"
|
||||
css={css({
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
'&': { msOverflowStyle: 'none' },
|
||||
})}
|
||||
overflowX="hidden"
|
||||
{...props}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Box, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const cellBorder = {
|
||||
dark: { borderLeft: '1px', borderLeftColor: 'whiteAlpha.100' },
|
||||
light: { borderLeft: '1px', borderLeftColor: 'blackAlpha.100' },
|
||||
};
|
||||
|
||||
export const TableCell = ({ bordersVertical = [false, 0, 0], align, cell, children, ...props }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const [doVerticalBorders, index] = bordersVertical;
|
||||
let borderProps = {};
|
||||
if (doVerticalBorders && index !== 0) {
|
||||
borderProps = cellBorder[colorMode];
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
as="td"
|
||||
p={4}
|
||||
m={0}
|
||||
w="1%"
|
||||
whiteSpace="nowrap"
|
||||
textAlign={align}
|
||||
{...borderProps}
|
||||
{...props}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Box, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const bg = { dark: 'whiteAlpha.100', light: 'blackAlpha.100' };
|
||||
|
||||
export const TableHead = ({ children, ...props }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
return (
|
||||
<Box as="thead" overflowX="hidden" overflowY="auto" bg={bg[colorMode]} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { IconButton } from '@chakra-ui/core';
|
||||
|
||||
export const TableIconButton = ({ icon, onClick, isDisabled, color, children, ...props }) => (
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon={icon}
|
||||
borderWidth={1}
|
||||
onClick={onClick}
|
||||
variantColor={color}
|
||||
isDisabled={isDisabled}
|
||||
aria-label="Table Icon Button"
|
||||
{...props}>
|
||||
{children}
|
||||
</IconButton>
|
||||
);
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/** @jsx jsx */
|
||||
import { jsx } from '@emotion/core';
|
||||
import { Box, css, useTheme, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const scrollbar = { dark: 'whiteAlpha.300', light: 'blackAlpha.300' };
|
||||
const scrollbarHover = { dark: 'whiteAlpha.400', light: 'blackAlpha.400' };
|
||||
const scrollbarBg = { dark: 'whiteAlpha.50', light: 'blackAlpha.50' };
|
||||
|
||||
export const TableMain = ({ children, ...props }) => {
|
||||
const theme = useTheme();
|
||||
const { colorMode } = useColorMode();
|
||||
return (
|
||||
<Box
|
||||
as="table"
|
||||
display="block"
|
||||
css={css({
|
||||
'&::-webkit-scrollbar': { height: '5px' },
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: scrollbarBg[colorMode],
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: scrollbar[colorMode],
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
backgroundColor: scrollbarHover[colorMode],
|
||||
},
|
||||
|
||||
'-ms-overflow-style': { display: 'none' },
|
||||
})(theme)}
|
||||
overflowX="auto"
|
||||
borderRadius="md"
|
||||
boxSizing="border-box"
|
||||
{...props}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { PseudoBox, useColorMode, useTheme } from '@chakra-ui/core';
|
||||
import { opposingColor } from 'app/util';
|
||||
|
||||
const hoverBg = { dark: 'whiteAlpha.50', light: 'blackAlpha.50' };
|
||||
const bgStripe = { dark: 'whiteAlpha.50', light: 'blackAlpha.50' };
|
||||
const rowBorder = {
|
||||
dark: { borderTop: '1px', borderTopColor: 'whiteAlpha.100' },
|
||||
light: { borderTop: '1px', borderTopColor: 'blackAlpha.100' },
|
||||
};
|
||||
const alphaMap = { dark: '200', light: '100' };
|
||||
const alphaMapHover = { dark: '100', light: '200' };
|
||||
|
||||
export const TableRow = ({
|
||||
highlight = false,
|
||||
highlightBg = 'primary',
|
||||
doStripe = false,
|
||||
doHorizontalBorders = false,
|
||||
index = 0,
|
||||
children = false,
|
||||
...props
|
||||
}) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const theme = useTheme();
|
||||
|
||||
let bg = null;
|
||||
if (highlight) {
|
||||
bg = `${highlightBg}.${alphaMap[colorMode]}`;
|
||||
} else if (doStripe && index % 2 !== 0) {
|
||||
bg = bgStripe[colorMode];
|
||||
}
|
||||
const color = highlight ? opposingColor(theme, bg) : null;
|
||||
|
||||
const borderProps = doHorizontalBorders && index !== 0 ? rowBorder[colorMode] : {};
|
||||
return (
|
||||
<PseudoBox
|
||||
as="tr"
|
||||
_hover={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: highlight
|
||||
? `${highlightBg}.${alphaMapHover[colorMode]}`
|
||||
: hoverBg[colorMode],
|
||||
}}
|
||||
bg={bg}
|
||||
color={color}
|
||||
fontWeight={highlight ? 'bold' : null}
|
||||
{...borderProps}
|
||||
{...props}>
|
||||
{children}
|
||||
</PseudoBox>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Select } from '@chakra-ui/core';
|
||||
|
||||
export const TableSelectShow = ({ value, onChange, children, ...props }) => (
|
||||
<Select size="sm" onChange={onChange} {...props}>
|
||||
{[5, 10, 20, 30, 40, 50].map(value => (
|
||||
<option key={value} value={value}>
|
||||
Show {value}
|
||||
</option>
|
||||
))}
|
||||
{children}
|
||||
</Select>
|
||||
);
|
||||
16
hyperglass/ui/components/Table/body.tsx
Normal file
16
hyperglass/ui/components/Table/body.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
export const TableBody = (props: BoxProps) => (
|
||||
<Box
|
||||
as="tbody"
|
||||
overflowY="scroll"
|
||||
css={{
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
'&': { msOverflowStyle: 'none' },
|
||||
}}
|
||||
overflowX="hidden"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
7
hyperglass/ui/components/Table/button.tsx
Normal file
7
hyperglass/ui/components/Table/button.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { IconButton } from '@chakra-ui/react';
|
||||
|
||||
import type { TTableIconButton } from './types';
|
||||
|
||||
export const TableIconButton = (props: TTableIconButton) => (
|
||||
<IconButton size="sm" borderWidth={1} {...props} aria-label="Table Icon Button" />
|
||||
);
|
||||
28
hyperglass/ui/components/Table/cell.tsx
Normal file
28
hyperglass/ui/components/Table/cell.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Box } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/context';
|
||||
|
||||
import type { TTableCell } from './types';
|
||||
|
||||
export const TableCell = (props: TTableCell) => {
|
||||
const { bordersVertical = [false, 0], align, ...rest } = props;
|
||||
const [doVerticalBorders, index] = bordersVertical;
|
||||
const borderLeftColor = useColorValue('blackAlpha.100', 'whiteAlpha.100');
|
||||
|
||||
let borderProps = {};
|
||||
if (doVerticalBorders && index !== 0) {
|
||||
borderProps = { borderLeft: '1px solid', borderLeftColor };
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={4}
|
||||
m={0}
|
||||
w="1%"
|
||||
as="td"
|
||||
textAlign={align}
|
||||
whiteSpace="nowrap"
|
||||
{...borderProps}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
9
hyperglass/ui/components/Table/head.tsx
Normal file
9
hyperglass/ui/components/Table/head.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Box } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/context';
|
||||
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
export const TableHead = (props: BoxProps) => {
|
||||
const bg = useColorValue('blackAlpha.100', 'whiteAlpha.100');
|
||||
return <Box as="thead" overflowX="hidden" overflowY="auto" bg={bg} {...props} />;
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export * from './Table';
|
||||
export * from './TableBody';
|
||||
export * from './TableCell';
|
||||
export * from './TableHead';
|
||||
export * from './TableIconButton';
|
||||
export * from './TableMain';
|
||||
export * from './TableRow';
|
||||
export * from './TableSelectShow';
|
||||
8
hyperglass/ui/components/Table/index.ts
Normal file
8
hyperglass/ui/components/Table/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export * from './body';
|
||||
export * from './button';
|
||||
export * from './cell';
|
||||
export * from './head';
|
||||
export * from './main';
|
||||
export * from './main';
|
||||
export * from './pageSelect';
|
||||
export * from './row';
|
||||
202
hyperglass/ui/components/Table/main.tsx
Normal file
202
hyperglass/ui/components/Table/main.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import dynamic from 'next/dynamic';
|
||||
import { Flex, Icon, Text } from '@chakra-ui/react';
|
||||
import { usePagination, useSortBy, useTable } from 'react-table';
|
||||
import { useMobile } from '~/context';
|
||||
import { CardBody, CardFooter, CardHeader, If } from '~/components';
|
||||
import { TableMain } from './table';
|
||||
import { TableCell } from './cell';
|
||||
import { TableHead } from './head';
|
||||
import { TableRow } from './row';
|
||||
import { TableBody } from './body';
|
||||
import { TableIconButton } from './button';
|
||||
import { TableSelectShow } from './pageSelect';
|
||||
|
||||
import type { TableOptions, PluginHook } from 'react-table';
|
||||
import type { TCellRender } from '~/types';
|
||||
import type { TTable } from './types';
|
||||
|
||||
const ChevronRight = dynamic<MeronexIcon>(() =>
|
||||
import('@meronex/icons/fa').then(i => i.FaChevronRight),
|
||||
);
|
||||
|
||||
const ChevronLeft = dynamic<MeronexIcon>(() =>
|
||||
import('@meronex/icons/fa').then(i => i.FaChevronLeft),
|
||||
);
|
||||
|
||||
const ChevronDown = dynamic<MeronexIcon>(() =>
|
||||
import('@meronex/icons/fa').then(i => i.FaChevronDown),
|
||||
);
|
||||
|
||||
const DoubleChevronRight = dynamic<MeronexIcon>(() =>
|
||||
import('@meronex/icons/fi').then(i => i.FiChevronsRight),
|
||||
);
|
||||
const DoubleChevronLeft = dynamic<MeronexIcon>(() =>
|
||||
import('@meronex/icons/fi').then(i => i.FiChevronsLeft),
|
||||
);
|
||||
|
||||
export function Table(props: TTable) {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
heading,
|
||||
Cell,
|
||||
rowHighlightBg,
|
||||
striped = false,
|
||||
rowHighlightProp,
|
||||
bordersVertical = false,
|
||||
bordersHorizontal = false,
|
||||
} = props;
|
||||
|
||||
const isMobile = useMobile();
|
||||
|
||||
const defaultColumn = {
|
||||
minWidth: 100,
|
||||
width: 150,
|
||||
maxWidth: 300,
|
||||
};
|
||||
|
||||
let hiddenColumns = [] as string[];
|
||||
|
||||
for (const col of columns) {
|
||||
if (col.hidden) {
|
||||
hiddenColumns.push(col.accessor);
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
columns,
|
||||
defaultColumn,
|
||||
data,
|
||||
initialState: { hiddenColumns },
|
||||
} as TableOptions<TRoute>;
|
||||
|
||||
const plugins = [useSortBy, usePagination] as PluginHook<TRoute>[];
|
||||
|
||||
const instance = useTable<TRoute>(options, ...plugins);
|
||||
|
||||
const {
|
||||
page,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
pageCount,
|
||||
prepareRow,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
setPageSize,
|
||||
headerGroups,
|
||||
previousPage,
|
||||
getTableProps,
|
||||
canPreviousPage,
|
||||
state: { pageIndex, pageSize },
|
||||
} = instance;
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
{heading && <CardHeader>{heading}</CardHeader>}
|
||||
<TableMain {...getTableProps()}>
|
||||
<TableHead>
|
||||
{headerGroups.map((headerGroup, i) => (
|
||||
<TableRow index={i} {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map(column => (
|
||||
<TableCell
|
||||
as="th"
|
||||
align={column.align}
|
||||
{...column.getHeaderProps()}
|
||||
{...column.getSortByToggleProps()}>
|
||||
<Text fontSize="sm" fontWeight="bold" display="inline-block">
|
||||
{column.render('Header')}
|
||||
</Text>
|
||||
<If c={column.isSorted}>
|
||||
<If c={typeof column.isSortedDesc !== 'undefined'}>
|
||||
<Icon as={ChevronDown} boxSize={4} ml={1} />
|
||||
</If>
|
||||
<If c={!column.isSortedDesc}>
|
||||
<Icon as={ChevronRight} boxSize={4} ml={1} />
|
||||
</If>
|
||||
</If>
|
||||
<If c={!column.isSorted}>{''}</If>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{page.map((row, key) => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<TableRow
|
||||
index={key}
|
||||
doStripe={striped}
|
||||
highlightBg={rowHighlightBg}
|
||||
doHorizontalBorders={bordersHorizontal}
|
||||
highlight={row.values[rowHighlightProp ?? ''] ?? false}
|
||||
{...row.getRowProps()}>
|
||||
{row.cells.map((cell, i) => {
|
||||
const { column, row, value } = cell as TCellRender;
|
||||
return (
|
||||
<TableCell
|
||||
align={cell.column.align}
|
||||
bordersVertical={[bordersVertical, i]}
|
||||
{...cell.getCellProps()}>
|
||||
{typeof Cell !== 'undefined' ? (
|
||||
<Cell column={column} row={row} value={value} />
|
||||
) : (
|
||||
cell.render('Cell')
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</TableMain>
|
||||
<CardFooter>
|
||||
<Flex direction="row">
|
||||
<TableIconButton
|
||||
mr={2}
|
||||
onClick={() => gotoPage(0)}
|
||||
isDisabled={!canPreviousPage}
|
||||
icon={<Icon as={DoubleChevronLeft} boxSize={4} />}
|
||||
/>
|
||||
<TableIconButton
|
||||
mr={2}
|
||||
onClick={() => previousPage()}
|
||||
isDisabled={!canPreviousPage}
|
||||
icon={<Icon as={ChevronLeft} boxSize={3} />}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justifyContent="center" alignItems="center">
|
||||
<Text fontSize="sm" mr={4} whiteSpace="nowrap">
|
||||
Page{' '}
|
||||
<strong>
|
||||
{pageIndex + 1} of {pageOptions.length}
|
||||
</strong>{' '}
|
||||
</Text>
|
||||
{!isMobile && (
|
||||
<TableSelectShow
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex direction="row">
|
||||
<TableIconButton
|
||||
ml={2}
|
||||
onClick={nextPage}
|
||||
isDisabled={!canNextPage}
|
||||
icon={<Icon as={ChevronRight} boxSize={3} />}
|
||||
/>
|
||||
<TableIconButton
|
||||
ml={2}
|
||||
isDisabled={!canNextPage}
|
||||
icon={<Icon as={DoubleChevronRight} boxSize={4} />}
|
||||
onClick={() => gotoPage(pageCount ? pageCount - 1 : 1)}
|
||||
/>
|
||||
</Flex>
|
||||
</CardFooter>
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
15
hyperglass/ui/components/Table/pageSelect.tsx
Normal file
15
hyperglass/ui/components/Table/pageSelect.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Select } from '@chakra-ui/react';
|
||||
import { SelectProps } from '@chakra-ui/react';
|
||||
|
||||
export const TableSelectShow = (props: SelectProps) => {
|
||||
const { value, ...rest } = props;
|
||||
return (
|
||||
<Select size="sm" {...rest}>
|
||||
{[5, 10, 20, 30, 40, 50].map(value => (
|
||||
<option key={value} value={value}>
|
||||
Show {value}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
51
hyperglass/ui/components/Table/row.tsx
Normal file
51
hyperglass/ui/components/Table/row.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { Box } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/context';
|
||||
import { useOpposingColor } from '~/hooks';
|
||||
|
||||
import type { TTableRow } from './types';
|
||||
|
||||
export const TableRow = (props: TTableRow) => {
|
||||
const {
|
||||
index = 0,
|
||||
doStripe = false,
|
||||
highlight = false,
|
||||
highlightBg = 'primary',
|
||||
doHorizontalBorders = false,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const alpha = useColorValue('100', '200');
|
||||
const alphaHover = useColorValue('200', '100');
|
||||
const bgStripe = useColorValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
let hoverBg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
const rowBorder = useColorValue(
|
||||
{ borderTop: '1px', borderTopColor: 'blackAlpha.100' },
|
||||
{ borderTop: '1px', borderTopColor: 'whiteAlpha.100' },
|
||||
);
|
||||
let bg;
|
||||
|
||||
if (highlight) {
|
||||
bg = `${String(highlightBg)}.${alpha}`;
|
||||
hoverBg = `${String(highlightBg)}.${alphaHover}`;
|
||||
} else if (doStripe && index % 2 !== 0) {
|
||||
bg = bgStripe;
|
||||
}
|
||||
const defaultBg = useColorValue('white', 'black');
|
||||
const color = useOpposingColor(bg ?? defaultBg);
|
||||
const borderProps = doHorizontalBorders && index !== 0 ? rowBorder : {};
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="tr"
|
||||
bg={bg}
|
||||
css={{ '& > td': { color } }}
|
||||
fontWeight={highlight ? 'bold' : undefined}
|
||||
_hover={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: highlight ? `${String(highlightBg)}.${alphaHover}` : hoverBg,
|
||||
}}
|
||||
{...borderProps}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
30
hyperglass/ui/components/Table/types.ts
Normal file
30
hyperglass/ui/components/Table/types.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { BoxProps, IconButtonProps } from '@chakra-ui/react';
|
||||
|
||||
import type { Theme, TColumn, TCellRender } from '~/types';
|
||||
|
||||
export interface TTable {
|
||||
data: TRoute[];
|
||||
striped?: boolean;
|
||||
columns: TColumn[];
|
||||
heading?: React.ReactNode;
|
||||
bordersVertical?: boolean;
|
||||
bordersHorizontal?: boolean;
|
||||
Cell?: React.FC<TCellRender>;
|
||||
rowHighlightProp?: keyof IRoute;
|
||||
rowHighlightBg?: Theme.ColorNames;
|
||||
}
|
||||
|
||||
export interface TTableCell extends Omit<BoxProps, 'align'> {
|
||||
bordersVertical?: [boolean, number];
|
||||
align?: 'left' | 'right' | 'center';
|
||||
}
|
||||
|
||||
export interface TTableRow extends BoxProps {
|
||||
highlightBg?: Theme.ColorNames;
|
||||
doHorizontalBorders?: boolean;
|
||||
highlight?: boolean;
|
||||
doStripe?: boolean;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export type TTableIconButton = Omit<IconButtonProps, 'aria-label'>;
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
/** @jsx jsx */
|
||||
import { jsx } from '@emotion/core';
|
||||
import { Box, css, useColorMode } from '@chakra-ui/core';
|
||||
|
||||
const bg = { dark: 'gray.800', light: 'blackAlpha.100' };
|
||||
const color = { dark: 'white', light: 'black' };
|
||||
const selectionBg = { dark: 'white', light: 'black' };
|
||||
const selectionColor = { dark: 'black', light: 'white' };
|
||||
|
||||
export const TextOutput = ({ children, ...props }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
return (
|
||||
<Box
|
||||
fontFamily="mono"
|
||||
mt={5}
|
||||
mx={2}
|
||||
p={3}
|
||||
border="1px"
|
||||
borderColor="inherit"
|
||||
rounded="md"
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}
|
||||
fontSize="sm"
|
||||
whiteSpace="pre-wrap"
|
||||
as="pre"
|
||||
css={css({
|
||||
'&::selection': {
|
||||
backgroundColor: selectionBg[colorMode],
|
||||
color: selectionColor[colorMode],
|
||||
},
|
||||
})}
|
||||
{...props}>
|
||||
{children
|
||||
.split('\\n')
|
||||
.join('\n')
|
||||
.replace(/\n\n/g, '\n')}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
/** @jsx jsx */
|
||||
import { jsx } from '@emotion/core';
|
||||
import { forwardRef } from 'react';
|
||||
import { Button, Heading, Image, Stack, useColorMode } from '@chakra-ui/core';
|
||||
import { useConfig, useMedia } from 'app/context';
|
||||
|
||||
const titleSize = { true: ['2xl', '2xl', '5xl', '5xl'], false: '2xl' };
|
||||
const titleMargin = { true: 2, false: 0 };
|
||||
const textAlignment = { false: ['right', 'center'], true: ['left', 'center'] };
|
||||
const logoName = { light: 'dark', dark: 'light' };
|
||||
const justifyMap = {
|
||||
true: ['flex-end', 'center', 'center', 'center'],
|
||||
false: ['flex-start', 'center', 'center', 'center'],
|
||||
};
|
||||
|
||||
const logoFallback = {
|
||||
light: 'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-dark.svg',
|
||||
dark: 'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-light.svg',
|
||||
};
|
||||
|
||||
const TitleOnly = ({ text, showSubtitle }) => (
|
||||
<Heading as="h1" mb={titleMargin[showSubtitle]} fontSize={titleSize[showSubtitle]}>
|
||||
{text}
|
||||
</Heading>
|
||||
);
|
||||
|
||||
const SubtitleOnly = ({ text, mediaSize, ...props }) => (
|
||||
<Heading
|
||||
as="h3"
|
||||
fontSize={['md', 'md', 'xl', 'xl']}
|
||||
whiteSpace="break-spaces"
|
||||
textAlign={['left', 'left', 'center', 'center']}
|
||||
{...props}>
|
||||
{text}
|
||||
</Heading>
|
||||
);
|
||||
|
||||
const TextOnly = ({ text, mediaSize, showSubtitle, ...props }) => (
|
||||
<Stack spacing={2} maxW="100%" textAlign={textAlignment[showSubtitle]} {...props}>
|
||||
<TitleOnly text={text.title} showSubtitle={showSubtitle} />
|
||||
{showSubtitle && <SubtitleOnly text={text.subtitle} mediaSize={mediaSize} />}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const Logo = ({ text, logo }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const { width, dark_format, light_format } = logo;
|
||||
const logoExt = { light: dark_format, dark: light_format };
|
||||
return (
|
||||
<Image
|
||||
css={{
|
||||
userDrag: 'none',
|
||||
userSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
WebkitUserDrag: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
}}
|
||||
alt={text.title}
|
||||
width={width ?? 'auto'}
|
||||
fallbackSrc={logoFallback[colorMode]}
|
||||
src={`/images/${logoName[colorMode]}${logoExt[colorMode]}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const LogoSubtitle = ({ text, logo, mediaSize }) => (
|
||||
<>
|
||||
<Logo text={text} logo={logo} mediaSize={mediaSize} />
|
||||
<SubtitleOnly mt={6} text={text.subtitle} />
|
||||
</>
|
||||
);
|
||||
|
||||
const All = ({ text, logo, mediaSize, showSubtitle }) => (
|
||||
<>
|
||||
<Logo text={text} logo={logo} />
|
||||
<TextOnly mediaSize={mediaSize} showSubtitle={showSubtitle} mt={2} text={text} />
|
||||
</>
|
||||
);
|
||||
|
||||
const modeMap = {
|
||||
text_only: TextOnly,
|
||||
logo_only: Logo,
|
||||
logo_subtitle: LogoSubtitle,
|
||||
all: All,
|
||||
};
|
||||
|
||||
export const Title = forwardRef(({ onClick, isSubmitting, ...props }, ref) => {
|
||||
const { web } = useConfig();
|
||||
const { mediaSize } = useMedia();
|
||||
const titleMode = web.text.title_mode;
|
||||
const MatchedMode = modeMap[titleMode];
|
||||
return (
|
||||
<Button
|
||||
px={0}
|
||||
w="100%"
|
||||
ref={ref}
|
||||
variant="link"
|
||||
flexWrap="wrap"
|
||||
flexDir="column"
|
||||
onClick={onClick}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
justifyContent={justifyMap[isSubmitting]}
|
||||
alignItems={['flex-start', 'flex-start', 'center']}
|
||||
{...props}>
|
||||
<MatchedMode
|
||||
mediaSize={mediaSize}
|
||||
showSubtitle={!isSubmitting}
|
||||
text={web.text}
|
||||
logo={web.logo}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
6
hyperglass/ui/components/Util/If.tsx
Normal file
6
hyperglass/ui/components/Util/If.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import type { TIf } from './types';
|
||||
|
||||
export const If = (props: TIf) => {
|
||||
const { c, render, children, ...rest } = props;
|
||||
return c ? (render ? render(rest) : children) : null;
|
||||
};
|
||||
8
hyperglass/ui/components/Util/animated.ts
Normal file
8
hyperglass/ui/components/Util/animated.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { chakra } from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const AnimatedDiv = motion.custom(chakra.div);
|
||||
export const AnimatedForm = motion.custom(chakra.form);
|
||||
export const AnimatedH1 = motion.custom(chakra.h1);
|
||||
export const AnimatedH3 = motion.custom(chakra.h3);
|
||||
export const AnimatedButton = motion.custom(chakra.button);
|
||||
2
hyperglass/ui/components/Util/index.ts
Normal file
2
hyperglass/ui/components/Util/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './animated';
|
||||
export * from './if';
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue