forked from mirrors/thatmattlove-hyperglass
Implement UI configuration response model
This commit is contained in:
parent
0e6c5e02ad
commit
76bf5eb380
65 changed files with 422 additions and 478 deletions
|
|
@ -141,7 +141,7 @@ set_log_level(logger=log, debug=user_config.get("debug", True))
|
||||||
|
|
||||||
# Map imported user configuration to expected schema.
|
# Map imported user configuration to expected schema.
|
||||||
log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config)
|
log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config)
|
||||||
params: Params = validate_config(config=user_config, importer=Params)
|
params = validate_config(config=user_config, importer=Params)
|
||||||
|
|
||||||
# Re-evaluate debug state after config is validated
|
# Re-evaluate debug state after config is validated
|
||||||
log_level = current_log_level(log)
|
log_level = current_log_level(log)
|
||||||
|
|
|
||||||
|
|
@ -3,38 +3,20 @@
|
||||||
Some validations need to occur across multiple config files.
|
Some validations need to occur across multiple config files.
|
||||||
"""
|
"""
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Dict, List, Union, Callable
|
from typing import Any, Dict, List, Union, TypeVar
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.models import HyperglassModel
|
from hyperglass.exceptions.private import ConfigInvalid
|
||||||
from hyperglass.constants import TRANSPORT_REST, SUPPORTED_STRUCTURED_OUTPUT
|
|
||||||
from hyperglass.models.commands import Commands
|
Importer = TypeVar("Importer")
|
||||||
from hyperglass.exceptions.private import ConfigError, ConfigInvalid
|
|
||||||
|
|
||||||
|
|
||||||
def validate_nos_commands(all_nos: List[str], commands: Commands) -> bool:
|
def validate_config(
|
||||||
"""Ensure defined devices have associated commands."""
|
config: Union[Dict[str, Any], List[Any]], importer: Importer
|
||||||
custom_commands = commands.dict().keys()
|
) -> Importer:
|
||||||
|
|
||||||
for nos in all_nos:
|
|
||||||
valid = False
|
|
||||||
if nos in (*SUPPORTED_STRUCTURED_OUTPUT, *TRANSPORT_REST, *custom_commands):
|
|
||||||
valid = True
|
|
||||||
|
|
||||||
if not valid:
|
|
||||||
raise ConfigError(
|
|
||||||
'"{nos}" is used on a device, '
|
|
||||||
+ 'but no command profile for "{nos}" is defined.',
|
|
||||||
nos=nos,
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config(config: Union[Dict, List], importer: Callable) -> HyperglassModel:
|
|
||||||
"""Validate a config dict against a model."""
|
"""Validate a config dict against a model."""
|
||||||
validated = None
|
validated = None
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
"""All Data Models used by hyperglass."""
|
"""All Data Models used by hyperglass."""
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .main import HyperglassModel, HyperglassModelExtra
|
from .main import HyperglassModel
|
||||||
|
|
||||||
|
__all__ = ("HyperglassModel",)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from .frr import FRRCommands
|
||||||
from .bird import BIRDCommands
|
from .bird import BIRDCommands
|
||||||
from .tnsr import TNSRCommands
|
from .tnsr import TNSRCommands
|
||||||
from .vyos import VyosCommands
|
from .vyos import VyosCommands
|
||||||
from ..main import HyperglassModelExtra
|
from ..main import HyperglassModel
|
||||||
from .common import CommandGroup
|
from .common import CommandGroup
|
||||||
from .huawei import HuaweiCommands
|
from .huawei import HuaweiCommands
|
||||||
from .juniper import JuniperCommands
|
from .juniper import JuniperCommands
|
||||||
|
|
@ -34,7 +34,7 @@ _NOS_MAP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Commands(HyperglassModelExtra):
|
class Commands(HyperglassModel, extra="allow", validate_all=False):
|
||||||
"""Base class for command definitions."""
|
"""Base class for command definitions."""
|
||||||
|
|
||||||
arista_eos: CommandGroup = AristaEOSCommands()
|
arista_eos: CommandGroup = AristaEOSCommands()
|
||||||
|
|
@ -69,8 +69,3 @@ class Commands(HyperglassModelExtra):
|
||||||
nos_cmds = nos_cmd_set(**cmds)
|
nos_cmds = nos_cmd_set(**cmds)
|
||||||
setattr(obj, nos, nos_cmds)
|
setattr(obj, nos, nos_cmds)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Override pydantic config."""
|
|
||||||
|
|
||||||
validate_all = False
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
from pydantic import StrictStr
|
from pydantic import StrictStr
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel, HyperglassModelExtra
|
from ..main import HyperglassModel
|
||||||
|
|
||||||
|
|
||||||
class CommandSet(HyperglassModel):
|
class CommandSet(HyperglassModel):
|
||||||
|
|
@ -17,7 +17,7 @@ class CommandSet(HyperglassModel):
|
||||||
traceroute: StrictStr
|
traceroute: StrictStr
|
||||||
|
|
||||||
|
|
||||||
class CommandGroup(HyperglassModelExtra):
|
class CommandGroup(HyperglassModel, extra="allow"):
|
||||||
"""Validation model for all commands."""
|
"""Validation model for all commands."""
|
||||||
|
|
||||||
ipv4_default: CommandSet
|
ipv4_default: CommandSet
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ from typing import Optional
|
||||||
from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator
|
from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModelExtra
|
from ..main import HyperglassModel
|
||||||
|
|
||||||
Methods = constr(regex=r"(password|unencrypted_key|encrypted_key)")
|
Methods = constr(regex=r"(password|unencrypted_key|encrypted_key)")
|
||||||
|
|
||||||
|
|
||||||
class Credential(HyperglassModelExtra):
|
class Credential(HyperglassModel, extra="allow"):
|
||||||
"""Model for per-credential config in devices.yaml."""
|
"""Model for per-credential config in devices.yaml."""
|
||||||
|
|
||||||
username: StrictStr
|
username: StrictStr
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ from hyperglass.models.commands.generic import Directive
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .ssl import Ssl
|
from .ssl import Ssl
|
||||||
from ..main import HyperglassModel, HyperglassModelExtra
|
from ..main import HyperglassModel
|
||||||
from .proxy import Proxy
|
from .proxy import Proxy
|
||||||
from .params import Params
|
from .params import Params
|
||||||
from ..fields import SupportedDriver
|
from ..fields import SupportedDriver
|
||||||
|
|
@ -34,7 +34,7 @@ from .network import Network
|
||||||
from .credential import Credential
|
from .credential import Credential
|
||||||
|
|
||||||
|
|
||||||
class Device(HyperglassModelExtra):
|
class Device(HyperglassModel, extra="allow"):
|
||||||
"""Validation model for per-router config in devices.yaml."""
|
"""Validation model for per-router config in devices.yaml."""
|
||||||
|
|
||||||
_id: StrictStr = PrivateAttr()
|
_id: StrictStr = PrivateAttr()
|
||||||
|
|
@ -222,7 +222,7 @@ class Device(HyperglassModelExtra):
|
||||||
return get_driver(values["nos"], value)
|
return get_driver(values["nos"], value)
|
||||||
|
|
||||||
|
|
||||||
class Devices(HyperglassModelExtra):
|
class Devices(HyperglassModel, extra="allow"):
|
||||||
"""Validation model for device configurations."""
|
"""Validation model for device configurations."""
|
||||||
|
|
||||||
_ids: List[StrictStr] = []
|
_ids: List[StrictStr] = []
|
||||||
|
|
@ -290,7 +290,7 @@ class Devices(HyperglassModelExtra):
|
||||||
"directives": [c.frontend(params) for c in device.commands],
|
"directives": [c.frontend(params) for c in device.commands],
|
||||||
}
|
}
|
||||||
for device in self.objects
|
for device in self.objects
|
||||||
if device.network.display_name in names
|
if device.network.display_name == name
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
for name in names
|
for name in names
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ from pydantic import (
|
||||||
from hyperglass.constants import __version__
|
from hyperglass.constants import __version__
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel, HyperglassModelExtra
|
from ..main import HyperglassModel
|
||||||
|
|
||||||
HttpAuthMode = constr(regex=r"(basic|api_key)")
|
HttpAuthMode = constr(regex=r"(basic|api_key)")
|
||||||
HttpProvider = constr(regex=r"(msteams|slack|generic)")
|
HttpProvider = constr(regex=r"(msteams|slack|generic)")
|
||||||
|
|
@ -55,7 +55,7 @@ class HttpAuth(HyperglassModel):
|
||||||
return (self.username, self.password.get_secret_value())
|
return (self.username, self.password.get_secret_value())
|
||||||
|
|
||||||
|
|
||||||
class Http(HyperglassModelExtra):
|
class Http(HyperglassModel, extra="allow"):
|
||||||
"""HTTP logging parameters."""
|
"""HTTP logging parameters."""
|
||||||
|
|
||||||
enable: StrictBool = True
|
enable: StrictBool = True
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ class Params(ParamsPublic, HyperglassModel):
|
||||||
def frontend(self) -> Dict[str, Any]:
|
def frontend(self) -> Dict[str, Any]:
|
||||||
"""Export UI-specific parameters."""
|
"""Export UI-specific parameters."""
|
||||||
|
|
||||||
return self.dict(
|
return self.export_dict(
|
||||||
include={
|
include={
|
||||||
"cache": {"show_text", "timeout"},
|
"cache": {"show_text", "timeout"},
|
||||||
"debug": ...,
|
"debug": ...,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ from pydantic import (
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel, HyperglassModelExtra
|
from ..main import HyperglassModel
|
||||||
|
|
||||||
ACLAction = constr(regex=r"permit|deny")
|
ACLAction = constr(regex=r"permit|deny")
|
||||||
AddressFamily = Union[Literal[4], Literal[6]]
|
AddressFamily = Union[Literal[4], Literal[6]]
|
||||||
|
|
@ -125,7 +125,7 @@ class AccessList6(HyperglassModel):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class InfoConfigParams(HyperglassModelExtra):
|
class InfoConfigParams(HyperglassModel, extra="allow"):
|
||||||
"""Validation model for per-help params."""
|
"""Validation model for per-help params."""
|
||||||
|
|
||||||
title: Optional[StrictStr]
|
title: Optional[StrictStr]
|
||||||
|
|
@ -197,7 +197,7 @@ class Info(HyperglassModel):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceVrf4(HyperglassModelExtra):
|
class DeviceVrf4(HyperglassModel, extra="allow"):
|
||||||
"""Validation model for IPv4 AFI definitions."""
|
"""Validation model for IPv4 AFI definitions."""
|
||||||
|
|
||||||
source_address: IPv4Address
|
source_address: IPv4Address
|
||||||
|
|
@ -205,7 +205,7 @@ class DeviceVrf4(HyperglassModelExtra):
|
||||||
force_cidr: StrictBool = True
|
force_cidr: StrictBool = True
|
||||||
|
|
||||||
|
|
||||||
class DeviceVrf6(HyperglassModelExtra):
|
class DeviceVrf6(HyperglassModel, extra="allow"):
|
||||||
"""Validation model for IPv6 AFI definitions."""
|
"""Validation model for IPv6 AFI definitions."""
|
||||||
|
|
||||||
source_address: IPv6Address
|
source_address: IPv6Address
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,55 @@
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import re
|
import re
|
||||||
from typing import Type, TypeVar
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import HttpUrl, BaseModel
|
from pydantic import HttpUrl, BaseModel, BaseConfig
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
|
from hyperglass.log import log
|
||||||
from hyperglass.util import snake_to_camel
|
from hyperglass.util import snake_to_camel
|
||||||
|
|
||||||
|
|
||||||
def clean_name(_name: str) -> str:
|
|
||||||
"""Remove unsupported characters from field names.
|
|
||||||
|
|
||||||
Converts any "desirable" seperators to underscore, then removes all
|
|
||||||
characters that are unsupported in Python class variable names.
|
|
||||||
Also removes leading numbers underscores.
|
|
||||||
"""
|
|
||||||
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", _name)
|
|
||||||
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
|
|
||||||
return _scrubbed.lower()
|
|
||||||
|
|
||||||
|
|
||||||
AsUIModel = TypeVar("AsUIModel", bound="BaseModel")
|
|
||||||
|
|
||||||
|
|
||||||
class HyperglassModel(BaseModel):
|
class HyperglassModel(BaseModel):
|
||||||
"""Base model for all hyperglass configuration models."""
|
"""Base model for all hyperglass configuration models."""
|
||||||
|
|
||||||
class Config:
|
class Config(BaseConfig):
|
||||||
"""Pydantic model configuration.
|
"""Pydantic model configuration."""
|
||||||
|
|
||||||
See https://pydantic-docs.helpmanual.io/usage/model_config
|
|
||||||
"""
|
|
||||||
|
|
||||||
validate_all = True
|
validate_all = True
|
||||||
extra = "forbid"
|
extra = "forbid"
|
||||||
validate_assignment = True
|
validate_assignment = True
|
||||||
alias_generator = clean_name
|
allow_population_by_field_name = True
|
||||||
json_encoders = {HttpUrl: lambda v: str(v)}
|
json_encoders = {HttpUrl: lambda v: str(v), Path: str}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def alias_generator(cls: "HyperglassModel", field: str) -> str:
|
||||||
|
"""Remove unsupported characters from field names.
|
||||||
|
|
||||||
|
Converts any "desirable" seperators to underscore, then removes all
|
||||||
|
characters that are unsupported in Python class variable names.
|
||||||
|
Also removes leading numbers underscores.
|
||||||
|
"""
|
||||||
|
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", field)
|
||||||
|
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
|
||||||
|
snake_field = _scrubbed.lower()
|
||||||
|
if snake_field != field:
|
||||||
|
log.debug(
|
||||||
|
"Model field '{}.{}' was converted from {} to {}",
|
||||||
|
cls.__module__,
|
||||||
|
snake_field,
|
||||||
|
repr(field),
|
||||||
|
repr(snake_field),
|
||||||
|
)
|
||||||
|
return snake_to_camel(snake_field)
|
||||||
|
|
||||||
def export_json(self, *args, **kwargs):
|
def export_json(self, *args, **kwargs):
|
||||||
"""Return instance as JSON."""
|
"""Return instance as JSON."""
|
||||||
|
|
||||||
export_kwargs = {"by_alias": True, "exclude_unset": False}
|
export_kwargs = {"by_alias": False, "exclude_unset": False}
|
||||||
|
|
||||||
for key in export_kwargs.keys():
|
for key in kwargs.keys():
|
||||||
export_kwargs.pop(key, None)
|
export_kwargs.pop(key, None)
|
||||||
|
|
||||||
return self.json(*args, **export_kwargs, **kwargs)
|
return self.json(*args, **export_kwargs, **kwargs)
|
||||||
|
|
@ -54,9 +58,9 @@ class HyperglassModel(BaseModel):
|
||||||
def export_dict(self, *args, **kwargs):
|
def export_dict(self, *args, **kwargs):
|
||||||
"""Return instance as dictionary."""
|
"""Return instance as dictionary."""
|
||||||
|
|
||||||
export_kwargs = {"by_alias": True, "exclude_unset": False}
|
export_kwargs = {"by_alias": False, "exclude_unset": False}
|
||||||
|
|
||||||
for key in export_kwargs.keys():
|
for key in kwargs.keys():
|
||||||
export_kwargs.pop(key, None)
|
export_kwargs.pop(key, None)
|
||||||
|
|
||||||
return self.dict(*args, **export_kwargs, **kwargs)
|
return self.dict(*args, **export_kwargs, **kwargs)
|
||||||
|
|
@ -71,34 +75,10 @@ class HyperglassModel(BaseModel):
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
export_kwargs = {
|
export_kwargs = {
|
||||||
"by_alias": kwargs.pop("by_alias", True),
|
"by_alias": kwargs.pop("by_alias", False),
|
||||||
"exclude_unset": kwargs.pop("by_alias", False),
|
"exclude_unset": kwargs.pop("exclude_unset", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
return yaml.safe_dump(
|
return yaml.safe_dump(
|
||||||
json.loads(self.export_json(**export_kwargs)), *args, **kwargs
|
json.loads(self.export_json(**export_kwargs)), *args, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HyperglassModelExtra(HyperglassModel):
|
|
||||||
"""Model for hyperglass configuration models with dynamic fields."""
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
extra = "allow"
|
|
||||||
|
|
||||||
|
|
||||||
class HyperglassUIModel(HyperglassModel):
|
|
||||||
"""Base class for UI configuration parameters."""
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
alias_generator = snake_to_camel
|
|
||||||
allow_population_by_field_name = True
|
|
||||||
|
|
||||||
|
|
||||||
def as_ui_model(name: str, model: Type[AsUIModel]) -> Type[AsUIModel]:
|
|
||||||
"""Override a model's configuration to confirm to a UI model."""
|
|
||||||
return type(name, (model, HyperglassUIModel), {})
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from typing import Any, Dict, List, Tuple, Union, Literal, Optional
|
||||||
from pydantic import StrictStr, StrictBool
|
from pydantic import StrictStr, StrictBool
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .main import HyperglassUIModel, as_ui_model
|
from .main import HyperglassModel
|
||||||
from .config.web import WebPublic
|
from .config.web import WebPublic
|
||||||
from .config.cache import CachePublic
|
from .config.cache import CachePublic
|
||||||
from .config.params import ParamsPublic
|
from .config.params import ParamsPublic
|
||||||
|
|
@ -16,12 +16,8 @@ from .config.messages import Messages
|
||||||
Alignment = Union[Literal["left"], Literal["center"], Literal["right"], None]
|
Alignment = Union[Literal["left"], Literal["center"], Literal["right"], None]
|
||||||
StructuredDataField = Tuple[str, str, Alignment]
|
StructuredDataField = Tuple[str, str, Alignment]
|
||||||
|
|
||||||
CacheUI = as_ui_model("CacheUI", CachePublic)
|
|
||||||
WebUI = as_ui_model("WebUI", WebPublic)
|
|
||||||
MessagesUI = as_ui_model("MessagesUI", Messages)
|
|
||||||
|
|
||||||
|
class UIDirectiveInfo(HyperglassModel):
|
||||||
class UIDirectiveInfo(HyperglassUIModel):
|
|
||||||
"""UI: Directive Info."""
|
"""UI: Directive Info."""
|
||||||
|
|
||||||
enable: StrictBool
|
enable: StrictBool
|
||||||
|
|
@ -29,7 +25,7 @@ class UIDirectiveInfo(HyperglassUIModel):
|
||||||
content: StrictStr
|
content: StrictStr
|
||||||
|
|
||||||
|
|
||||||
class UIDirective(HyperglassUIModel):
|
class UIDirective(HyperglassModel):
|
||||||
"""UI: Directive."""
|
"""UI: Directive."""
|
||||||
|
|
||||||
id: StrictStr
|
id: StrictStr
|
||||||
|
|
@ -41,7 +37,7 @@ class UIDirective(HyperglassUIModel):
|
||||||
options: Optional[List[Dict[str, Any]]]
|
options: Optional[List[Dict[str, Any]]]
|
||||||
|
|
||||||
|
|
||||||
class UILocation(HyperglassUIModel):
|
class UILocation(HyperglassModel):
|
||||||
"""UI: Location (Device)."""
|
"""UI: Location (Device)."""
|
||||||
|
|
||||||
id: StrictStr
|
id: StrictStr
|
||||||
|
|
@ -50,26 +46,26 @@ class UILocation(HyperglassUIModel):
|
||||||
directives: List[UIDirective] = []
|
directives: List[UIDirective] = []
|
||||||
|
|
||||||
|
|
||||||
class UINetwork(HyperglassUIModel):
|
class UINetwork(HyperglassModel):
|
||||||
"""UI: Network."""
|
"""UI: Network."""
|
||||||
|
|
||||||
display_name: StrictStr
|
display_name: StrictStr
|
||||||
locations: List[UILocation] = []
|
locations: List[UILocation] = []
|
||||||
|
|
||||||
|
|
||||||
class UIContent(HyperglassUIModel):
|
class UIContent(HyperglassModel):
|
||||||
"""UI: Content."""
|
"""UI: Content."""
|
||||||
|
|
||||||
credit: StrictStr
|
credit: StrictStr
|
||||||
greeting: StrictStr
|
greeting: StrictStr
|
||||||
|
|
||||||
|
|
||||||
class UIParameters(HyperglassUIModel, ParamsPublic):
|
class UIParameters(ParamsPublic, HyperglassModel):
|
||||||
"""UI Configuration Parameters."""
|
"""UI Configuration Parameters."""
|
||||||
|
|
||||||
cache: CacheUI
|
cache: CachePublic
|
||||||
web: WebUI
|
web: WebPublic
|
||||||
messages: MessagesUI
|
messages: Messages
|
||||||
version: StrictStr
|
version: StrictStr
|
||||||
networks: List[UINetwork] = []
|
networks: List[UINetwork] = []
|
||||||
parsed_data_fields: Tuple[StructuredDataField, ...]
|
parsed_data_fields: Tuple[StructuredDataField, ...]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from pydantic import StrictStr, root_validator
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .main import HyperglassModel, HyperglassModelExtra
|
from .main import HyperglassModel
|
||||||
|
|
||||||
_WEBHOOK_TITLE = "hyperglass received a valid query with the following data"
|
_WEBHOOK_TITLE = "hyperglass received a valid query with the following data"
|
||||||
_ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png"
|
_ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png"
|
||||||
|
|
@ -39,7 +39,7 @@ class WebhookHeaders(HyperglassModel):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class WebhookNetwork(HyperglassModelExtra):
|
class WebhookNetwork(HyperglassModel, extra="allow"):
|
||||||
"""Webhook data model."""
|
"""Webhook data model."""
|
||||||
|
|
||||||
prefix: StrictStr = "Unknown"
|
prefix: StrictStr = "Unknown"
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import { Markdown } from '~/components';
|
||||||
import { useColorValue, useBreakpointValue, useConfig } from '~/context';
|
import { useColorValue, useBreakpointValue, useConfig } from '~/context';
|
||||||
import { useOpposingColor, useStrf } from '~/hooks';
|
import { useOpposingColor, useStrf } from '~/hooks';
|
||||||
|
|
||||||
import type { IConfig } from '~/types';
|
import type { Config } from '~/types';
|
||||||
import type { TFooterButton } from './types';
|
import type { TFooterButton } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter the configuration object based on values that are strings for formatting.
|
* Filter the configuration object based on values that are strings for formatting.
|
||||||
*/
|
*/
|
||||||
function getConfigFmt(config: IConfig): Record<string, string> {
|
function getConfigFmt(config: Config): Record<string, string> {
|
||||||
const fmt = {} as Record<string, string>;
|
const fmt = {} as Record<string, string>;
|
||||||
for (const [k, v] of Object.entries(config)) {
|
for (const [k, v] of Object.entries(config)) {
|
||||||
if (typeof v === 'string') {
|
if (typeof v === 'string') {
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ import { FooterLink } from './link';
|
||||||
import { isLink, isMenu } from './types';
|
import { isLink, isMenu } from './types';
|
||||||
|
|
||||||
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
|
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
|
||||||
import type { TLink, TMenu } from '~/types';
|
import type { Link, Menu } from '~/types';
|
||||||
|
|
||||||
const CodeIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCode));
|
const CodeIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCode));
|
||||||
const ExtIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/go').then(i => i.GoLinkExternal));
|
const ExtIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/go').then(i => i.GoLinkExternal));
|
||||||
|
|
||||||
function buildItems(links: TLink[], menus: TMenu[]): [(TLink | TMenu)[], (TLink | TMenu)[]] {
|
function buildItems(links: Link[], menus: Menu[]): [(Link | Menu)[], (Link | Menu)[]] {
|
||||||
const leftLinks = links.filter(link => link.side === 'left');
|
const leftLinks = links.filter(link => link.side === 'left');
|
||||||
const leftMenus = menus.filter(menu => menu.side === 'left');
|
const leftMenus = menus.filter(menu => menu.side === 'left');
|
||||||
const rightLinks = links.filter(link => link.side === 'right');
|
const rightLinks = links.filter(link => link.side === 'right');
|
||||||
|
|
@ -27,7 +27,7 @@ function buildItems(links: TLink[], menus: TMenu[]): [(TLink | TMenu)[], (TLink
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Footer: React.FC = () => {
|
export const Footer: React.FC = () => {
|
||||||
const { web, content, primary_asn } = useConfig();
|
const { web, content, primaryAsn } = useConfig();
|
||||||
|
|
||||||
const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
|
const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
|
||||||
const footerColor = useColorValue('black', 'white');
|
const footerColor = useColorValue('black', 'white');
|
||||||
|
|
@ -57,10 +57,10 @@ export const Footer: React.FC = () => {
|
||||||
>
|
>
|
||||||
{left.map(item => {
|
{left.map(item => {
|
||||||
if (isLink(item)) {
|
if (isLink(item)) {
|
||||||
const url = strF(item.url, { primary_asn }, '/');
|
const url = strF(item.url, { primaryAsn }, '/');
|
||||||
const icon: Partial<ButtonProps & LinkProps> = {};
|
const icon: Partial<ButtonProps & LinkProps> = {};
|
||||||
|
|
||||||
if (item.show_icon) {
|
if (item.showIcon) {
|
||||||
icon.rightIcon = <ExtIcon />;
|
icon.rightIcon = <ExtIcon />;
|
||||||
}
|
}
|
||||||
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
|
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
|
||||||
|
|
@ -73,10 +73,10 @@ export const Footer: React.FC = () => {
|
||||||
{!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />}
|
{!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />}
|
||||||
{right.map(item => {
|
{right.map(item => {
|
||||||
if (isLink(item)) {
|
if (isLink(item)) {
|
||||||
const url = strF(item.url, { primary_asn }, '/');
|
const url = strF(item.url, { primaryAsn }, '/');
|
||||||
const icon: Partial<ButtonProps & LinkProps> = {};
|
const icon: Partial<ButtonProps & LinkProps> = {};
|
||||||
|
|
||||||
if (item.show_icon) {
|
if (item.showIcon) {
|
||||||
icon.rightIcon = <ExtIcon />;
|
icon.rightIcon = <ExtIcon />;
|
||||||
}
|
}
|
||||||
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
|
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { ButtonProps, LinkProps, MenuListProps } from '@chakra-ui/react';
|
import type { ButtonProps, LinkProps, MenuListProps } from '@chakra-ui/react';
|
||||||
import type { TLink, TMenu } from '~/types';
|
import type { Link, Menu } from '~/types';
|
||||||
|
|
||||||
type TFooterSide = 'left' | 'right';
|
type TFooterSide = 'left' | 'right';
|
||||||
|
|
||||||
|
|
@ -17,10 +17,10 @@ export interface TColorModeToggle extends ButtonProps {
|
||||||
size?: string;
|
size?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLink(item: TLink | TMenu): item is TLink {
|
export function isLink(item: Link | Menu): item is Link {
|
||||||
return 'url' in item;
|
return 'url' in item;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMenu(item: TLink | TMenu): item is TMenu {
|
export function isMenu(item: Link | Menu): item is Menu {
|
||||||
return 'content' in item;
|
return 'content' in item;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,13 @@ export const QueryGroup: React.FC<TQueryGroup> = (props: TQueryGroup) => {
|
||||||
} else {
|
} else {
|
||||||
selections.queryGroup.set(null);
|
selections.queryGroup.set(null);
|
||||||
}
|
}
|
||||||
onChange({ field: 'query_group', value });
|
onChange({ field: 'queryGroup', value });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
size="lg"
|
size="lg"
|
||||||
name="query_group"
|
name="queryGroup"
|
||||||
options={options}
|
options={options}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,18 @@ import { Select } from '~/components';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { useLGState, useLGMethods } from '~/hooks';
|
import { useLGState, useLGMethods } from '~/hooks';
|
||||||
|
|
||||||
import type { TNetwork, TSelectOption } from '~/types';
|
import type { Network, TSelectOption } from '~/types';
|
||||||
import type { TQuerySelectField } from './types';
|
import type { TQuerySelectField } from './types';
|
||||||
|
|
||||||
function buildOptions(networks: TNetwork[]) {
|
function buildOptions(networks: Network[]) {
|
||||||
return networks
|
return networks
|
||||||
.map(net => {
|
.map(net => {
|
||||||
const label = net.display_name;
|
const label = net.displayName;
|
||||||
const options = net.locations
|
const options = net.locations
|
||||||
.map(loc => ({
|
.map(loc => ({
|
||||||
label: loc.name,
|
label: loc.name,
|
||||||
value: loc._id,
|
value: loc.id,
|
||||||
group: net.display_name,
|
group: net.displayName,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
|
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
|
||||||
return { label, options };
|
return { label, options };
|
||||||
|
|
@ -43,7 +43,7 @@ export const QueryLocation: React.FC<TQuerySelectField> = (props: TQuerySelectFi
|
||||||
}
|
}
|
||||||
if (Array.isArray(e)) {
|
if (Array.isArray(e)) {
|
||||||
const value = e.map(sel => sel!.value);
|
const value = e.map(sel => sel!.value);
|
||||||
onChange({ field: 'query_location', value });
|
onChange({ field: 'queryLocation', value });
|
||||||
selections.queryLocation.set(e);
|
selections.queryLocation.set(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -54,11 +54,11 @@ export const QueryLocation: React.FC<TQuerySelectField> = (props: TQuerySelectFi
|
||||||
size="lg"
|
size="lg"
|
||||||
options={options}
|
options={options}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
name="query_location"
|
name="queryLocation"
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={false}
|
||||||
value={exportState(selections.queryLocation.value)}
|
value={exportState(selections.queryLocation.value)}
|
||||||
isError={typeof errors.query_location !== 'undefined'}
|
isError={typeof errors.queryLocation !== 'undefined'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import { useLGState, useDirective } from '~/hooks';
|
||||||
import { isSelectDirective } from '~/types';
|
import { isSelectDirective } from '~/types';
|
||||||
|
|
||||||
import type { OptionProps } from 'react-select';
|
import type { OptionProps } from 'react-select';
|
||||||
import type { TSelectOption, TDirective } from '~/types';
|
import type { TSelectOption, Directive } from '~/types';
|
||||||
import type { TQueryTarget } from './types';
|
import type { TQueryTarget } from './types';
|
||||||
|
|
||||||
function buildOptions(directive: Nullable<TDirective>): TSelectOption[] {
|
function buildOptions(directive: Nullable<Directive>): TSelectOption[] {
|
||||||
if (directive !== null && isSelectDirective(directive)) {
|
if (directive !== null && isSelectDirective(directive)) {
|
||||||
return directive.options.map(o => ({
|
return directive.options.map(o => ({
|
||||||
value: o.value,
|
value: o.value,
|
||||||
|
|
@ -61,7 +61,7 @@ export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input {...register('query_target')} hidden readOnly value={queryTarget.value} />
|
<input {...register('queryTarget')} hidden readOnly value={queryTarget.value} />
|
||||||
<If c={directive !== null && isSelectDirective(directive)}>
|
<If c={directive !== null && isSelectDirective(directive)}>
|
||||||
<Select
|
<Select
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|
@ -82,7 +82,7 @@ export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => {
|
||||||
aria-label={placeholder}
|
aria-label={placeholder}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={displayTarget.value}
|
value={displayTarget.value}
|
||||||
name="query_target_display"
|
name="queryTargetDisplay"
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
_placeholder={{ color: placeholderColor }}
|
_placeholder={{ color: placeholderColor }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -28,18 +28,18 @@ export const QueryType: React.FC<TQuerySelectField> = (props: TQuerySelectField)
|
||||||
selections.queryType.set(null);
|
selections.queryType.set(null);
|
||||||
queryType.set('');
|
queryType.set('');
|
||||||
}
|
}
|
||||||
onChange({ field: 'query_type', value });
|
onChange({ field: 'queryType', value });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
size="lg"
|
size="lg"
|
||||||
name="query_type"
|
name="queryType"
|
||||||
options={options}
|
options={options}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
value={exportState(selections.queryType.value)}
|
value={exportState(selections.queryType.value)}
|
||||||
isError={'query_type' in errors}
|
isError={'queryType' in errors}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,11 @@ export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget
|
||||||
const query4 = Array.from(families.value).includes(4);
|
const query4 = Array.from(families.value).includes(4);
|
||||||
const query6 = Array.from(families.value).includes(6);
|
const query6 = Array.from(families.value).includes(6);
|
||||||
|
|
||||||
const tooltip4 = strF(web.text.fqdn_tooltip, { protocol: 'IPv4' });
|
const tooltip4 = strF(web.text.fqdnTooltip, { protocol: 'IPv4' });
|
||||||
const tooltip6 = strF(web.text.fqdn_tooltip, { protocol: 'IPv6' });
|
const tooltip6 = strF(web.text.fqdnTooltip, { protocol: 'IPv6' });
|
||||||
|
|
||||||
const [messageStart, messageEnd] = web.text.fqdn_message.split('{fqdn}');
|
const [messageStart, messageEnd] = web.text.fqdnMessage.split('{fqdn}');
|
||||||
const [errorStart, errorEnd] = web.text.fqdn_error.split('{fqdn}');
|
const [errorStart, errorEnd] = web.text.fqdnError.split('{fqdn}');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: data4,
|
data: data4,
|
||||||
|
|
@ -63,7 +63,7 @@ export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget
|
||||||
const answer6 = useMemo(() => findAnswer(data6), [data6]);
|
const answer6 = useMemo(() => findAnswer(data6), [data6]);
|
||||||
|
|
||||||
const handleOverride = useCallback(
|
const handleOverride = useCallback(
|
||||||
(value: string): void => setTarget({ field: 'query_target', value }),
|
(value: string): void => setTarget({ field: 'queryTarget', value }),
|
||||||
[setTarget],
|
[setTarget],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -137,7 +137,7 @@ export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget
|
||||||
onClick={errorClose}
|
onClick={errorClose}
|
||||||
leftIcon={<LeftArrow />}
|
leftIcon={<LeftArrow />}
|
||||||
>
|
>
|
||||||
{web.text.fqdn_error_button}
|
{web.text.fqdnErrorButton}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { FormControlProps } from '@chakra-ui/react';
|
import type { FormControlProps } from '@chakra-ui/react';
|
||||||
import type { UseFormRegister } from 'react-hook-form';
|
import type { UseFormRegister } from 'react-hook-form';
|
||||||
import type { TBGPCommunity, OnChangeArgs, TFormData } from '~/types';
|
import type { OnChangeArgs, FormData } from '~/types';
|
||||||
|
|
||||||
export interface TField extends FormControlProps {
|
export interface TField extends FormControlProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -21,17 +21,10 @@ export interface TQueryGroup extends TQuerySelectField {
|
||||||
groups: string[];
|
groups: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TCommunitySelect {
|
|
||||||
name: string;
|
|
||||||
onChange: OnChange;
|
|
||||||
communities: TBGPCommunity[];
|
|
||||||
register: UseFormRegister<TFormData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TQueryTarget {
|
export interface TQueryTarget {
|
||||||
name: string;
|
name: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
register: UseFormRegister<TFormData>;
|
register: UseFormRegister<FormData>;
|
||||||
onChange(e: OnChangeArgs): void;
|
onChange(e: OnChangeArgs): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ import type { TLogo } from './types';
|
||||||
*/
|
*/
|
||||||
function useLogo(): [string, () => void] {
|
function useLogo(): [string, () => void] {
|
||||||
const { web } = useConfig();
|
const { web } = useConfig();
|
||||||
const { dark_format, light_format } = web.logo;
|
const { darkFormat, lightFormat } = web.logo;
|
||||||
|
|
||||||
const src = useColorValue(`/images/dark${dark_format}`, `/images/light${light_format}`);
|
const src = useColorValue(`/images/dark${darkFormat}`, `/images/light${lightFormat}`);
|
||||||
|
|
||||||
// Use the hyperglass logo if the user's logo can't be loaded for whatever reason.
|
// Use the hyperglass logo if the user's logo can't be loaded for whatever reason.
|
||||||
const fallbackSrc = useColorValue(
|
const fallbackSrc = useColorValue(
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ const All: React.FC = () => (
|
||||||
export const Title: React.FC<TTitle> = (props: TTitle) => {
|
export const Title: React.FC<TTitle> = (props: TTitle) => {
|
||||||
const { fontSize, ...rest } = props;
|
const { fontSize, ...rest } = props;
|
||||||
const { web } = useConfig();
|
const { web } = useConfig();
|
||||||
const titleMode = web.text.title_mode;
|
const { titleMode } = web.text;
|
||||||
|
|
||||||
const { isSubmitting } = useLGState();
|
const { isSubmitting } = useLGState();
|
||||||
const { resetForm } = useLGMethods();
|
const { resetForm } = useLGMethods();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { ModalContentProps } from '@chakra-ui/react';
|
import type { ModalContentProps } from '@chakra-ui/react';
|
||||||
import type { TQueryContent, TQueryFields } from '~/types';
|
import type { QueryContent } from '~/types';
|
||||||
|
|
||||||
export interface THelpModal extends ModalContentProps {
|
export interface THelpModal extends ModalContentProps {
|
||||||
item: TQueryContent | null;
|
item: QueryContent | null;
|
||||||
name: TQueryFields;
|
name: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import type { TFrame } from './types';
|
||||||
|
|
||||||
export const Frame = (props: TFrame): JSX.Element => {
|
export const Frame = (props: TFrame): JSX.Element => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { developer_mode, google_analytics } = useConfig();
|
const { developerMode, googleAnalytics } = useConfig();
|
||||||
const { isSubmitting } = useLGState();
|
const { isSubmitting } = useLGState();
|
||||||
const { resetForm } = useLGMethods();
|
const { resetForm } = useLGMethods();
|
||||||
const { initialize, trackPage } = useGoogleAnalytics();
|
const { initialize, trackPage } = useGoogleAnalytics();
|
||||||
|
|
@ -25,7 +25,7 @@ export const Frame = (props: TFrame): JSX.Element => {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initialize(google_analytics, developer_mode);
|
initialize(googleAnalytics, developerMode);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -62,10 +62,10 @@ export const Frame = (props: TFrame): JSX.Element => {
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Footer />
|
<Footer />
|
||||||
<If c={developer_mode}>
|
<If c={developerMode}>
|
||||||
<Debugger />
|
<Debugger />
|
||||||
</If>
|
</If>
|
||||||
<ResetButton developerMode={developer_mode} resetForm={handleReset} />
|
<ResetButton developerMode={developerMode} resetForm={handleReset} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Greeting />
|
<Greeting />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ import {
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { useStrf, useGreeting, useDevice, useLGState, useLGMethods } from '~/hooks';
|
import { useStrf, useGreeting, useDevice, useLGState, useLGMethods } from '~/hooks';
|
||||||
import { dedupObjectArray } from '~/util';
|
import { dedupObjectArray } from '~/util';
|
||||||
import { isString, isQueryField, TDirective } from '~/types';
|
import { isString, isQueryField, Directive } from '~/types';
|
||||||
|
|
||||||
import type { TFormData, OnChangeArgs } from '~/types';
|
import type { FormData, OnChangeArgs } from '~/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Don't set the global flag on this.
|
* Don't set the global flag on this.
|
||||||
|
|
@ -50,13 +50,12 @@ export const LookingGlass: React.FC = () => {
|
||||||
const getDevice = useDevice();
|
const getDevice = useDevice();
|
||||||
const strF = useStrf();
|
const strF = useStrf();
|
||||||
|
|
||||||
const noQueryType = strF(messages.no_input, { field: web.text.query_type });
|
const noQueryType = strF(messages.noInput, { field: web.text.queryType });
|
||||||
const noQueryLoc = strF(messages.no_input, { field: web.text.query_location });
|
const noQueryLoc = strF(messages.noInput, { field: web.text.queryLocation });
|
||||||
const noQueryTarget = strF(messages.no_input, { field: web.text.query_target });
|
const noQueryTarget = strF(messages.noInput, { field: web.text.queryTarget });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
availableGroups,
|
availableGroups,
|
||||||
queryVrf,
|
|
||||||
queryType,
|
queryType,
|
||||||
directive,
|
directive,
|
||||||
availableTypes,
|
availableTypes,
|
||||||
|
|
@ -71,29 +70,28 @@ export const LookingGlass: React.FC = () => {
|
||||||
|
|
||||||
const queryTypes = useMemo(() => availableTypes.map(t => t.id.value), [availableTypes]);
|
const queryTypes = useMemo(() => availableTypes.map(t => t.id.value), [availableTypes]);
|
||||||
|
|
||||||
const formSchema = vest.create((data: TFormData = {} as TFormData) => {
|
const formSchema = vest.create((data: FormData = {} as FormData) => {
|
||||||
test('query_location', noQueryLoc, () => {
|
test('queryLocation', noQueryLoc, () => {
|
||||||
enforce(data.query_location).isArrayOf(enforce.isString()).isNotEmpty();
|
enforce(data.queryLocation).isArrayOf(enforce.isString()).isNotEmpty();
|
||||||
});
|
});
|
||||||
test('query_target', noQueryTarget, () => {
|
test('queryTarget', noQueryTarget, () => {
|
||||||
enforce(data.query_target).longerThan(1);
|
enforce(data.queryTarget).longerThan(1);
|
||||||
});
|
});
|
||||||
test('query_type', noQueryType, () => {
|
test('queryType', noQueryType, () => {
|
||||||
enforce(data.query_type).inside(queryTypes);
|
enforce(data.queryType).inside(queryTypes);
|
||||||
});
|
});
|
||||||
test('query_group', 'Query Group is empty', () => {
|
test('queryGroup', 'Query Group is empty', () => {
|
||||||
enforce(data.query_group).isString();
|
enforce(data.queryGroup).isString();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const formInstance = useForm<TFormData>({
|
const formInstance = useForm<FormData>({
|
||||||
resolver: vestResolver(formSchema),
|
resolver: vestResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
// query_vrf: 'default',
|
queryTarget: '',
|
||||||
query_target: '',
|
queryLocation: [],
|
||||||
query_location: [],
|
queryType: '',
|
||||||
query_type: '',
|
queryGroup: '',
|
||||||
query_group: '',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -157,10 +155,10 @@ export const LookingGlass: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLocChange(locations: string[]): void {
|
function handleLocChange(locations: string[]): void {
|
||||||
clearErrors('query_location');
|
clearErrors('queryLocation');
|
||||||
const locationNames = [] as string[];
|
const locationNames = [] as string[];
|
||||||
const allGroups = [] as string[][];
|
const allGroups = [] as string[][];
|
||||||
const allTypes = [] as TDirective[][];
|
const allTypes = [] as Directive[][];
|
||||||
const allDevices = [];
|
const allDevices = [];
|
||||||
|
|
||||||
queryLocation.set(locations);
|
queryLocation.set(locations);
|
||||||
|
|
@ -207,18 +205,17 @@ export const LookingGlass: React.FC = () => {
|
||||||
|
|
||||||
// If there is more than one location selected, but there are no intersecting VRFs, show an error.
|
// If there is more than one location selected, but there are no intersecting VRFs, show an error.
|
||||||
if (locations.length > 1 && intersecting.length === 0) {
|
if (locations.length > 1 && intersecting.length === 0) {
|
||||||
setError('query_location', {
|
setError('queryLocation', {
|
||||||
// message: `${locationNames.join(', ')} have no VRFs in common.`,
|
// message: `${locationNames.join(', ')} have no VRFs in common.`,
|
||||||
message: `${locationNames.join(', ')} have no groups in common.`,
|
message: `${locationNames.join(', ')} have no groups in common.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// If there is only one intersecting VRF, set it as the form value so the user doesn't have to.
|
// If there is only one intersecting VRF, set it as the form value so the user doesn't have to.
|
||||||
else if (intersecting.length === 1) {
|
else if (intersecting.length === 1) {
|
||||||
// queryVrf.set(intersecting[0]._id);
|
|
||||||
queryGroup.set(intersecting[0]);
|
queryGroup.set(intersecting[0]);
|
||||||
}
|
}
|
||||||
if (availableGroups.length > 1 && intersectingTypes.length === 0) {
|
if (availableGroups.length > 1 && intersectingTypes.length === 0) {
|
||||||
setError('query_location', {
|
setError('queryLocation', {
|
||||||
message: `${locationNames.join(', ')} have no query types in common.`,
|
message: `${locationNames.join(', ')} have no query types in common.`,
|
||||||
});
|
});
|
||||||
} else if (intersectingTypes.length === 1) {
|
} else if (intersectingTypes.length === 1) {
|
||||||
|
|
@ -228,7 +225,7 @@ export const LookingGlass: React.FC = () => {
|
||||||
|
|
||||||
function handleGroupChange(group: string): void {
|
function handleGroupChange(group: string): void {
|
||||||
queryGroup.set(group);
|
queryGroup.set(group);
|
||||||
let availTypes = new Array<TDirective>();
|
let availTypes = new Array<Directive>();
|
||||||
for (const loc of queryLocation) {
|
for (const loc of queryLocation) {
|
||||||
const device = getDevice(loc.value);
|
const device = getDevice(loc.value);
|
||||||
for (const directive of device.directives) {
|
for (const directive of device.directives) {
|
||||||
|
|
@ -237,7 +234,7 @@ export const LookingGlass: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
availTypes = dedupObjectArray<TDirective>(availTypes, 'id');
|
availTypes = dedupObjectArray<Directive>(availTypes, 'id');
|
||||||
availableTypes.set(availTypes);
|
availableTypes.set(availTypes);
|
||||||
if (availableTypes.length === 1) {
|
if (availableTypes.length === 1) {
|
||||||
queryType.set(availableTypes[0].name.value);
|
queryType.set(availableTypes[0].name.value);
|
||||||
|
|
@ -252,9 +249,9 @@ export const LookingGlass: React.FC = () => {
|
||||||
throw new Error(`Field '${e.field}' is not a valid form field.`);
|
throw new Error(`Field '${e.field}' is not a valid form field.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.field === 'query_location' && Array.isArray(e.value)) {
|
if (e.field === 'queryLocation' && Array.isArray(e.value)) {
|
||||||
handleLocChange(e.value);
|
handleLocChange(e.value);
|
||||||
} else if (e.field === 'query_type' && isString(e.value)) {
|
} else if (e.field === 'queryType' && isString(e.value)) {
|
||||||
queryType.set(e.value);
|
queryType.set(e.value);
|
||||||
if (queryTarget.value !== '') {
|
if (queryTarget.value !== '') {
|
||||||
// Reset queryTarget as well, so that, for example, selecting BGP Community, and selecting
|
// Reset queryTarget as well, so that, for example, selecting BGP Community, and selecting
|
||||||
|
|
@ -263,21 +260,19 @@ export const LookingGlass: React.FC = () => {
|
||||||
queryTarget.set('');
|
queryTarget.set('');
|
||||||
displayTarget.set('');
|
displayTarget.set('');
|
||||||
}
|
}
|
||||||
} else if (e.field === 'query_vrf' && isString(e.value)) {
|
} else if (e.field === 'queryTarget' && isString(e.value)) {
|
||||||
queryVrf.set(e.value);
|
|
||||||
} else if (e.field === 'query_target' && isString(e.value)) {
|
|
||||||
queryTarget.set(e.value);
|
queryTarget.set(e.value);
|
||||||
} else if (e.field === 'query_group' && isString(e.value)) {
|
} else if (e.field === 'queryGroup' && isString(e.value)) {
|
||||||
// queryGroup.set(e.value);
|
// queryGroup.set(e.value);
|
||||||
handleGroupChange(e.value);
|
handleGroupChange(e.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
register('query_location', { required: true });
|
register('queryLocation', { required: true });
|
||||||
// register('query_target', { required: true });
|
// register('queryTarget', { required: true });
|
||||||
register('query_type', { required: true });
|
register('queryType', { required: true });
|
||||||
register('query_group');
|
register('queryGroup');
|
||||||
}, [register]);
|
}, [register]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -297,13 +292,13 @@ export const LookingGlass: React.FC = () => {
|
||||||
onSubmit={handleSubmit(submitHandler)}
|
onSubmit={handleSubmit(submitHandler)}
|
||||||
>
|
>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
<FormField name="query_location" label={web.text.query_location}>
|
<FormField name="queryLocation" label={web.text.queryLocation}>
|
||||||
<QueryLocation onChange={handleChange} label={web.text.query_location} />
|
<QueryLocation onChange={handleChange} label={web.text.queryLocation} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<If c={availableGroups.length > 1}>
|
<If c={availableGroups.length > 1}>
|
||||||
<FormField label={web.text.query_group} name="query_group">
|
<FormField label={web.text.queryGroup} name="queryGroup">
|
||||||
<QueryGroup
|
<QueryGroup
|
||||||
label={web.text.query_group}
|
label={web.text.queryGroup}
|
||||||
groups={availableGroups.value}
|
groups={availableGroups.value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
@ -313,24 +308,24 @@ export const LookingGlass: React.FC = () => {
|
||||||
<FormRow>
|
<FormRow>
|
||||||
<SlideFade offsetX={-100} in={availableTypes.length > 1} unmountOnExit>
|
<SlideFade offsetX={-100} in={availableTypes.length > 1} unmountOnExit>
|
||||||
<FormField
|
<FormField
|
||||||
name="query_type"
|
name="queryType"
|
||||||
label={web.text.query_type}
|
label={web.text.queryType}
|
||||||
labelAddOn={
|
labelAddOn={
|
||||||
<HelpModal
|
<HelpModal
|
||||||
visible={selectedDirective?.info.value !== null}
|
visible={selectedDirective?.info.value !== null}
|
||||||
item={selectedDirective?.info.value ?? null}
|
item={selectedDirective?.info.value ?? null}
|
||||||
name="query_type"
|
name="queryType"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<QueryType onChange={handleChange} label={web.text.query_type} />
|
<QueryType onChange={handleChange} label={web.text.queryType} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</SlideFade>
|
</SlideFade>
|
||||||
<SlideFade offsetX={100} in={selectedDirective !== null} unmountOnExit>
|
<SlideFade offsetX={100} in={selectedDirective !== null} unmountOnExit>
|
||||||
{selectedDirective !== null && (
|
{selectedDirective !== null && (
|
||||||
<FormField name="query_target" label={web.text.query_target}>
|
<FormField name="queryTarget" label={web.text.queryTarget}>
|
||||||
<QueryTarget
|
<QueryTarget
|
||||||
name="query_target"
|
name="queryTarget"
|
||||||
register={register}
|
register={register}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={selectedDirective.description.value}
|
placeholder={selectedDirective.description.value}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ export const Meta: React.FC = () => {
|
||||||
const [location, setLocation] = useState('/');
|
const [location, setLocation] = useState('/');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
site_title: title = 'hyperglass',
|
siteTitle: title = 'hyperglass',
|
||||||
site_description: description = 'Network Looking Glass',
|
siteDescription: description = 'Network Looking Glass',
|
||||||
site_keywords: keywords = [
|
siteKeywords: keywords = [
|
||||||
'hyperglass',
|
'hyperglass',
|
||||||
'looking glass',
|
'looking glass',
|
||||||
'lg',
|
'lg',
|
||||||
|
|
@ -53,7 +53,7 @@ export const Meta: React.FC = () => {
|
||||||
<meta property="og:image:alt" content={siteName} />
|
<meta property="og:image:alt" content={siteName} />
|
||||||
<meta name="og:description" content={description} />
|
<meta name="og:description" content={description} />
|
||||||
<meta name="keywords" content={keywords.join(', ')} />
|
<meta name="keywords" content={keywords.join(', ')} />
|
||||||
<meta name="hg-version" content={config.hyperglass_version} />
|
<meta name="hg-version" content={config.version} />
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type { TCell } from './types';
|
||||||
|
|
||||||
export const Cell: React.FC<TCell> = (props: TCell) => {
|
export const Cell: React.FC<TCell> = (props: TCell) => {
|
||||||
const { data, rawData } = props;
|
const { data, rawData } = props;
|
||||||
const cellId = data.column.id as keyof TRoute;
|
const cellId = data.column.id as keyof Route;
|
||||||
const component = {
|
const component = {
|
||||||
med: <MonoField v={data.value} />,
|
med: <MonoField v={data.value} />,
|
||||||
age: <Age inSeconds={data.value} />,
|
age: <Age inSeconds={data.value} />,
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ export const Communities: React.FC<TCommunities> = (props: TCommunities) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<If c={communities.length === 0}>
|
<If c={communities.length === 0}>
|
||||||
<Tooltip placement="right" hasArrow label={web.text.no_communities}>
|
<Tooltip placement="right" hasArrow label={web.text.noCommunities}>
|
||||||
<Link>
|
<Link>
|
||||||
<Icon as={Question} />
|
<Icon as={Question} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -166,10 +166,10 @@ const _RPKIState: React.ForwardRefRenderFunction<HTMLDivElement, TRPKIState> = (
|
||||||
const icon = [NotAllowed, Check, Warning, Question];
|
const icon = [NotAllowed, Check, Warning, Question];
|
||||||
|
|
||||||
const text = [
|
const text = [
|
||||||
web.text.rpki_invalid,
|
web.text.rpkiInvalid,
|
||||||
web.text.rpki_valid,
|
web.text.rpkiValid,
|
||||||
web.text.rpki_unknown,
|
web.text.rpkiUnknown,
|
||||||
web.text.rpki_unverified,
|
web.text.rpkiUnverified,
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import { useConfig } from '~/context';
|
||||||
import { Table } from '~/components';
|
import { Table } from '~/components';
|
||||||
import { Cell } from './cell';
|
import { Cell } from './cell';
|
||||||
|
|
||||||
import type { TColumn, TParsedDataField, TCellRender } from '~/types';
|
import type { TColumn, ParsedDataField, TCellRender } from '~/types';
|
||||||
import type { TBGPTable } from './types';
|
import type { TBGPTable } from './types';
|
||||||
|
|
||||||
function makeColumns(fields: TParsedDataField[]): TColumn[] {
|
function makeColumns(fields: ParsedDataField[]): TColumn[] {
|
||||||
return fields.map(pair => {
|
return fields.map(pair => {
|
||||||
const [header, accessor, align] = pair;
|
const [header, accessor, align] = pair;
|
||||||
|
|
||||||
|
|
@ -27,8 +27,8 @@ function makeColumns(fields: TParsedDataField[]): TColumn[] {
|
||||||
|
|
||||||
export const BGPTable: React.FC<TBGPTable> = (props: TBGPTable) => {
|
export const BGPTable: React.FC<TBGPTable> = (props: TBGPTable) => {
|
||||||
const { children: data, ...rest } = props;
|
const { children: data, ...rest } = props;
|
||||||
const { parsed_data_fields } = useConfig();
|
const { parsedDataFields } = useConfig();
|
||||||
const columns = makeColumns(parsed_data_fields);
|
const columns = makeColumns(parsedDataFields);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex my={8} justify="center" maxW="100%" w="100%" {...rest}>
|
<Flex my={8} justify="center" maxW="100%" w="100%" {...rest}>
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,9 @@ export interface TRPKIState {
|
||||||
|
|
||||||
export interface TCell {
|
export interface TCell {
|
||||||
data: TCellRender;
|
data: TCellRender;
|
||||||
rawData: TStructuredResponse;
|
rawData: StructuredResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TBGPTable extends Omit<FlexProps, 'children'> {
|
export interface TBGPTable extends Omit<FlexProps, 'children'> {
|
||||||
children: TStructuredResponse;
|
children: StructuredResponse;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ import type { TChart, TNode, TNodeData } from './types';
|
||||||
|
|
||||||
export const Chart: React.FC<TChart> = (props: TChart) => {
|
export const Chart: React.FC<TChart> = (props: TChart) => {
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
const { primary_asn, org_name } = useConfig();
|
const { primaryAsn, orgName } = useConfig();
|
||||||
|
|
||||||
const dots = useColorToken('colors', 'blackAlpha.500', 'whiteAlpha.400');
|
const dots = useColorToken('colors', 'blackAlpha.500', 'whiteAlpha.400');
|
||||||
|
|
||||||
const elements = useElements({ asn: primary_asn, name: org_name }, data);
|
const elements = useElements({ asn: primaryAsn, name: orgName }, data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export const Path: React.FC<TPath> = (props: TPath) => {
|
||||||
const { getResponse } = useLGMethods();
|
const { getResponse } = useLGMethods();
|
||||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
const response = getResponse(device);
|
const response = getResponse(device);
|
||||||
const output = response?.output as TStructuredResponse;
|
const output = response?.output as StructuredResponse;
|
||||||
const bg = useColorValue('light.50', 'dark.900');
|
const bg = useColorValue('light.50', 'dark.900');
|
||||||
const centered = useBreakpointValue({ base: false, lg: true }) ?? true;
|
const centered = useBreakpointValue({ base: false, lg: true }) ?? true;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { NodeProps } from 'react-flow-renderer';
|
import type { NodeProps } from 'react-flow-renderer';
|
||||||
|
|
||||||
export interface TChart {
|
export interface TChart {
|
||||||
data: TStructuredResponse;
|
data: StructuredResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TPath {
|
export interface TPath {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type { BasePath } from './types';
|
||||||
const NODE_WIDTH = 200;
|
const NODE_WIDTH = 200;
|
||||||
const NODE_HEIGHT = 48;
|
const NODE_HEIGHT = 48;
|
||||||
|
|
||||||
export function useElements(base: BasePath, data: TStructuredResponse): FlowElement[] {
|
export function useElements(base: BasePath, data: StructuredResponse): FlowElement[] {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return [...buildElements(base, data)];
|
return [...buildElements(base, data)];
|
||||||
}, [base, data]);
|
}, [base, data]);
|
||||||
|
|
@ -18,7 +18,7 @@ export function useElements(base: BasePath, data: TStructuredResponse): FlowElem
|
||||||
* Calculate the positions for each AS Path.
|
* Calculate the positions for each AS Path.
|
||||||
* @see https://github.com/MrBlenny/react-flow-chart/issues/61
|
* @see https://github.com/MrBlenny/react-flow-chart/issues/61
|
||||||
*/
|
*/
|
||||||
function* buildElements(base: BasePath, data: TStructuredResponse): Generator<FlowElement> {
|
function* buildElements(base: BasePath, data: StructuredResponse): Generator<FlowElement> {
|
||||||
const { routes } = data;
|
const { routes } = data;
|
||||||
// Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths.
|
// Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths.
|
||||||
const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]);
|
const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Result } from './individual';
|
||||||
import { Tags } from './tags';
|
import { Tags } from './tags';
|
||||||
|
|
||||||
export const Results: React.FC = () => {
|
export const Results: React.FC = () => {
|
||||||
const { queryLocation, queryTarget, queryType, queryVrf, queryGroup } = useLGState();
|
const { queryLocation, queryTarget, queryType, queryGroup } = useLGState();
|
||||||
|
|
||||||
const getDevice = useDevice();
|
const getDevice = useDevice();
|
||||||
|
|
||||||
|
|
@ -45,9 +45,8 @@ export const Results: React.FC = () => {
|
||||||
<Result
|
<Result
|
||||||
index={i}
|
index={i}
|
||||||
device={device}
|
device={device}
|
||||||
key={device._id}
|
key={device.id}
|
||||||
queryLocation={loc.value}
|
queryLocation={loc.value}
|
||||||
queryVrf={queryVrf.value}
|
|
||||||
queryType={queryType.value}
|
queryType={queryType.value}
|
||||||
queryGroup={queryGroup.value}
|
queryGroup={queryGroup.value}
|
||||||
queryTarget={queryTarget.value}
|
queryTarget={queryTarget.value}
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ export function isFetchError(error: any): error is Response {
|
||||||
return typeof error !== 'undefined' && error !== null && 'statusText' in error;
|
return typeof error !== 'undefined' && error !== null && 'statusText' in error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLGError(error: any): error is TQueryResponse {
|
export function isLGError(error: any): error is QueryResponse {
|
||||||
return typeof error !== 'undefined' && error !== null && 'output' in error;
|
return typeof error !== 'undefined' && error !== null && 'output' in error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the response is an LG error, false if not.
|
* Returns true if the response is an LG error, false if not.
|
||||||
*/
|
*/
|
||||||
export function isLGOutputOrError(data: any): data is TQueryResponse {
|
export function isLGOutputOrError(data: any): data is QueryResponse {
|
||||||
return typeof data !== 'undefined' && data !== null && data?.level !== 'success';
|
return typeof data !== 'undefined' && data !== null && data?.level !== 'success';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const ResultHeader: React.FC<TResultHeader> = (props: TResultHeader) => {
|
||||||
|
|
||||||
const { web } = useConfig();
|
const { web } = useConfig();
|
||||||
const strF = useStrf();
|
const strF = useStrf();
|
||||||
const text = strF(web.text.complete_time, { seconds: runtime });
|
const text = strF(web.text.completeTime, { seconds: runtime });
|
||||||
const label = useMemo(() => runtimeText(runtime, text), [runtime, text]);
|
const label = useMemo(() => runtimeText(runtime, text), [runtime, text]);
|
||||||
|
|
||||||
const color = useOpposingColor(isError ? warning : defaultStatus);
|
const color = useOpposingColor(isError ? warning : defaultStatus);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const AccordionHeaderWrapper = chakra('div', {
|
||||||
});
|
});
|
||||||
|
|
||||||
const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props: TResult, ref) => {
|
const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props: TResult, ref) => {
|
||||||
const { index, device, queryVrf, queryType, queryTarget, queryLocation, queryGroup } = props;
|
const { index, device, queryType, queryTarget, queryLocation, queryGroup } = props;
|
||||||
|
|
||||||
const { web, cache, messages } = useConfig();
|
const { web, cache, messages } = useConfig();
|
||||||
const { index: indices, setIndex } = useAccordionContext();
|
const { index: indices, setIndex } = useAccordionContext();
|
||||||
|
|
@ -56,17 +56,16 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||||
queryLocation,
|
queryLocation,
|
||||||
queryTarget,
|
queryTarget,
|
||||||
queryType,
|
queryType,
|
||||||
queryVrf,
|
|
||||||
queryGroup,
|
queryGroup,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [data, isFetchedAfterMount]);
|
const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [data, isFetchedAfterMount]);
|
||||||
|
|
||||||
if (typeof data !== 'undefined') {
|
if (typeof data !== 'undefined') {
|
||||||
responses.merge({ [device._id]: data });
|
responses.merge({ [device.id]: data });
|
||||||
}
|
}
|
||||||
const strF = useStrf();
|
const strF = useStrf();
|
||||||
const cacheLabel = strF(web.text.cache_icon, { time: data?.timestamp });
|
const cacheLabel = strF(web.text.cacheIcon, { time: data?.timestamp });
|
||||||
|
|
||||||
const errorKeywords = useMemo(() => {
|
const errorKeywords = useMemo(() => {
|
||||||
let kw = [] as string[];
|
let kw = [] as string[];
|
||||||
|
|
@ -85,13 +84,13 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||||
} else if (isFetchError(error)) {
|
} else if (isFetchError(error)) {
|
||||||
return startCase(error.statusText);
|
return startCase(error.statusText);
|
||||||
} else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
|
} else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
|
||||||
return messages.request_timeout;
|
return messages.requestTimeout;
|
||||||
} else if (isStackError(error)) {
|
} else if (isStackError(error)) {
|
||||||
return startCase(error.message);
|
return startCase(error.message);
|
||||||
} else {
|
} else {
|
||||||
return messages.general;
|
return messages.general;
|
||||||
}
|
}
|
||||||
}, [error, data, messages.general, messages.request_timeout]);
|
}, [error, data, messages.general, messages.requestTimeout]);
|
||||||
|
|
||||||
isError && console.error(error);
|
isError && console.error(error);
|
||||||
|
|
||||||
|
|
@ -101,12 +100,12 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||||
warning: 'warning',
|
warning: 'warning',
|
||||||
error: 'warning',
|
error: 'warning',
|
||||||
danger: 'error',
|
danger: 'error',
|
||||||
} as { [k in TResponseLevel]: 'success' | 'warning' | 'error' };
|
} as { [K in ResponseLevel]: 'success' | 'warning' | 'error' };
|
||||||
|
|
||||||
let e: TErrorLevels = 'error';
|
let e: TErrorLevels = 'error';
|
||||||
|
|
||||||
if (isLGError(error)) {
|
if (isLGError(error)) {
|
||||||
const idx = error.level as TResponseLevel;
|
const idx = error.level as ResponseLevel;
|
||||||
e = statusMap[idx];
|
e = statusMap[idx];
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
|
|
@ -146,7 +145,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||||
return (
|
return (
|
||||||
<AnimatedAccordionItem
|
<AnimatedAccordionItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={device._id}
|
id={device.id}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
exit={{ opacity: 0, y: 300 }}
|
exit={{ opacity: 0, y: 300 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
|
@ -171,7 +170,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||||
</AccordionButton>
|
</AccordionButton>
|
||||||
<HStack py={2} spacing={1}>
|
<HStack py={2} spacing={1}>
|
||||||
{isStructuredOutput(data) && data.level === 'success' && tableComponent && (
|
{isStructuredOutput(data) && data.level === 'success' && tableComponent && (
|
||||||
<Path device={device._id} />
|
<Path device={device.id} />
|
||||||
)}
|
)}
|
||||||
<CopyButton copyValue={copyValue} isDisabled={isLoading} />
|
<CopyButton copyValue={copyValue} isDisabled={isLoading} />
|
||||||
<RequeryButton requery={refetch} isDisabled={isLoading} />
|
<RequeryButton requery={refetch} isDisabled={isLoading} />
|
||||||
|
|
@ -230,9 +229,9 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||||
flex="1 0 auto"
|
flex="1 0 auto"
|
||||||
justifyContent={{ base: 'flex-start', lg: 'flex-end' }}
|
justifyContent={{ base: 'flex-start', lg: 'flex-end' }}
|
||||||
>
|
>
|
||||||
<If c={cache.show_text && !isError && isCached}>
|
<If c={cache.showText && !isError && isCached}>
|
||||||
<If c={!isMobile}>
|
<If c={!isMobile}>
|
||||||
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} />
|
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
|
||||||
</If>
|
</If>
|
||||||
<Tooltip hasArrow label={cacheLabel} placement="top">
|
<Tooltip hasArrow label={cacheLabel} placement="top">
|
||||||
<Box>
|
<Box>
|
||||||
|
|
@ -240,7 +239,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<If c={isMobile}>
|
<If c={isMobile}>
|
||||||
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} />
|
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
|
||||||
</If>
|
</If>
|
||||||
</If>
|
</If>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export const Tags: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Label
|
<Label
|
||||||
bg={queryBg}
|
bg={queryBg}
|
||||||
label={web.text.query_type}
|
label={web.text.queryType}
|
||||||
fontSize={{ base: 'xs', md: 'sm' }}
|
fontSize={{ base: 'xs', md: 'sm' }}
|
||||||
value={selectedDirective?.value.name ?? 'None'}
|
value={selectedDirective?.value.name ?? 'None'}
|
||||||
/>
|
/>
|
||||||
|
|
@ -107,7 +107,7 @@ export const Tags: React.FC = () => {
|
||||||
<Label
|
<Label
|
||||||
bg={targetBg}
|
bg={targetBg}
|
||||||
value={queryTarget.value}
|
value={queryTarget.value}
|
||||||
label={web.text.query_target}
|
label={web.text.queryTarget}
|
||||||
fontSize={{ base: 'xs', md: 'sm' }}
|
fontSize={{ base: 'xs', md: 'sm' }}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -119,7 +119,7 @@ export const Tags: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Label
|
<Label
|
||||||
bg={vrfBg}
|
bg={vrfBg}
|
||||||
label={web.text.query_group}
|
label={web.text.queryGroup}
|
||||||
value={queryGroup.value}
|
value={queryGroup.value}
|
||||||
fontSize={{ base: 'xs', md: 'sm' }}
|
fontSize={{ base: 'xs', md: 'sm' }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { State } from '@hookstate/core';
|
import type { State } from '@hookstate/core';
|
||||||
import type { ButtonProps } from '@chakra-ui/react';
|
import type { ButtonProps } from '@chakra-ui/react';
|
||||||
import type { UseQueryResult } from 'react-query';
|
import type { UseQueryResult } from 'react-query';
|
||||||
import type { TDevice } from '~/types';
|
import type { Device } from '~/types';
|
||||||
|
|
||||||
export interface TResultHeader {
|
export interface TResultHeader {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -19,8 +19,7 @@ export interface TFormattedError {
|
||||||
|
|
||||||
export interface TResult {
|
export interface TResult {
|
||||||
index: number;
|
index: number;
|
||||||
device: TDevice;
|
device: Device;
|
||||||
queryVrf: string;
|
|
||||||
queryGroup: string;
|
queryGroup: string;
|
||||||
queryTarget: string;
|
queryTarget: string;
|
||||||
queryLocation: string;
|
queryLocation: string;
|
||||||
|
|
@ -34,7 +33,7 @@ export interface TCopyButton extends ButtonProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TRequeryButton extends ButtonProps {
|
export interface TRequeryButton extends ButtonProps {
|
||||||
requery: UseQueryResult<TQueryResponse>['refetch'];
|
requery: UseQueryResult<QueryResponse>['refetch'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TUseResults = {
|
export type TUseResults = {
|
||||||
|
|
|
||||||
|
|
@ -71,11 +71,11 @@ export const Table: React.FC<TTable> = (props: TTable) => {
|
||||||
defaultColumn,
|
defaultColumn,
|
||||||
data,
|
data,
|
||||||
initialState: { hiddenColumns },
|
initialState: { hiddenColumns },
|
||||||
} as TableOptions<TRoute>;
|
} as TableOptions<Route>;
|
||||||
|
|
||||||
const plugins = [useSortBy, usePagination] as PluginHook<TRoute>[];
|
const plugins = [useSortBy, usePagination] as PluginHook<Route>[];
|
||||||
|
|
||||||
const instance = useTable<TRoute>(options, ...plugins);
|
const instance = useTable<Route>(options, ...plugins);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
page,
|
page,
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ import type { BoxProps, IconButtonProps } from '@chakra-ui/react';
|
||||||
import type { Theme, TColumn, TCellRender } from '~/types';
|
import type { Theme, TColumn, TCellRender } from '~/types';
|
||||||
|
|
||||||
export interface TTable {
|
export interface TTable {
|
||||||
data: TRoute[];
|
data: Route[];
|
||||||
striped?: boolean;
|
striped?: boolean;
|
||||||
columns: TColumn[];
|
columns: TColumn[];
|
||||||
heading?: React.ReactNode;
|
heading?: React.ReactNode;
|
||||||
bordersVertical?: boolean;
|
bordersVertical?: boolean;
|
||||||
bordersHorizontal?: boolean;
|
bordersHorizontal?: boolean;
|
||||||
Cell?: React.FC<TCellRender>;
|
Cell?: React.FC<TCellRender>;
|
||||||
rowHighlightProp?: keyof IRoute;
|
rowHighlightProp?: keyof Route;
|
||||||
rowHighlightBg?: Theme.ColorNames;
|
rowHighlightBg?: Theme.ColorNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,17 @@ import {
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
import { makeTheme, defaultTheme } from '~/util';
|
import { makeTheme, defaultTheme } from '~/util';
|
||||||
|
|
||||||
import type { IConfig, Theme } from '~/types';
|
import type { Config, Theme } from '~/types';
|
||||||
import type { THyperglassProvider } from './types';
|
import type { THyperglassProvider } from './types';
|
||||||
|
|
||||||
const HyperglassContext = createContext<IConfig>(Object());
|
const HyperglassContext = createContext<Config>(Object());
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export const HyperglassProvider: React.FC<THyperglassProvider> = (props: THyperglassProvider) => {
|
export const HyperglassProvider: React.FC<THyperglassProvider> = (props: THyperglassProvider) => {
|
||||||
const { config, children } = props;
|
const { config, children } = props;
|
||||||
const value = useMemo(() => config, [config]);
|
const value = useMemo(() => config, [config]);
|
||||||
const userTheme = value && makeTheme(value.web.theme, value.web.theme.default_color_mode);
|
const userTheme = value && makeTheme(value.web.theme, value.web.theme.defaultColorMode);
|
||||||
const theme = value ? userTheme : defaultTheme;
|
const theme = value ? userTheme : defaultTheme;
|
||||||
return (
|
return (
|
||||||
<ChakraProvider theme={theme}>
|
<ChakraProvider theme={theme}>
|
||||||
|
|
@ -33,7 +33,7 @@ export const HyperglassProvider: React.FC<THyperglassProvider> = (props: THyperg
|
||||||
/**
|
/**
|
||||||
* Get the current configuration.
|
* Get the current configuration.
|
||||||
*/
|
*/
|
||||||
export const useConfig = (): IConfig => useContext(HyperglassContext);
|
export const useConfig = (): Config => useContext(HyperglassContext);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current theme object.
|
* Get the current theme object.
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import type { State } from '@hookstate/core';
|
import type { State } from '@hookstate/core';
|
||||||
import type { IConfig, TFormData } from '~/types';
|
import type { Config, FormData } from '~/types';
|
||||||
|
|
||||||
export interface THyperglassProvider {
|
export interface THyperglassProvider {
|
||||||
config: IConfig;
|
config: Config;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TGlobalState {
|
export interface TGlobalState {
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
formData: TFormData;
|
formData: FormData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TUseGlobalState {
|
export interface TUseGlobalState {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { State } from '@hookstate/core';
|
import type { State } from '@hookstate/core';
|
||||||
import type * as ReactGA from 'react-ga';
|
import type * as ReactGA from 'react-ga';
|
||||||
import type { TDevice, Families, TFormQuery, TDeviceVrf, TSelectOption, TDirective } from '~/types';
|
import type { Device, Families, TFormQuery, TSelectOption, Directive } from '~/types';
|
||||||
|
|
||||||
export type LGQueryKey = [string, TFormQuery];
|
export type LGQueryKey = [string, TFormQuery];
|
||||||
export type DNSQueryKey = [string, { target: string | null; family: 4 | 6 }];
|
export type DNSQueryKey = [string, { target: string | null; family: 4 | 6 }];
|
||||||
|
|
@ -23,56 +23,50 @@ export type TUseDevice = (
|
||||||
* Device's ID, e.g. the device.name field.
|
* Device's ID, e.g. the device.name field.
|
||||||
*/
|
*/
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
) => TDevice;
|
) => Device;
|
||||||
|
|
||||||
export type TUseVrf = (vrfId: string) => TDeviceVrf;
|
|
||||||
|
|
||||||
export interface TSelections {
|
export interface TSelections {
|
||||||
queryLocation: TSelectOption[] | [];
|
queryLocation: TSelectOption[] | [];
|
||||||
queryType: TSelectOption | null;
|
queryType: TSelectOption | null;
|
||||||
queryVrf: TSelectOption | null;
|
|
||||||
queryGroup: TSelectOption | null;
|
queryGroup: TSelectOption | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TMethodsExtension {
|
export interface TMethodsExtension {
|
||||||
getResponse(d: string): TQueryResponse | null;
|
getResponse(d: string): QueryResponse | null;
|
||||||
resolvedClose(): void;
|
resolvedClose(): void;
|
||||||
resolvedOpen(): void;
|
resolvedOpen(): void;
|
||||||
formReady(): boolean;
|
formReady(): boolean;
|
||||||
resetForm(): void;
|
resetForm(): void;
|
||||||
stateExporter<O extends unknown>(o: O): O | null;
|
stateExporter<O extends unknown>(o: O): O | null;
|
||||||
getDirective(n: string): Nullable<State<TDirective>>;
|
getDirective(n: string): Nullable<State<Directive>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TLGState = {
|
export type TLGState = {
|
||||||
queryVrf: string;
|
|
||||||
queryGroup: string;
|
queryGroup: string;
|
||||||
families: Families;
|
families: Families;
|
||||||
queryTarget: string;
|
queryTarget: string;
|
||||||
btnLoading: boolean;
|
btnLoading: boolean;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
displayTarget: string;
|
displayTarget: string;
|
||||||
directive: TDirective | null;
|
directive: Directive | null;
|
||||||
// queryType: TQueryTypes;
|
|
||||||
queryType: string;
|
queryType: string;
|
||||||
queryLocation: string[];
|
queryLocation: string[];
|
||||||
availVrfs: TDeviceVrf[];
|
|
||||||
availableGroups: string[];
|
availableGroups: string[];
|
||||||
availableTypes: TDirective[];
|
availableTypes: Directive[];
|
||||||
resolvedIsOpen: boolean;
|
resolvedIsOpen: boolean;
|
||||||
selections: TSelections;
|
selections: TSelections;
|
||||||
responses: { [d: string]: TQueryResponse };
|
responses: { [d: string]: QueryResponse };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLGStateHandlers = {
|
export type TLGStateHandlers = {
|
||||||
exportState<S extends unknown | null>(s: S): S | null;
|
exportState<S extends unknown | null>(s: S): S | null;
|
||||||
getResponse(d: string): TQueryResponse | null;
|
getResponse(d: string): QueryResponse | null;
|
||||||
resolvedClose(): void;
|
resolvedClose(): void;
|
||||||
resolvedOpen(): void;
|
resolvedOpen(): void;
|
||||||
formReady(): boolean;
|
formReady(): boolean;
|
||||||
resetForm(): void;
|
resetForm(): void;
|
||||||
stateExporter<O extends unknown>(o: O): O | null;
|
stateExporter<O extends unknown>(o: O): O | null;
|
||||||
getDirective(n: string): Nullable<State<TDirective>>;
|
getDirective(n: string): Nullable<State<Directive>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UseStrfArgs = { [k: string]: unknown } | string;
|
export type UseStrfArgs = { [k: string]: unknown } | string;
|
||||||
|
|
@ -89,7 +83,7 @@ export type TTableToStringFormatted = {
|
||||||
active: (v: boolean) => string;
|
active: (v: boolean) => string;
|
||||||
as_path: (v: number[]) => string;
|
as_path: (v: number[]) => string;
|
||||||
communities: (v: string[]) => string;
|
communities: (v: string[]) => string;
|
||||||
rpki_state: (v: number, n: TRPKIStates) => string;
|
rpki_state: (v: number, n: RPKIState) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GAEffect = (ga: typeof ReactGA) => void;
|
export type GAEffect = (ga: typeof ReactGA) => void;
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export function useDNSQuery(
|
||||||
}
|
}
|
||||||
|
|
||||||
return useQuery<DnsOverHttps.Response, unknown, DnsOverHttps.Response, DNSQueryKey>({
|
return useQuery<DnsOverHttps.Response, unknown, DnsOverHttps.Response, DNSQueryKey>({
|
||||||
queryKey: [web.dns_provider.url, { target, family }],
|
queryKey: [web.dnsProvider.url, { target, family }],
|
||||||
queryFn: query,
|
queryFn: query,
|
||||||
cacheTime: cache.timeout * 1000,
|
cacheTime: cache.timeout * 1000,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
|
|
||||||
import type { TDevice } from '~/types';
|
import type { Device } from '~/types';
|
||||||
import type { TUseDevice } from './types';
|
import type { TUseDevice } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -12,8 +12,8 @@ export function useDevice(): TUseDevice {
|
||||||
|
|
||||||
const devices = useMemo(() => networks.map(n => n.locations).flat(), [networks]);
|
const devices = useMemo(() => networks.map(n => n.locations).flat(), [networks]);
|
||||||
|
|
||||||
function getDevice(id: string): TDevice {
|
function getDevice(id: string): Device {
|
||||||
return devices.filter(dev => dev._id === id)[0];
|
return devices.filter(dev => dev.id === id)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return useCallback(getDevice, [devices]);
|
return useCallback(getDevice, [devices]);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useLGMethods, useLGState } from './useLGState';
|
import { useLGMethods, useLGState } from './useLGState';
|
||||||
|
|
||||||
import type { TDirective } from '~/types';
|
import type { Directive } from '~/types';
|
||||||
|
|
||||||
export function useDirective(): Nullable<TDirective> {
|
export function useDirective(): Nullable<Directive> {
|
||||||
const { queryType, queryGroup } = useLGState();
|
const { queryType, queryGroup } = useLGState();
|
||||||
const { getDirective } = useLGMethods();
|
const { getDirective } = useLGMethods();
|
||||||
|
|
||||||
return useMemo((): Nullable<TDirective> => {
|
return useMemo((): Nullable<Directive> => {
|
||||||
if (queryType.value === '') {
|
if (queryType.value === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ import { getHyperglassConfig } from '~/util';
|
||||||
|
|
||||||
import type { UseQueryResult } from 'react-query';
|
import type { UseQueryResult } from 'react-query';
|
||||||
import type { ConfigLoadError } from '~/util';
|
import type { ConfigLoadError } from '~/util';
|
||||||
import type { IConfig } from '~/types';
|
import type { Config } from '~/types';
|
||||||
|
|
||||||
type UseHyperglassConfig = UseQueryResult<IConfig, ConfigLoadError> & {
|
type UseHyperglassConfig = UseQueryResult<Config, ConfigLoadError> & {
|
||||||
/**
|
/**
|
||||||
* Initial configuration load has failed.
|
* Initial configuration load has failed.
|
||||||
*/
|
*/
|
||||||
|
|
@ -37,7 +37,7 @@ export function useHyperglassConfig(): UseHyperglassConfig {
|
||||||
// will be displayed, which will also show the loading state.
|
// will be displayed, which will also show the loading state.
|
||||||
const [initFailed, setInitFailed] = useState<boolean>(false);
|
const [initFailed, setInitFailed] = useState<boolean>(false);
|
||||||
|
|
||||||
const query = useQuery<IConfig, ConfigLoadError>({
|
const query = useQuery<Config, ConfigLoadError>({
|
||||||
queryKey: 'hyperglass-ui-config',
|
queryKey: 'hyperglass-ui-config',
|
||||||
queryFn: getHyperglassConfig,
|
queryFn: getHyperglassConfig,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import type { LGQueryKey } from './types';
|
||||||
/**
|
/**
|
||||||
* Custom hook handle submission of a query to the hyperglass backend.
|
* Custom hook handle submission of a query to the hyperglass backend.
|
||||||
*/
|
*/
|
||||||
export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryResponse> {
|
export function useLGQuery(query: TFormQuery): QueryObserverResult<QueryResponse> {
|
||||||
const { request_timeout, cache } = useConfig();
|
const { requestTimeout, cache } = useConfig();
|
||||||
const controller = useMemo(() => new AbortController(), []);
|
const controller = useMemo(() => new AbortController(), []);
|
||||||
|
|
||||||
const { trackEvent } = useGoogleAnalytics();
|
const { trackEvent } = useGoogleAnalytics();
|
||||||
|
|
@ -26,9 +26,9 @@ export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryRespons
|
||||||
dimension4: query.queryGroup,
|
dimension4: query.queryGroup,
|
||||||
});
|
});
|
||||||
|
|
||||||
const runQuery: QueryFunction<TQueryResponse, LGQueryKey> = async (
|
const runQuery: QueryFunction<QueryResponse, LGQueryKey> = async (
|
||||||
ctx: QueryFunctionContext<LGQueryKey>,
|
ctx: QueryFunctionContext<LGQueryKey>,
|
||||||
): Promise<TQueryResponse> => {
|
): Promise<QueryResponse> => {
|
||||||
const [url, data] = ctx.queryKey;
|
const [url, data] = ctx.queryKey;
|
||||||
const { queryLocation, queryTarget, queryType, queryGroup } = data;
|
const { queryLocation, queryTarget, queryType, queryGroup } = data;
|
||||||
const res = await fetchWithTimeout(
|
const res = await fetchWithTimeout(
|
||||||
|
|
@ -44,7 +44,7 @@ export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryRespons
|
||||||
}),
|
}),
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
},
|
},
|
||||||
request_timeout * 1000,
|
requestTimeout * 1000,
|
||||||
controller,
|
controller,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
|
@ -62,7 +62,7 @@ export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryRespons
|
||||||
[controller],
|
[controller],
|
||||||
);
|
);
|
||||||
|
|
||||||
return useQuery<TQueryResponse, Response | TQueryResponse | Error, TQueryResponse, LGQueryKey>({
|
return useQuery<QueryResponse, Response | QueryResponse | Error, QueryResponse, LGQueryKey>({
|
||||||
queryKey: ['/api/query/', query],
|
queryKey: ['/api/query/', query],
|
||||||
queryFn: runQuery,
|
queryFn: runQuery,
|
||||||
// Invalidate react-query's cache just shy of the configured cache timeout.
|
// Invalidate react-query's cache just shy of the configured cache timeout.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { all } from '~/util';
|
||||||
|
|
||||||
import type { State, PluginStateControl, Plugin } from '@hookstate/core';
|
import type { State, PluginStateControl, Plugin } from '@hookstate/core';
|
||||||
import type { TLGState, TLGStateHandlers, TMethodsExtension } from './types';
|
import type { TLGState, TLGStateHandlers, TMethodsExtension } from './types';
|
||||||
import { TDirective } from '~/types';
|
import type { Directive } from '~/types';
|
||||||
|
|
||||||
const MethodsId = Symbol('Methods');
|
const MethodsId = Symbol('Methods');
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ class MethodsInstance {
|
||||||
/**
|
/**
|
||||||
* Find a response based on the device ID.
|
* Find a response based on the device ID.
|
||||||
*/
|
*/
|
||||||
public getResponse(state: State<TLGState>, device: string): TQueryResponse | null {
|
public getResponse(state: State<TLGState>, device: string): QueryResponse | null {
|
||||||
if (device in state.responses) {
|
if (device in state.responses) {
|
||||||
return state.responses[device].value;
|
return state.responses[device].value;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -38,7 +38,7 @@ class MethodsInstance {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDirective(state: State<TLGState>, name: string): Nullable<State<TDirective>> {
|
public getDirective(state: State<TLGState>, name: string): Nullable<State<Directive>> {
|
||||||
const [directive] = state.availableTypes.filter(t => t.id.value === name);
|
const [directive] = state.availableTypes.filter(t => t.id.value === name);
|
||||||
if (typeof directive !== 'undefined') {
|
if (typeof directive !== 'undefined') {
|
||||||
return directive;
|
return directive;
|
||||||
|
|
@ -55,7 +55,6 @@ class MethodsInstance {
|
||||||
state.isSubmitting.value &&
|
state.isSubmitting.value &&
|
||||||
all(
|
all(
|
||||||
...[
|
...[
|
||||||
// state.queryVrf.value !== '',
|
|
||||||
state.queryType.value !== '',
|
state.queryType.value !== '',
|
||||||
state.queryGroup.value !== '',
|
state.queryGroup.value !== '',
|
||||||
state.queryTarget.value !== '',
|
state.queryTarget.value !== '',
|
||||||
|
|
@ -70,7 +69,6 @@ class MethodsInstance {
|
||||||
*/
|
*/
|
||||||
public resetForm(state: State<TLGState>) {
|
public resetForm(state: State<TLGState>) {
|
||||||
state.merge({
|
state.merge({
|
||||||
queryVrf: '',
|
|
||||||
families: [],
|
families: [],
|
||||||
queryType: '',
|
queryType: '',
|
||||||
queryGroup: '',
|
queryGroup: '',
|
||||||
|
|
@ -81,10 +79,9 @@ class MethodsInstance {
|
||||||
btnLoading: false,
|
btnLoading: false,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
resolvedIsOpen: false,
|
resolvedIsOpen: false,
|
||||||
availVrfs: [],
|
|
||||||
availableGroups: [],
|
availableGroups: [],
|
||||||
availableTypes: [],
|
availableTypes: [],
|
||||||
selections: { queryLocation: [], queryType: null, queryVrf: null, queryGroup: null },
|
selections: { queryLocation: [], queryType: null, queryGroup: null },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,7 +147,7 @@ function Methods(inst?: State<TLGState>): Plugin | TMethodsExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
const LGState = createState<TLGState>({
|
const LGState = createState<TLGState>({
|
||||||
selections: { queryLocation: [], queryType: null, queryVrf: null, queryGroup: null },
|
selections: { queryLocation: [], queryType: null, queryGroup: null },
|
||||||
resolvedIsOpen: false,
|
resolvedIsOpen: false,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
availableGroups: [],
|
availableGroups: [],
|
||||||
|
|
@ -162,9 +159,7 @@ const LGState = createState<TLGState>({
|
||||||
queryTarget: '',
|
queryTarget: '',
|
||||||
queryGroup: '',
|
queryGroup: '',
|
||||||
queryType: '',
|
queryType: '',
|
||||||
availVrfs: [],
|
|
||||||
responses: {},
|
responses: {},
|
||||||
queryVrf: '',
|
|
||||||
families: [],
|
families: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,17 +42,17 @@ function formatTime(val: number): string {
|
||||||
*/
|
*/
|
||||||
export function useTableToString(
|
export function useTableToString(
|
||||||
target: string,
|
target: string,
|
||||||
data: TQueryResponse | undefined,
|
data: QueryResponse | undefined,
|
||||||
...deps: unknown[]
|
...deps: unknown[]
|
||||||
): () => string {
|
): () => string {
|
||||||
const { web, parsed_data_fields, messages } = useConfig();
|
const { web, parsedDataFields, messages } = useConfig();
|
||||||
|
|
||||||
function formatRpkiState(val: number): string {
|
function formatRpkiState(val: number): string {
|
||||||
const rpkiStates = [
|
const rpkiStates = [
|
||||||
web.text.rpki_invalid,
|
web.text.rpkiInvalid,
|
||||||
web.text.rpki_valid,
|
web.text.rpkiValid,
|
||||||
web.text.rpki_unknown,
|
web.text.rpkiUnknown,
|
||||||
web.text.rpki_unverified,
|
web.text.rpkiUnverified,
|
||||||
];
|
];
|
||||||
return rpkiStates[val];
|
return rpkiStates[val];
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +69,7 @@ export function useTableToString(
|
||||||
return key in tableFormatMap;
|
return key in tableFormatMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFmtFunc(accessor: keyof TRoute): TTableToStringFormatter {
|
function getFmtFunc(accessor: keyof Route): TTableToStringFormatter {
|
||||||
if (isFormatted(accessor)) {
|
if (isFormatted(accessor)) {
|
||||||
return tableFormatMap[accessor];
|
return tableFormatMap[accessor];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -77,13 +77,13 @@ export function useTableToString(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doFormat(target: string, data: TQueryResponse | undefined): string {
|
function doFormat(target: string, data: QueryResponse | undefined): string {
|
||||||
let result = messages.no_output;
|
let result = messages.noOutput;
|
||||||
try {
|
try {
|
||||||
if (typeof data !== 'undefined' && isStructuredOutput(data)) {
|
if (typeof data !== 'undefined' && isStructuredOutput(data)) {
|
||||||
const tableStringParts = [`Routes For: ${target}`, `Timestamp: ${data.timestamp} UTC`];
|
const tableStringParts = [`Routes For: ${target}`, `Timestamp: ${data.timestamp} UTC`];
|
||||||
for (const route of data.output.routes) {
|
for (const route of data.output.routes) {
|
||||||
for (const field of parsed_data_fields) {
|
for (const field of parsedDataFields) {
|
||||||
const [header, accessor, align] = field;
|
const [header, accessor, align] = field;
|
||||||
if (align !== null) {
|
if (align !== null) {
|
||||||
let value = route[accessor];
|
let value = route[accessor];
|
||||||
|
|
|
||||||
1
hyperglass/ui/package.json
vendored
1
hyperglass/ui/package.json
vendored
|
|
@ -69,6 +69,7 @@
|
||||||
"http-proxy-middleware": "0.20.0",
|
"http-proxy-middleware": "0.20.0",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"prettier-eslint": "^13.0.0",
|
"prettier-eslint": "^13.0.0",
|
||||||
|
"type-fest": "^2.3.2",
|
||||||
"typescript": "^4.4.2"
|
"typescript": "^4.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import type { Theme } from './theme';
|
import type { Theme } from './theme';
|
||||||
|
import type { CamelCasedPropertiesDeep, CamelCasedProperties } from 'type-fest';
|
||||||
|
|
||||||
export type TQueryFields = 'query_type' | 'query_target' | 'query_location' | 'query_vrf';
|
// export type QueryFields = 'query_type' | 'query_target' | 'query_location' | 'query_vrf';
|
||||||
|
|
||||||
type TSide = 'left' | 'right';
|
type Side = 'left' | 'right';
|
||||||
|
|
||||||
export interface IConfigMessages {
|
export type ParsedDataField = [string, keyof Route, 'left' | 'right' | 'center' | null];
|
||||||
|
|
||||||
|
interface _Messages {
|
||||||
no_input: string;
|
no_input: string;
|
||||||
acl_denied: string;
|
acl_denied: string;
|
||||||
acl_not_allowed: string;
|
acl_not_allowed: string;
|
||||||
|
|
@ -20,13 +23,13 @@ export interface IConfigMessages {
|
||||||
parsing_error: string;
|
parsing_error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigTheme {
|
interface _ThemeConfig {
|
||||||
colors: { [k: string]: string };
|
colors: Record<string, string>;
|
||||||
default_color_mode: 'light' | 'dark' | null;
|
default_color_mode: 'light' | 'dark' | null;
|
||||||
fonts: Theme.Fonts;
|
fonts: Theme.Fonts;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigWebText {
|
interface _Text {
|
||||||
title_mode: string;
|
title_mode: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
|
@ -48,145 +51,150 @@ export interface IConfigWebText {
|
||||||
no_communities: string;
|
no_communities: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TConfigGreeting {
|
interface _Greeting {
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
button: string;
|
button: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TConfigWebLogo {
|
interface _Logo {
|
||||||
width: string;
|
width: string;
|
||||||
height: string | null;
|
height: string | null;
|
||||||
light_format: string;
|
light_format: string;
|
||||||
dark_format: string;
|
dark_format: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLink {
|
interface _Link {
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
show_icon: boolean;
|
show_icon: boolean;
|
||||||
side: TSide;
|
side: Side;
|
||||||
order: number;
|
order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TMenu {
|
interface _Menu {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
side: TSide;
|
side: Side;
|
||||||
order: number;
|
order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigWeb {
|
interface _Credit {
|
||||||
credit: { enable: boolean };
|
|
||||||
dns_provider: { name: string; url: string };
|
|
||||||
links: TLink[];
|
|
||||||
menus: TMenu[];
|
|
||||||
greeting: TConfigGreeting;
|
|
||||||
help_menu: { enable: boolean; title: string };
|
|
||||||
logo: TConfigWebLogo;
|
|
||||||
terms: { enable: boolean; title: string };
|
|
||||||
text: IConfigWebText;
|
|
||||||
theme: IConfigTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TQuery {
|
|
||||||
name: string;
|
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
display_name: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TBGPCommunity {
|
interface _Web {
|
||||||
community: string;
|
credit: _Credit;
|
||||||
display_name: string;
|
dns_provider: { name: string; url: string };
|
||||||
description: string;
|
links: _Link[];
|
||||||
|
menus: _Menu[];
|
||||||
|
greeting: _Greeting;
|
||||||
|
help_menu: { enable: boolean; title: string };
|
||||||
|
logo: _Logo;
|
||||||
|
terms: { enable: boolean; title: string };
|
||||||
|
text: _Text;
|
||||||
|
theme: _ThemeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IQueryBGPRoute extends TQuery {}
|
// export interface Query {
|
||||||
export interface IQueryBGPASPath extends TQuery {}
|
// name: string;
|
||||||
export interface IQueryPing extends TQuery {}
|
// enable: boolean;
|
||||||
export interface IQueryTraceroute extends TQuery {}
|
// display_name: string;
|
||||||
export interface IQueryBGPCommunity extends TQuery {
|
// }
|
||||||
mode: 'input' | 'select';
|
|
||||||
communities: TBGPCommunity[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TConfigQueries {
|
// export interface BGPCommunity {
|
||||||
bgp_route: IQueryBGPRoute;
|
// community: string;
|
||||||
bgp_community: IQueryBGPCommunity;
|
// display_name: string;
|
||||||
bgp_aspath: IQueryBGPASPath;
|
// description: string;
|
||||||
ping: IQueryPing;
|
// }
|
||||||
traceroute: IQueryTraceroute;
|
|
||||||
list: TQuery[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TDeviceVrfBase {
|
// export interface QueryBGPRoute extends Query {}
|
||||||
_id: string;
|
// export interface QueryBGPASPath extends Query {}
|
||||||
display_name: string;
|
// export interface QueryPing extends Query {}
|
||||||
default: boolean;
|
// export interface QueryTraceroute extends Query {}
|
||||||
}
|
// export interface QueryBGPCommunity extends Query {
|
||||||
|
// mode: 'input' | 'select';
|
||||||
|
// communities: BGPCommunity[];
|
||||||
|
// }
|
||||||
|
|
||||||
export interface TDeviceVrf extends TDeviceVrfBase {
|
// export interface Queries {
|
||||||
ipv4: boolean;
|
// bgp_route: QueryBGPRoute;
|
||||||
ipv6: boolean;
|
// bgp_community: QueryBGPCommunity;
|
||||||
}
|
// bgp_aspath: QueryBGPASPath;
|
||||||
|
// ping: QueryPing;
|
||||||
|
// traceroute: QueryTraceroute;
|
||||||
|
// list: Query[];
|
||||||
|
// }
|
||||||
|
|
||||||
type TDirectiveBase = {
|
type _DirectiveBase = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
field_type: 'text' | 'select' | null;
|
field_type: 'text' | 'select' | null;
|
||||||
description: string;
|
description: string;
|
||||||
groups: string[];
|
groups: string[];
|
||||||
info: TQueryContent | null;
|
info: _QueryContent | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDirectiveOption = {
|
type _DirectiveOption = {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDirectiveSelect = TDirectiveBase & {
|
type _DirectiveSelect = _DirectiveBase & {
|
||||||
options: TDirectiveOption[];
|
options: _DirectiveOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDirective = TDirectiveBase | TDirectiveSelect;
|
type _Directive = _DirectiveBase | _DirectiveSelect;
|
||||||
|
|
||||||
export interface TDevice {
|
interface _Device {
|
||||||
_id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
network: string;
|
network: string;
|
||||||
directives: TDirective[];
|
directives: _Directive[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TNetworkLocation extends TDevice {}
|
interface _Network {
|
||||||
|
|
||||||
export interface TNetwork {
|
|
||||||
display_name: string;
|
display_name: string;
|
||||||
locations: TDevice[];
|
locations: _Device[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TParsedDataField = [string, keyof TRoute, 'left' | 'right' | 'center' | null];
|
interface _QueryContent {
|
||||||
|
|
||||||
export interface TQueryContent {
|
|
||||||
content: string;
|
content: string;
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
params: {
|
params: {
|
||||||
primary_asn: IConfig['primary_asn'];
|
primary_asn: _Config['primary_asn'];
|
||||||
org_name: IConfig['org_name'];
|
org_name: _Config['org_name'];
|
||||||
site_title: IConfig['site_title'];
|
site_title: _Config['site_title'];
|
||||||
title: string;
|
title: string;
|
||||||
[k: string]: string;
|
[k: string]: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigContent {
|
interface _Content {
|
||||||
credit: string;
|
credit: string;
|
||||||
greeting: string;
|
greeting: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfig {
|
interface _Cache {
|
||||||
cache: { show_text: boolean; timeout: number };
|
show_text: boolean;
|
||||||
|
timeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type _Config = _ConfigDeep & _ConfigShallow;
|
||||||
|
|
||||||
|
interface _ConfigDeep {
|
||||||
|
cache: _Cache;
|
||||||
|
web: _Web;
|
||||||
|
messages: _Messages;
|
||||||
|
// queries: Queries;
|
||||||
|
devices: _Device[];
|
||||||
|
networks: _Network[];
|
||||||
|
content: _Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _ConfigShallow {
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
developer_mode: boolean;
|
developer_mode: boolean;
|
||||||
primary_asn: string;
|
primary_asn: string;
|
||||||
|
|
@ -196,14 +204,8 @@ export interface IConfig {
|
||||||
site_title: string;
|
site_title: string;
|
||||||
site_keywords: string[];
|
site_keywords: string[];
|
||||||
site_description: string;
|
site_description: string;
|
||||||
web: IConfigWeb;
|
version: string;
|
||||||
messages: IConfigMessages;
|
parsed_data_fields: ParsedDataField[];
|
||||||
hyperglass_version: string;
|
|
||||||
queries: TConfigQueries;
|
|
||||||
devices: TDevice[];
|
|
||||||
networks: TNetwork[];
|
|
||||||
parsed_data_fields: TParsedDataField[];
|
|
||||||
content: IConfigContent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Favicon {
|
export interface Favicon {
|
||||||
|
|
@ -218,3 +220,19 @@ export interface FaviconComponent {
|
||||||
href: string;
|
href: string;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Config = CamelCasedPropertiesDeep<_ConfigDeep> & CamelCasedProperties<_ConfigShallow>;
|
||||||
|
export type ThemeConfig = CamelCasedProperties<_ThemeConfig>;
|
||||||
|
export type Content = CamelCasedProperties<_Content>;
|
||||||
|
export type QueryContent = CamelCasedPropertiesDeep<_QueryContent>;
|
||||||
|
export type Network = CamelCasedPropertiesDeep<_Network>;
|
||||||
|
export type Device = CamelCasedPropertiesDeep<_Device>;
|
||||||
|
export type Directive = CamelCasedPropertiesDeep<_Directive>;
|
||||||
|
export type DirectiveSelect = CamelCasedPropertiesDeep<_DirectiveSelect>;
|
||||||
|
export type DirectiveOption = CamelCasedPropertiesDeep<_DirectiveOption>;
|
||||||
|
export type Text = CamelCasedProperties<_Text>;
|
||||||
|
export type Web = CamelCasedPropertiesDeep<_Web>;
|
||||||
|
export type Greeting = CamelCasedProperties<_Greeting>;
|
||||||
|
export type Logo = CamelCasedProperties<_Logo>;
|
||||||
|
export type Link = CamelCasedProperties<_Link>;
|
||||||
|
export type Menu = CamelCasedProperties<_Menu>;
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,21 @@
|
||||||
export type TQueryTypes = '' | TValidQueryTypes;
|
export type TQueryTypes = '' | TValidQueryTypes;
|
||||||
export type TValidQueryTypes = 'bgp_route' | 'bgp_community' | 'bgp_aspath' | 'ping' | 'traceroute';
|
export type TValidQueryTypes = 'bgp_route' | 'bgp_community' | 'bgp_aspath' | 'ping' | 'traceroute';
|
||||||
|
|
||||||
export interface TFormData {
|
export interface FormData {
|
||||||
query_location: string[];
|
|
||||||
query_type: TQueryTypes;
|
|
||||||
query_vrf: string;
|
|
||||||
query_target: string;
|
|
||||||
query_group: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TFormState {
|
|
||||||
queryLocation: string[];
|
queryLocation: string[];
|
||||||
queryType: string;
|
queryType: string;
|
||||||
queryVrf: string;
|
|
||||||
queryTarget: string;
|
queryTarget: string;
|
||||||
queryGroup: string;
|
queryGroup: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TFormQuery extends Omit<TFormState, 'queryLocation'> {
|
export interface TFormQuery extends Omit<FormData, 'queryLocation'> {
|
||||||
queryLocation: string;
|
queryLocation: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TStringTableData extends Omit<TQueryResponse, 'output'> {
|
export interface TStringTableData extends Omit<QueryResponse, 'output'> {
|
||||||
output: TStructuredResponse;
|
output: StructuredResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TQueryResponseString extends Omit<TQueryResponse, 'output'> {
|
export interface TQueryResponseString extends Omit<QueryResponse, 'output'> {
|
||||||
output: string;
|
output: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
hyperglass/ui/types/globals.d.ts
vendored
38
hyperglass/ui/types/globals.d.ts
vendored
|
|
@ -6,11 +6,11 @@ declare global {
|
||||||
|
|
||||||
type Nullable<T> = T | null;
|
type Nullable<T> = T | null;
|
||||||
|
|
||||||
type TRPKIStates = 0 | 1 | 2 | 3;
|
type RPKIState = 0 | 1 | 2 | 3;
|
||||||
|
|
||||||
type TResponseLevel = 'success' | 'warning' | 'error' | 'danger';
|
type ResponseLevel = 'success' | 'warning' | 'error' | 'danger';
|
||||||
|
|
||||||
interface IRoute {
|
type Route = {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
age: number;
|
age: number;
|
||||||
|
|
@ -23,40 +23,26 @@ declare global {
|
||||||
source_as: number;
|
source_as: number;
|
||||||
source_rid: string;
|
source_rid: string;
|
||||||
peer_rid: string;
|
peer_rid: string;
|
||||||
rpki_state: TRPKIStates;
|
rpki_state: RPKIState;
|
||||||
}
|
|
||||||
|
|
||||||
type TRoute = {
|
|
||||||
prefix: string;
|
|
||||||
active: boolean;
|
|
||||||
age: number;
|
|
||||||
weight: number;
|
|
||||||
med: number;
|
|
||||||
local_preference: number;
|
|
||||||
as_path: number[];
|
|
||||||
communities: string[];
|
|
||||||
next_hop: string;
|
|
||||||
source_as: number;
|
|
||||||
source_rid: string;
|
|
||||||
peer_rid: string;
|
|
||||||
rpki_state: TRPKIStates;
|
|
||||||
};
|
};
|
||||||
type TRouteField = { [k in keyof TRoute]: ValueOf<TRoute> };
|
|
||||||
|
|
||||||
type TStructuredResponse = {
|
type RouteField = { [K in keyof Route]: Route[K] };
|
||||||
|
|
||||||
|
type StructuredResponse = {
|
||||||
vrf: string;
|
vrf: string;
|
||||||
count: number;
|
count: number;
|
||||||
routes: TRoute[];
|
routes: Route[];
|
||||||
winning_weight: 'high' | 'low';
|
winning_weight: 'high' | 'low';
|
||||||
};
|
};
|
||||||
type TQueryResponse = {
|
|
||||||
|
type QueryResponse = {
|
||||||
random: string;
|
random: string;
|
||||||
cached: boolean;
|
cached: boolean;
|
||||||
runtime: number;
|
runtime: number;
|
||||||
level: TResponseLevel;
|
level: ResponseLevel;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
output: string | TStructuredResponse;
|
output: string | StructuredResponse;
|
||||||
format: 'text/plain' | 'application/json';
|
format: 'text/plain' | 'application/json';
|
||||||
};
|
};
|
||||||
type ReactRef<T = HTMLElement> = MutableRefObject<T>;
|
type ReactRef<T = HTMLElement> = MutableRefObject<T>;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { State } from '@hookstate/core';
|
import type { State } from '@hookstate/core';
|
||||||
import type { TFormData, TStringTableData, TQueryResponseString } from './data';
|
import type { FormData, TStringTableData, TQueryResponseString } from './data';
|
||||||
import type { TSelectOption } from './common';
|
import type { TSelectOption } from './common';
|
||||||
import type { TQueryContent, TDirectiveSelect, TDirective } from './config';
|
import type { QueryContent, DirectiveSelect, Directive } from './config';
|
||||||
|
|
||||||
export function isString(a: unknown): a is string {
|
export function isString(a: unknown): a is string {
|
||||||
return typeof a === 'string';
|
return typeof a === 'string';
|
||||||
|
|
@ -27,7 +27,7 @@ export function isStringOutput(data: unknown): data is TQueryResponseString {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isQueryContent(content: unknown): content is TQueryContent {
|
export function isQueryContent(content: unknown): content is QueryContent {
|
||||||
return isObject(content) && 'content' in content;
|
return isObject(content) && 'content' in content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,13 +52,13 @@ export function isState<S>(a: unknown): a is State<NonNullable<S>> {
|
||||||
/**
|
/**
|
||||||
* Determine if a form field name is a valid form key name.
|
* Determine if a form field name is a valid form key name.
|
||||||
*/
|
*/
|
||||||
export function isQueryField(field: string): field is keyof TFormData {
|
export function isQueryField(field: string): field is keyof FormData {
|
||||||
return ['query_location', 'query_type', 'query_group', 'query_target'].includes(field);
|
return ['queryLocation', 'queryType', 'queryGroup', 'queryTarget'].includes(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a directive is a select directive.
|
* Determine if a directive is a select directive.
|
||||||
*/
|
*/
|
||||||
export function isSelectDirective(directive: TDirective): directive is TDirectiveSelect {
|
export function isSelectDirective(directive: Directive): directive is DirectiveSelect {
|
||||||
return directive.field_type === 'select';
|
return directive.fieldType === 'select';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import type { CellProps } from 'react-table';
|
||||||
|
|
||||||
export interface TColumn {
|
export interface TColumn {
|
||||||
Header: string;
|
Header: string;
|
||||||
accessor: keyof TRoute;
|
accessor: keyof Route;
|
||||||
align: string;
|
align: string;
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCellRender = {
|
export type TCellRender = {
|
||||||
column: CellProps<TRouteField>['column'];
|
column: CellProps<RouteField>['column'];
|
||||||
row: CellProps<TRouteField>['row'];
|
row: CellProps<RouteField>['row'];
|
||||||
value: CellProps<TRouteField>['value'];
|
value: CellProps<RouteField>['value'];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,18 @@ export function arrangeIntoTree<P extends unknown>(paths: P[][]): PathPart[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strictly typed version of `Object.entries()`.
|
||||||
|
*/
|
||||||
|
export function entries<O, K extends keyof O = keyof O>(obj: O): [K, O[K]][] {
|
||||||
|
const _entries = [] as [K, O[K]][];
|
||||||
|
const keys = Object.keys(obj) as K[];
|
||||||
|
for (const key of keys) {
|
||||||
|
_entries.push([key, obj[key]]);
|
||||||
|
}
|
||||||
|
return _entries;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch Wrapper that incorporates a timeout via a passed AbortController instance.
|
* Fetch Wrapper that incorporates a timeout via a passed AbortController instance.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { isObject } from '~/types';
|
import { isObject } from '~/types';
|
||||||
|
|
||||||
import type { IConfig, FaviconComponent } from '~/types';
|
import type { Config, FaviconComponent } from '~/types';
|
||||||
|
|
||||||
export class ConfigLoadError extends Error {
|
export class ConfigLoadError extends Error {
|
||||||
public url: string = '/ui/props/';
|
public url: string = '/ui/props/';
|
||||||
|
|
@ -23,7 +23,7 @@ export class ConfigLoadError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHyperglassConfig(): Promise<IConfig> {
|
export async function getHyperglassConfig(): Promise<Config> {
|
||||||
let mode: RequestInit['mode'];
|
let mode: RequestInit['mode'];
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
|
@ -40,7 +40,7 @@ export async function getHyperglassConfig(): Promise<IConfig> {
|
||||||
throw response;
|
throw response;
|
||||||
}
|
}
|
||||||
if (isObject(data)) {
|
if (isObject(data)) {
|
||||||
return data as IConfig;
|
return data as Config;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TypeError) {
|
if (error instanceof TypeError) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { mode } from '@chakra-ui/theme-tools';
|
||||||
import { generateFontFamily, generatePalette } from 'palette-by-numbers';
|
import { generateFontFamily, generatePalette } from 'palette-by-numbers';
|
||||||
|
|
||||||
import type { ChakraTheme } from '@chakra-ui/react';
|
import type { ChakraTheme } from '@chakra-ui/react';
|
||||||
import type { IConfigTheme, Theme } from '~/types';
|
import type { ThemeConfig, Theme } from '~/types';
|
||||||
|
|
||||||
function importFonts(userFonts: Theme.Fonts): ChakraTheme['fonts'] {
|
function importFonts(userFonts: Theme.Fonts): ChakraTheme['fonts'] {
|
||||||
const { body: userBody, mono: userMono } = userFonts;
|
const { body: userBody, mono: userMono } = userFonts;
|
||||||
|
|
@ -15,9 +15,9 @@ function importFonts(userFonts: Theme.Fonts): ChakraTheme['fonts'] {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function importColors(userColors: IConfigTheme['colors']): Theme.Colors {
|
function importColors(userColors: ThemeConfig['colors']): Theme.Colors {
|
||||||
const generatedColors = {} as Theme.Colors;
|
const generatedColors = {} as Theme.Colors;
|
||||||
for (const [k, v] of Object.entries(userColors)) {
|
for (const [k, v] of Object.entries<string>(userColors)) {
|
||||||
generatedColors[k] = generatePalette(v);
|
generatedColors[k] = generatePalette(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ function importColors(userColors: IConfigTheme['colors']): Theme.Colors {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeTheme(
|
export function makeTheme(
|
||||||
userTheme: IConfigTheme,
|
userTheme: ThemeConfig,
|
||||||
defaultColorMode: 'dark' | 'light' | null,
|
defaultColorMode: 'dark' | 'light' | null,
|
||||||
): Theme.Full {
|
): Theme.Full {
|
||||||
const fonts = importFonts(userTheme.fonts);
|
const fonts = importFonts(userTheme.fonts);
|
||||||
|
|
|
||||||
5
hyperglass/ui/yarn.lock
vendored
5
hyperglass/ui/yarn.lock
vendored
|
|
@ -5995,6 +5995,11 @@ type-fest@^0.7.1:
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48"
|
||||||
integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==
|
integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==
|
||||||
|
|
||||||
|
type-fest@^2.3.2:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.3.2.tgz#bb91f7ff24788ed81e28463eb94e5a1306f5bab3"
|
||||||
|
integrity sha512-cfvZ1nOC/VqAt8bVOIlFz8x+HdDASpiFYrSi0U0nzcAFlOnzzQ/gsPg2PP1uqjreO7sQCtraYJHMduXSewQsSA==
|
||||||
|
|
||||||
type-is@~1.6.17, type-is@~1.6.18:
|
type-is@~1.6.17, type-is@~1.6.18:
|
||||||
version "1.6.18"
|
version "1.6.18"
|
||||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ reportMissingImports = true
|
||||||
reportMissingTypeStubs = true
|
reportMissingTypeStubs = true
|
||||||
|
|
||||||
[tool.taskipy.tasks]
|
[tool.taskipy.tasks]
|
||||||
|
check = {cmd = "task lint && task ui-lint", help = "Run all lint checks"}
|
||||||
lint = {cmd = "flake8 hyperglass", help = "Run Flake8"}
|
lint = {cmd = "flake8 hyperglass", help = "Run Flake8"}
|
||||||
sort = {cmd = "isort hyperglass", help = "Run iSort"}
|
sort = {cmd = "isort hyperglass", help = "Run iSort"}
|
||||||
start = {cmd = "uvicorn hyperglass.api:app", help = "Start hyperglass via Uvicorn"}
|
start = {cmd = "uvicorn hyperglass.api:app", help = "Start hyperglass via Uvicorn"}
|
||||||
|
|
@ -103,3 +104,4 @@ ui-format = {cmd = "yarn --cwd ./hyperglass/ui/ format", help = "Run Prettier"}
|
||||||
ui-lint = {cmd = "yarn --cwd ./hyperglass/ui/ lint", help = "Run ESLint"}
|
ui-lint = {cmd = "yarn --cwd ./hyperglass/ui/ lint", help = "Run ESLint"}
|
||||||
ui-typecheck = {cmd = "yarn --cwd ./hyperglass/ui/ typecheck", help = "Run TypeScript Check"}
|
ui-typecheck = {cmd = "yarn --cwd ./hyperglass/ui/ typecheck", help = "Run TypeScript Check"}
|
||||||
upgrade = {cmd = "python3 version.py", help = "Upgrade hyperglass version"}
|
upgrade = {cmd = "python3 version.py", help = "Upgrade hyperglass version"}
|
||||||
|
yarn = {cmd = "yarn --cwd ./hyperglass/ui/", help = "Run a yarn command from the UI directory"}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue