forked from mirrors/thatmattlove-hyperglass
initial work on generic commands
This commit is contained in:
parent
7636e044b2
commit
5f036228a5
21 changed files with 488 additions and 210 deletions
|
|
@ -4,7 +4,7 @@
|
||||||
import os
|
import os
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Sequence, Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
|
|
@ -25,9 +25,11 @@ from hyperglass.constants import (
|
||||||
)
|
)
|
||||||
from hyperglass.exceptions import ConfigError, ConfigMissing
|
from hyperglass.exceptions import ConfigError, ConfigMissing
|
||||||
from hyperglass.util.files import check_path
|
from hyperglass.util.files import check_path
|
||||||
from hyperglass.models.commands import Commands
|
|
||||||
|
# from hyperglass.models.commands import Commands
|
||||||
|
from hyperglass.models.commands.generic import Directive
|
||||||
from hyperglass.models.config.params import Params
|
from hyperglass.models.config.params import Params
|
||||||
from hyperglass.models.config.devices import Devices
|
from hyperglass.models.config.devices import Devices, Device
|
||||||
from hyperglass.configuration.defaults import (
|
from hyperglass.configuration.defaults import (
|
||||||
CREDIT,
|
CREDIT,
|
||||||
DEFAULT_HELP,
|
DEFAULT_HELP,
|
||||||
|
|
@ -116,6 +118,29 @@ def _config_optional(config_path: Path) -> Dict:
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _get_commands(data: Dict) -> Sequence[Directive]:
|
||||||
|
commands = []
|
||||||
|
for name, command in data.items():
|
||||||
|
commands.append(Directive(id=name, **command))
|
||||||
|
return commands
|
||||||
|
|
||||||
|
|
||||||
|
def _device_commands(
|
||||||
|
device: Dict, directives: Sequence[Directive]
|
||||||
|
) -> Generator[Directive, None, None]:
|
||||||
|
device_commands = device.get("commands", [])
|
||||||
|
for directive in directives:
|
||||||
|
if directive.id in device_commands:
|
||||||
|
yield directive
|
||||||
|
|
||||||
|
|
||||||
|
def _get_devices(data: Sequence[Dict], directives: Sequence[Directive]) -> Devices:
|
||||||
|
for device in data:
|
||||||
|
device_commands = list(_device_commands(device, directives))
|
||||||
|
device["commands"] = device_commands
|
||||||
|
return Devices(data)
|
||||||
|
|
||||||
|
|
||||||
user_config = _config_optional(CONFIG_MAIN)
|
user_config = _config_optional(CONFIG_MAIN)
|
||||||
|
|
||||||
# Read raw debug value from config to enable debugging quickly.
|
# Read raw debug value from config to enable debugging quickly.
|
||||||
|
|
@ -136,15 +161,16 @@ elif not params.debug and log_level == "debug":
|
||||||
# Map imported user commands to expected schema.
|
# Map imported user commands to expected schema.
|
||||||
_user_commands = _config_optional(CONFIG_COMMANDS)
|
_user_commands = _config_optional(CONFIG_COMMANDS)
|
||||||
log.debug("Unvalidated commands from {}: {}", CONFIG_COMMANDS, _user_commands)
|
log.debug("Unvalidated commands from {}: {}", CONFIG_COMMANDS, _user_commands)
|
||||||
commands = validate_config(config=_user_commands, importer=Commands.import_params)
|
commands = _get_commands(_user_commands)
|
||||||
|
|
||||||
# Map imported user devices to expected schema.
|
# Map imported user devices to expected schema.
|
||||||
_user_devices = _config_required(CONFIG_DEVICES)
|
_user_devices = _config_required(CONFIG_DEVICES)
|
||||||
log.debug("Unvalidated devices from {}: {}", CONFIG_DEVICES, _user_devices)
|
log.debug("Unvalidated devices from {}: {}", CONFIG_DEVICES, _user_devices)
|
||||||
devices = validate_config(config=_user_devices.get("routers", []), importer=Devices)
|
# devices = validate_config(config=_user_devices.get("routers", []), importer=Devices)
|
||||||
|
devices = _get_devices(_user_devices.get("routers", []), commands)
|
||||||
|
|
||||||
# Validate commands are both supported and properly mapped.
|
# Validate commands are both supported and properly mapped.
|
||||||
validate_nos_commands(devices.all_nos, commands)
|
# validate_nos_commands(devices.all_nos, commands)
|
||||||
|
|
||||||
# Set cache configurations to environment variables, so they can be
|
# Set cache configurations to environment variables, so they can be
|
||||||
# used without importing this module (Gunicorn, etc).
|
# used without importing this module (Gunicorn, etc).
|
||||||
|
|
@ -189,65 +215,6 @@ except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _build_frontend_devices():
|
|
||||||
"""Build filtered JSON structure of devices for frontend.
|
|
||||||
|
|
||||||
Schema:
|
|
||||||
{
|
|
||||||
"device.name": {
|
|
||||||
"display_name": "device.display_name",
|
|
||||||
"vrfs": [
|
|
||||||
"Global",
|
|
||||||
"vrf.display_name"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConfigError: Raised if parsing/building error occurs.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
{dict} -- Frontend devices
|
|
||||||
"""
|
|
||||||
frontend_dict = {}
|
|
||||||
for device in devices.objects:
|
|
||||||
if device.name in frontend_dict:
|
|
||||||
frontend_dict[device.name].update(
|
|
||||||
{
|
|
||||||
"network": device.network.display_name,
|
|
||||||
"display_name": device.display_name,
|
|
||||||
"vrfs": [
|
|
||||||
{
|
|
||||||
"id": vrf.name,
|
|
||||||
"display_name": vrf.display_name,
|
|
||||||
"default": vrf.default,
|
|
||||||
"ipv4": True if vrf.ipv4 else False, # noqa: IF100
|
|
||||||
"ipv6": True if vrf.ipv6 else False, # noqa: IF100
|
|
||||||
}
|
|
||||||
for vrf in device.vrfs
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif device.name not in frontend_dict:
|
|
||||||
frontend_dict[device.name] = {
|
|
||||||
"network": device.network.display_name,
|
|
||||||
"display_name": device.display_name,
|
|
||||||
"vrfs": [
|
|
||||||
{
|
|
||||||
"id": vrf.name,
|
|
||||||
"display_name": vrf.display_name,
|
|
||||||
"default": vrf.default,
|
|
||||||
"ipv4": True if vrf.ipv4 else False, # noqa: IF100
|
|
||||||
"ipv6": True if vrf.ipv6 else False, # noqa: IF100
|
|
||||||
}
|
|
||||||
for vrf in device.vrfs
|
|
||||||
],
|
|
||||||
}
|
|
||||||
if not frontend_dict:
|
|
||||||
raise ConfigError(error_msg="Unable to build network to device mapping")
|
|
||||||
return frontend_dict
|
|
||||||
|
|
||||||
|
|
||||||
def _build_networks() -> List[Dict]:
|
def _build_networks() -> List[Dict]:
|
||||||
"""Build filtered JSON Structure of networks & devices for Jinja templates."""
|
"""Build filtered JSON Structure of networks & devices for Jinja templates."""
|
||||||
networks = []
|
networks = []
|
||||||
|
|
@ -262,6 +229,7 @@ def _build_networks() -> List[Dict]:
|
||||||
"_id": device._id,
|
"_id": device._id,
|
||||||
"name": device.name,
|
"name": device.name,
|
||||||
"network": device.network.display_name,
|
"network": device.network.display_name,
|
||||||
|
"directives": [c.frontend(params) for c in device.commands],
|
||||||
"vrfs": [
|
"vrfs": [
|
||||||
{
|
{
|
||||||
"_id": vrf._id,
|
"_id": vrf._id,
|
||||||
|
|
@ -346,7 +314,7 @@ content_terms = get_markdown(
|
||||||
content_credit = CREDIT.format(version=__version__)
|
content_credit = CREDIT.format(version=__version__)
|
||||||
|
|
||||||
networks = _build_networks()
|
networks = _build_networks()
|
||||||
frontend_devices = _build_frontend_devices()
|
|
||||||
_include_fields = {
|
_include_fields = {
|
||||||
"cache": {"show_text", "timeout"},
|
"cache": {"show_text", "timeout"},
|
||||||
"debug": ...,
|
"debug": ...,
|
||||||
|
|
|
||||||
133
hyperglass/models/commands/generic.py
Normal file
133
hyperglass/models/commands/generic.py
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
import json
|
||||||
|
from ipaddress import IPv4Network, IPv6Network
|
||||||
|
from typing import Optional, Sequence, Union, Dict
|
||||||
|
from typing_extensions import Literal
|
||||||
|
from pydantic import StrictStr, PrivateAttr, conint, validator, FilePath
|
||||||
|
from ..main import HyperglassModel
|
||||||
|
from ..config.params import Params
|
||||||
|
from hyperglass.configuration.markdown import get_markdown
|
||||||
|
|
||||||
|
IPv4PrefixLength = conint(ge=0, le=32)
|
||||||
|
IPv6PrefixLength = conint(ge=0, le=128)
|
||||||
|
|
||||||
|
|
||||||
|
class Policy(HyperglassModel):
|
||||||
|
network: Union[IPv4Network, IPv6Network]
|
||||||
|
action: Literal["permit", "deny"]
|
||||||
|
|
||||||
|
@validator("ge", check_fields=False)
|
||||||
|
def validate_ge(cls, value: int, values: Dict) -> int:
|
||||||
|
"""Ensure ge is at least the size of the input prefix."""
|
||||||
|
|
||||||
|
network_len = values["network"].prefixlen
|
||||||
|
|
||||||
|
if network_len > value:
|
||||||
|
value = network_len
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class Policy4(Policy):
|
||||||
|
ge: IPv4PrefixLength = 0
|
||||||
|
le: IPv4PrefixLength = 32
|
||||||
|
|
||||||
|
|
||||||
|
class Policy6(Policy):
|
||||||
|
ge: IPv6PrefixLength = 0
|
||||||
|
le: IPv6PrefixLength = 128
|
||||||
|
|
||||||
|
|
||||||
|
class Input(HyperglassModel):
|
||||||
|
_type: PrivateAttr
|
||||||
|
description: StrictStr
|
||||||
|
|
||||||
|
def is_select(self) -> bool:
|
||||||
|
return self._type == "select"
|
||||||
|
|
||||||
|
def is_text(self) -> bool:
|
||||||
|
return self._type == "text"
|
||||||
|
|
||||||
|
def is_ip(self) -> bool:
|
||||||
|
return self._type == "ip"
|
||||||
|
|
||||||
|
|
||||||
|
class Text(Input):
|
||||||
|
_type: PrivateAttr = "text"
|
||||||
|
validation: Optional[StrictStr]
|
||||||
|
|
||||||
|
|
||||||
|
class IPInput(Input):
|
||||||
|
_type: PrivateAttr = "ip"
|
||||||
|
validation: Union[Policy4, Policy6]
|
||||||
|
|
||||||
|
|
||||||
|
class Option(HyperglassModel):
|
||||||
|
name: Optional[StrictStr]
|
||||||
|
value: StrictStr
|
||||||
|
|
||||||
|
|
||||||
|
class Select(Input):
|
||||||
|
_type: PrivateAttr = "select"
|
||||||
|
options: Sequence[Option]
|
||||||
|
|
||||||
|
|
||||||
|
class Directive(HyperglassModel):
|
||||||
|
id: StrictStr
|
||||||
|
name: StrictStr
|
||||||
|
command: Union[StrictStr, Sequence[StrictStr]]
|
||||||
|
field: Union[Text, Select, IPInput, None]
|
||||||
|
info: Optional[FilePath]
|
||||||
|
attrs: Dict = {}
|
||||||
|
groups: Sequence[
|
||||||
|
StrictStr
|
||||||
|
] = [] # TODO: Flesh this out. Replace VRFs, but use same logic in React to filter available commands for multi-device queries.
|
||||||
|
|
||||||
|
@validator("command")
|
||||||
|
def validate_command(cls, value: Union[str, Sequence[str]]) -> Sequence[str]:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return [value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_commands(self, target: str) -> Sequence[str]:
|
||||||
|
return [s.format(target=target, **self.attrs) for s in self.command]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def field_type(self) -> Literal["text", "select", None]:
|
||||||
|
if self.field.is_select():
|
||||||
|
return "select"
|
||||||
|
elif self.field.is_text() or self.field.is_ip():
|
||||||
|
return "text"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def frontend(self, params: Params) -> Dict:
|
||||||
|
|
||||||
|
value = {
|
||||||
|
"name": self.name,
|
||||||
|
"field_type": self.field_type,
|
||||||
|
"groups": self.groups,
|
||||||
|
"description": self.field.description,
|
||||||
|
"info": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.info is not None:
|
||||||
|
content_params = json.loads(
|
||||||
|
params.json(
|
||||||
|
include={
|
||||||
|
"primary_asn",
|
||||||
|
"org_name",
|
||||||
|
"site_title",
|
||||||
|
"site_description",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with self.info.open() as md:
|
||||||
|
value["info"] = {
|
||||||
|
"enable": True,
|
||||||
|
"params": content_params,
|
||||||
|
"content": md.read(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.field_type == "select":
|
||||||
|
value["options"]: [o.export_dict() for o in self.field.options]
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
from typing import Any, Dict, List, Tuple, Union, Optional, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
|
|
||||||
|
|
@ -22,6 +22,8 @@ from hyperglass.log import log
|
||||||
from hyperglass.util import get_driver, validate_nos, resolve_hostname
|
from hyperglass.util import get_driver, validate_nos, resolve_hostname
|
||||||
from hyperglass.constants import SCRAPE_HELPERS, SUPPORTED_STRUCTURED_OUTPUT
|
from hyperglass.constants import SCRAPE_HELPERS, SUPPORTED_STRUCTURED_OUTPUT
|
||||||
from hyperglass.exceptions import ConfigError, UnsupportedDevice
|
from hyperglass.exceptions import ConfigError, UnsupportedDevice
|
||||||
|
from hyperglass.models.commands.generic import Directive
|
||||||
|
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .ssl import Ssl
|
from .ssl import Ssl
|
||||||
|
|
@ -89,10 +91,8 @@ class Device(HyperglassModel):
|
||||||
port: StrictInt = 22
|
port: StrictInt = 22
|
||||||
ssl: Optional[Ssl]
|
ssl: Optional[Ssl]
|
||||||
nos: StrictStr
|
nos: StrictStr
|
||||||
commands: Optional[StrictStr]
|
commands: Sequence[Directive]
|
||||||
vrfs: List[Vrf] = [_default_vrf]
|
vrfs: List[Vrf] = [_default_vrf]
|
||||||
display_vrfs: List[StrictStr] = []
|
|
||||||
vrf_names: List[StrictStr] = []
|
|
||||||
structured_output: Optional[StrictBool]
|
structured_output: Optional[StrictBool]
|
||||||
driver: Optional[SupportedDriver]
|
driver: Optional[SupportedDriver]
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ class Device(HyperglassModel):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@root_validator(pre=True)
|
||||||
def validate_nos_commands(cls, values: "Device") -> "Device":
|
def validate_nos_commands(cls, values: Dict) -> Dict:
|
||||||
"""Validate & rewrite NOS, set default commands."""
|
"""Validate & rewrite NOS, set default commands."""
|
||||||
|
|
||||||
nos = values.get("nos", "")
|
nos = values.get("nos", "")
|
||||||
|
|
@ -205,7 +205,7 @@ class Device(HyperglassModel):
|
||||||
if "_telnet" in inferred:
|
if "_telnet" in inferred:
|
||||||
inferred = inferred.replace("_telnet", "")
|
inferred = inferred.replace("_telnet", "")
|
||||||
|
|
||||||
values["commands"] = inferred
|
values["commands"] = [inferred]
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
@ -288,7 +288,6 @@ class Devices(HyperglassModelExtra):
|
||||||
_ids: List[StrictStr] = []
|
_ids: List[StrictStr] = []
|
||||||
hostnames: List[StrictStr] = []
|
hostnames: List[StrictStr] = []
|
||||||
vrfs: List[StrictStr] = []
|
vrfs: List[StrictStr] = []
|
||||||
display_vrfs: List[StrictStr] = []
|
|
||||||
vrf_objects: List[Vrf] = []
|
vrf_objects: List[Vrf] = []
|
||||||
objects: List[Device] = []
|
objects: List[Device] = []
|
||||||
all_nos: List[StrictStr] = []
|
all_nos: List[StrictStr] = []
|
||||||
|
|
@ -300,15 +299,8 @@ class Devices(HyperglassModelExtra):
|
||||||
Remove unsupported characters from device names, dynamically
|
Remove unsupported characters from device names, dynamically
|
||||||
set attributes for the devices class. Builds lists of common
|
set attributes for the devices class. Builds lists of common
|
||||||
attributes for easy access in other modules.
|
attributes for easy access in other modules.
|
||||||
|
|
||||||
Arguments:
|
|
||||||
input_params {dict} -- Unvalidated router definitions
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
{object} -- Validated routers object
|
|
||||||
"""
|
"""
|
||||||
vrfs = set()
|
vrfs = set()
|
||||||
display_vrfs = set()
|
|
||||||
vrf_objects = set()
|
vrf_objects = set()
|
||||||
all_nos = set()
|
all_nos = set()
|
||||||
objects = set()
|
objects = set()
|
||||||
|
|
@ -328,19 +320,13 @@ class Devices(HyperglassModelExtra):
|
||||||
hostnames.add(device.name)
|
hostnames.add(device.name)
|
||||||
_ids.add(device._id)
|
_ids.add(device._id)
|
||||||
objects.add(device)
|
objects.add(device)
|
||||||
all_nos.add(device.commands)
|
all_nos.add(device.nos)
|
||||||
|
|
||||||
for vrf in device.vrfs:
|
for vrf in device.vrfs:
|
||||||
|
|
||||||
# For each configured router VRF, add its name and
|
# For each configured router VRF, add its name and
|
||||||
# display_name to a class set (for automatic de-duping).
|
# display_name to a class set (for automatic de-duping).
|
||||||
vrfs.add(vrf.name)
|
vrfs.add(vrf.name)
|
||||||
display_vrfs.add(vrf.display_name)
|
|
||||||
|
|
||||||
# Also add the names to a router-level list so each
|
|
||||||
# router's VRFs and display VRFs can be easily accessed.
|
|
||||||
device.display_vrfs.append(vrf.display_name)
|
|
||||||
device.vrf_names.append(vrf.name)
|
|
||||||
|
|
||||||
# Add a 'default_vrf' attribute to the devices class
|
# Add a 'default_vrf' attribute to the devices class
|
||||||
# which contains the configured default VRF display name.
|
# which contains the configured default VRF display name.
|
||||||
|
|
@ -367,7 +353,6 @@ class Devices(HyperglassModelExtra):
|
||||||
init_kwargs["hostnames"] = list(hostnames)
|
init_kwargs["hostnames"] = list(hostnames)
|
||||||
init_kwargs["all_nos"] = list(all_nos)
|
init_kwargs["all_nos"] = list(all_nos)
|
||||||
init_kwargs["vrfs"] = list(vrfs)
|
init_kwargs["vrfs"] = list(vrfs)
|
||||||
init_kwargs["display_vrfs"] = list(vrfs)
|
|
||||||
init_kwargs["vrf_objects"] = list(vrf_objects)
|
init_kwargs["vrf_objects"] = list(vrf_objects)
|
||||||
init_kwargs["objects"] = sorted(objects, key=lambda x: x.name)
|
init_kwargs["objects"] = sorted(objects, key=lambda x: x.name)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,12 @@ interface TViewer extends Pick<UseDisclosureReturn, 'isOpen' | 'onClose'> {
|
||||||
|
|
||||||
const Viewer: React.FC<TViewer> = (props: TViewer) => {
|
const Viewer: React.FC<TViewer> = (props: TViewer) => {
|
||||||
const { title, isOpen, onClose, children } = props;
|
const { title, isOpen, onClose, children } = props;
|
||||||
const bg = useColorValue('white', 'black');
|
const bg = useColorValue('white', 'blackSolid.700');
|
||||||
const color = useColorValue('black', 'white');
|
const color = useColorValue('black', 'white');
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="full" scrollBehavior="inside">
|
<Modal isOpen={isOpen} onClose={onClose} size="full" scrollBehavior="inside">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent bg={bg} color={color} py={4} borderRadius="md" maxW="90%">
|
<ModalContent bg={bg} color={color} py={4} borderRadius="md" maxW="90%" minH="90vh">
|
||||||
<ModalHeader>{title}</ModalHeader>
|
<ModalHeader>{title}</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,11 @@ export const FormField: React.FC<TField> = (props: TField) => {
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl
|
||||||
mx={2}
|
mx={2}
|
||||||
d="flex"
|
|
||||||
w="100%"
|
w="100%"
|
||||||
maxW="100%"
|
maxW="100%"
|
||||||
flexDir="column"
|
flexDir="column"
|
||||||
my={{ base: 2, lg: 4 }}
|
my={{ base: 2, lg: 4 }}
|
||||||
isInvalid={error !== false}
|
isInvalid={error !== false}
|
||||||
flex={{ base: '1 0 100%', lg: '1 0 33.33%' }}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<FormLabel
|
<FormLabel
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
export * from './field';
|
|
||||||
export * from './queryLocation';
|
|
||||||
export * from './queryTarget';
|
|
||||||
export * from './queryType';
|
|
||||||
export * from './queryVrf';
|
|
||||||
export * from './resolvedTarget';
|
|
||||||
export * from './row';
|
export * from './row';
|
||||||
|
export * from './field';
|
||||||
|
export * from './queryVrf';
|
||||||
|
export * from './queryType';
|
||||||
|
export * from './queryGroup';
|
||||||
|
export * from './queryTarget';
|
||||||
|
export * from './queryLocation';
|
||||||
|
export * from './resolvedTarget';
|
||||||
|
|
|
||||||
71
hyperglass/ui/components/form/queryGroup.tsx
Normal file
71
hyperglass/ui/components/form/queryGroup.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Select } from '~/components';
|
||||||
|
import { useLGMethods, useLGState } from '~/hooks';
|
||||||
|
|
||||||
|
import type { TNetwork, TSelectOption } from '~/types';
|
||||||
|
import type { TQueryGroup } from './types';
|
||||||
|
|
||||||
|
// function buildOptions(queryVrfs: TDeviceVrf[]): TSelectOption[] {
|
||||||
|
// return queryVrfs.map(q => ({ value: q._id, label: q.display_name }));
|
||||||
|
// }
|
||||||
|
|
||||||
|
type QueryGroups = Record<string, string[]>;
|
||||||
|
|
||||||
|
function buildOptions(networks: TNetwork[]): QueryGroups {
|
||||||
|
const options = {} as QueryGroups;
|
||||||
|
for (const net of networks) {
|
||||||
|
for (const loc of net.locations) {
|
||||||
|
for (const directive of loc.directives) {
|
||||||
|
for (const group of directive.groups) {
|
||||||
|
if (Object.keys(options).includes(group)) {
|
||||||
|
options[group] = [...options[group], loc.name];
|
||||||
|
} else {
|
||||||
|
options[group] = [loc.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QueryGroup: React.FC<TQueryGroup> = (props: TQueryGroup) => {
|
||||||
|
const { groups, onChange, label } = props;
|
||||||
|
const { selections, availableGroups, queryLocation, queryGroup } = useLGState();
|
||||||
|
const { exportState } = useLGMethods();
|
||||||
|
|
||||||
|
// const groups = useMemo(() => buildOptions(networks), []);
|
||||||
|
// const options = useMemo<TSelectOption[]>(
|
||||||
|
// () => Object.keys(groups).map(key => ({ label: key, value: key })),
|
||||||
|
// [groups],
|
||||||
|
// );
|
||||||
|
// const options = useMemo<TSelectOption[]>(() => groups.map(g => ({ label: g, value: g })), [
|
||||||
|
// groups,
|
||||||
|
// ]);
|
||||||
|
const options = useMemo<TSelectOption[]>(
|
||||||
|
() => availableGroups.map(g => ({ label: g.value, value: g.value })),
|
||||||
|
[availableGroups.length, queryLocation.length],
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleChange(e: TSelectOption | TSelectOption[]): void {
|
||||||
|
let value = '';
|
||||||
|
if (!Array.isArray(e) && e !== null) {
|
||||||
|
selections.queryGroup.set(e);
|
||||||
|
value = e.value;
|
||||||
|
} else {
|
||||||
|
selections.queryGroup.set(null);
|
||||||
|
}
|
||||||
|
onChange({ field: 'query_group', value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
size="lg"
|
||||||
|
name="query_group"
|
||||||
|
options={options}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={exportState(selections.queryGroup.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,34 +1,60 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { uniqBy } from 'lodash';
|
||||||
import { Select } from '~/components';
|
import { Select } from '~/components';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { useLGState, useLGMethods } from '~/hooks';
|
import { useLGState, useLGMethods } from '~/hooks';
|
||||||
|
|
||||||
import type { TQuery, TSelectOption } from '~/types';
|
import type { TNetwork, TSelectOption } from '~/types';
|
||||||
import type { TQuerySelectField } from './types';
|
import type { TQuerySelectField } from './types';
|
||||||
|
|
||||||
function buildOptions(queryTypes: TQuery[]): TSelectOption[] {
|
// function buildOptions(queryTypes: TQuery[]): TSelectOption[] {
|
||||||
return queryTypes
|
// return queryTypes
|
||||||
.filter(q => q.enable === true)
|
// .filter(q => q.enable === true)
|
||||||
.map(q => ({ value: q.name, label: q.display_name }));
|
// .map(q => ({ value: q.name, label: q.display_name }));
|
||||||
|
// }
|
||||||
|
|
||||||
|
function* buildOptions(networks: TNetwork[]): Generator<TSelectOption> {
|
||||||
|
for (const net of networks) {
|
||||||
|
for (const loc of net.locations) {
|
||||||
|
for (const directive of loc.directives) {
|
||||||
|
const { name } = directive;
|
||||||
|
yield { value: name, label: name };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryType: React.FC<TQuerySelectField> = (props: TQuerySelectField) => {
|
export const QueryType: React.FC<TQuerySelectField> = (props: TQuerySelectField) => {
|
||||||
const { onChange, label } = props;
|
const { onChange, label } = props;
|
||||||
const { queries } = useConfig();
|
// const {
|
||||||
|
// queries,
|
||||||
|
// networks,
|
||||||
|
// } = useConfig();
|
||||||
const { errors } = useFormContext();
|
const { errors } = useFormContext();
|
||||||
const { selections } = useLGState();
|
const { selections, availableTypes, queryType } = useLGState();
|
||||||
const { exportState } = useLGMethods();
|
const { exportState } = useLGMethods();
|
||||||
|
|
||||||
const options = useMemo(() => buildOptions(queries.list), [queries.list.length]);
|
// const options = useMemo(() => buildOptions(queries.list), [queries.list.length]);
|
||||||
|
// const options = useMemo(() => Array.from(buildOptions(networks)), []);
|
||||||
|
// const options = useMemo(
|
||||||
|
// () => uniqBy<TSelectOption>(Array.from(buildOptions(networks)), opt => opt?.label),
|
||||||
|
// [],
|
||||||
|
// );
|
||||||
|
const options = useMemo(() => availableTypes.map(t => ({ label: t.value, value: t.value })), [
|
||||||
|
availableTypes.length,
|
||||||
|
]);
|
||||||
|
|
||||||
function handleChange(e: TSelectOption | TSelectOption[]): void {
|
function handleChange(e: TSelectOption | TSelectOption[]): void {
|
||||||
|
let value = '';
|
||||||
if (!Array.isArray(e) && e !== null) {
|
if (!Array.isArray(e) && e !== null) {
|
||||||
selections.queryType.set(e);
|
selections.queryType.set(e);
|
||||||
onChange({ field: 'query_type', value: e.value });
|
value = e.value;
|
||||||
} else {
|
} else {
|
||||||
selections.queryType.set(null);
|
selections.queryType.set(null);
|
||||||
|
queryType.set('');
|
||||||
}
|
}
|
||||||
|
onChange({ field: 'query_type', value });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export const FormRow: React.FC<FlexProps> = (props: FlexProps) => {
|
||||||
flexDir="row"
|
flexDir="row"
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
justifyContent={{ base: 'center', lg: 'space-between' }}
|
justifyContent={{ base: 'center', lg: 'space-between' }}
|
||||||
|
sx={{ '& > *': { display: 'flex', flex: { base: '1 0 100%', lg: '1 0 33.33%' } } }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ export interface TQueryVrf extends TQuerySelectField {
|
||||||
vrfs: TDeviceVrf[];
|
vrfs: TDeviceVrf[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TQueryGroup extends TQuerySelectField {
|
||||||
|
groups: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface TCommunitySelect {
|
export interface TCommunitySelect {
|
||||||
name: string;
|
name: string;
|
||||||
onChange: OnChange;
|
onChange: OnChange;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex, ScaleFade, SlideFade } from '@chakra-ui/react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { intersectionWith } from 'lodash';
|
import { intersectionWith } from 'lodash';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
import { vestResolver } from '@hookform/resolvers/vest';
|
import { vestResolver } from '@hookform/resolvers/vest';
|
||||||
import vest, { test, enforce } from 'vest';
|
import vest, { test, enforce } from 'vest';
|
||||||
import {
|
import {
|
||||||
If,
|
If,
|
||||||
FormRow,
|
FormRow,
|
||||||
QueryVrf,
|
QueryGroup,
|
||||||
FormField,
|
FormField,
|
||||||
HelpModal,
|
HelpModal,
|
||||||
QueryType,
|
QueryType,
|
||||||
|
|
@ -18,7 +19,7 @@ import {
|
||||||
} from '~/components';
|
} from '~/components';
|
||||||
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 { isQueryType, isQueryContent, isString } from '~/types';
|
import { isString } from '~/types';
|
||||||
|
|
||||||
import type { TFormData, TDeviceVrf, OnChangeArgs } from '~/types';
|
import type { TFormData, TDeviceVrf, OnChangeArgs } from '~/types';
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ function useIsFqdn(target: string, _type: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LookingGlass: React.FC = () => {
|
export const LookingGlass: React.FC = () => {
|
||||||
const { web, content, messages } = useConfig();
|
const { web, messages } = useConfig();
|
||||||
|
|
||||||
const { ack, greetingReady } = useGreeting();
|
const { ack, greetingReady } = useGreeting();
|
||||||
const getDevice = useDevice();
|
const getDevice = useDevice();
|
||||||
|
|
@ -80,21 +81,39 @@ export const LookingGlass: React.FC = () => {
|
||||||
const { handleSubmit, register, setValue, setError, clearErrors } = formInstance;
|
const { handleSubmit, register, setValue, setError, clearErrors } = formInstance;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
availableGroups,
|
||||||
queryVrf,
|
queryVrf,
|
||||||
families,
|
|
||||||
queryType,
|
queryType,
|
||||||
availVrfs,
|
directive,
|
||||||
|
availableTypes,
|
||||||
btnLoading,
|
btnLoading,
|
||||||
|
queryGroup,
|
||||||
queryTarget,
|
queryTarget,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
queryLocation,
|
queryLocation,
|
||||||
displayTarget,
|
displayTarget,
|
||||||
|
selections,
|
||||||
} = useLGState();
|
} = useLGState();
|
||||||
|
|
||||||
const { resolvedOpen, resetForm } = useLGMethods();
|
const { resolvedOpen, resetForm } = useLGMethods();
|
||||||
|
|
||||||
const isFqdnQuery = useIsFqdn(queryTarget.value, queryType.value);
|
const isFqdnQuery = useIsFqdn(queryTarget.value, queryType.value);
|
||||||
|
|
||||||
|
const selectedDirective = useMemo(() => {
|
||||||
|
if (queryType.value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (const loc of queryLocation) {
|
||||||
|
const device = getDevice(loc.value);
|
||||||
|
for (const directive of device.directives) {
|
||||||
|
if (directive.name === queryType.value) {
|
||||||
|
return directive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [queryType.value]);
|
||||||
|
|
||||||
function submitHandler() {
|
function submitHandler() {
|
||||||
/**
|
/**
|
||||||
* Before submitting a query, make sure the greeting is acknowledged if required. This should
|
* Before submitting a query, make sure the greeting is acknowledged if required. This should
|
||||||
|
|
@ -133,6 +152,9 @@ export const LookingGlass: React.FC = () => {
|
||||||
clearErrors('query_location');
|
clearErrors('query_location');
|
||||||
const allVrfs = [] as TDeviceVrf[][];
|
const allVrfs = [] as TDeviceVrf[][];
|
||||||
const locationNames = [] as string[];
|
const locationNames = [] as string[];
|
||||||
|
const allGroups = [] as string[][];
|
||||||
|
const allTypes = [] as string[][];
|
||||||
|
const allDevices = [];
|
||||||
|
|
||||||
queryLocation.set(locations);
|
queryLocation.set(locations);
|
||||||
|
|
||||||
|
|
@ -141,66 +163,76 @@ export const LookingGlass: React.FC = () => {
|
||||||
const device = getDevice(loc);
|
const device = getDevice(loc);
|
||||||
locationNames.push(device.name);
|
locationNames.push(device.name);
|
||||||
allVrfs.push(device.vrfs);
|
allVrfs.push(device.vrfs);
|
||||||
|
allDevices.push(device);
|
||||||
|
const groups = new Set<string>();
|
||||||
|
for (const directive of device.directives) {
|
||||||
|
for (const group of directive.groups) {
|
||||||
|
groups.add(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allGroups.push(Array.from(groups));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use _.intersectionWith to create an array of VRFs common to all selected locations.
|
const intersecting = intersectionWith(...allGroups, isEqual);
|
||||||
const intersecting = intersectionWith(
|
|
||||||
...allVrfs,
|
|
||||||
(a: TDeviceVrf, b: TDeviceVrf) => a._id === b._id,
|
|
||||||
);
|
|
||||||
|
|
||||||
availVrfs.set(intersecting);
|
if (!intersecting.includes(queryGroup.value)) {
|
||||||
|
queryGroup.set('');
|
||||||
|
queryType.set('');
|
||||||
|
directive.set(null);
|
||||||
|
selections.merge({ queryGroup: null, queryType: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const group of intersecting) {
|
||||||
|
for (const device of allDevices) {
|
||||||
|
for (const directive of device.directives) {
|
||||||
|
if (directive.groups.includes(group)) {
|
||||||
|
// allTypes.add(directive.name);
|
||||||
|
allTypes.push(device.directives.map(d => d.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const intersectingTypes = intersectionWith(...allTypes, isEqual);
|
||||||
|
|
||||||
|
availableGroups.set(intersecting);
|
||||||
|
availableTypes.set(intersectingTypes);
|
||||||
|
|
||||||
// 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('query_location', {
|
||||||
message: `${locationNames.join(', ')} have no VRFs in common.`,
|
// message: `${locationNames.join(', ')} have no VRFs 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);
|
// queryVrf.set(intersecting[0]._id);
|
||||||
|
queryGroup.set(intersecting[0]);
|
||||||
}
|
}
|
||||||
|
if (availableGroups.length > 1 && intersectingTypes.length === 0) {
|
||||||
|
setError('query_location', {
|
||||||
|
message: `${locationNames.join(', ')} have no query types in common.`,
|
||||||
|
});
|
||||||
|
} else if (intersectingTypes.length === 1) {
|
||||||
|
queryType.set(intersectingTypes[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine which address families are available in the intersecting VRFs.
|
function handleGroupChange(group: string): void {
|
||||||
let ipv4 = 0;
|
queryGroup.set(group);
|
||||||
let ipv6 = 0;
|
const availTypes = new Set<string>();
|
||||||
|
for (const loc of queryLocation) {
|
||||||
for (const intersection of intersecting) {
|
const device = getDevice(loc.value);
|
||||||
if (intersection.ipv4) {
|
for (const directive of device.directives) {
|
||||||
// If IPv4 is enabled in this VRF, count it.
|
if (directive.groups.includes(group)) {
|
||||||
ipv4++;
|
availTypes.add(directive.name);
|
||||||
}
|
}
|
||||||
if (intersection.ipv6) {
|
|
||||||
// If IPv6 is enabled in this VRF, count it.
|
|
||||||
ipv6++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
availableTypes.set(Array.from(availTypes));
|
||||||
if (ipv4 !== 0 && ipv4 === ipv6) {
|
if (availableTypes.length === 1) {
|
||||||
/**
|
queryType.set(availableTypes[0].value);
|
||||||
* If ipv4 & ipv6 are equal, this means every VRF has both IPv4 & IPv6 enabled. In that
|
|
||||||
* case, signal that both A & AAAA records should be queried if the query is an FQDN.
|
|
||||||
*/
|
|
||||||
families.set([4, 6]);
|
|
||||||
} else if (ipv4 > ipv6) {
|
|
||||||
/**
|
|
||||||
* If ipv4 is greater than ipv6, this means that IPv6 is not enabled on all VRFs, i.e. there
|
|
||||||
* are some VRFs with IPv4 enabled but IPv6 disabled. In that case, only query A records.
|
|
||||||
*/
|
|
||||||
families.set([4]);
|
|
||||||
} else if (ipv4 < ipv6) {
|
|
||||||
/**
|
|
||||||
* If ipv6 is greater than ipv4, this means that IPv4 is not enabled on all VRFs, i.e. there
|
|
||||||
* are some VRFs with IPv6 enabled but IPv4 disabled. In that case, only query AAAA records.
|
|
||||||
*/
|
|
||||||
families.set([6]);
|
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
* If both ipv4 and ipv6 are 0, then both ipv4 and ipv6 are disabled, and why does that VRF
|
|
||||||
* even exist?
|
|
||||||
*/
|
|
||||||
families.set([]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,14 +242,12 @@ export const LookingGlass: React.FC = () => {
|
||||||
|
|
||||||
if (e.field === 'query_location' && Array.isArray(e.value)) {
|
if (e.field === 'query_location' && Array.isArray(e.value)) {
|
||||||
handleLocChange(e.value);
|
handleLocChange(e.value);
|
||||||
} else if (e.field === 'query_type' && isQueryType(e.value)) {
|
} else if (e.field === 'query_type' && 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
|
// a community, then changing the queryType to BGP Route doesn't preserve the selected
|
||||||
* a community, then changing the queryType to BGP Route doesn't preserve the selected
|
// community as the queryTarget.
|
||||||
* community as the queryTarget.
|
|
||||||
*/
|
|
||||||
queryTarget.set('');
|
queryTarget.set('');
|
||||||
displayTarget.set('');
|
displayTarget.set('');
|
||||||
}
|
}
|
||||||
|
|
@ -225,25 +255,21 @@ export const LookingGlass: React.FC = () => {
|
||||||
queryVrf.set(e.value);
|
queryVrf.set(e.value);
|
||||||
} else if (e.field === 'query_target' && isString(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)) {
|
||||||
|
// queryGroup.set(e.value);
|
||||||
|
handleGroupChange(e.value);
|
||||||
}
|
}
|
||||||
|
console.table({
|
||||||
|
'Query Location': queryLocation.value,
|
||||||
|
'Query Type': queryType.value,
|
||||||
|
'Query Group': queryGroup.value,
|
||||||
|
'Query Target': queryTarget.value,
|
||||||
|
'Selected Directive': selectedDirective?.name ?? null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Select the correct help content based on the selected VRF & Query Type. Also remove the icon
|
|
||||||
* if no locations are set.
|
|
||||||
*/
|
|
||||||
const vrfContent = useMemo(() => {
|
|
||||||
if (queryLocation.value.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (Object.keys(content.vrf).includes(queryVrf.value) && queryType.value !== '') {
|
|
||||||
return content.vrf[queryVrf.value][queryType.value];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, [queryVrf.value, queryLocation.value, queryType.value]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
register({ name: 'query_group', required: true });
|
||||||
register({ name: 'query_location', required: true });
|
register({ name: 'query_location', required: true });
|
||||||
register({ name: 'query_target', required: true });
|
register({ name: 'query_target', required: true });
|
||||||
register({ name: 'query_type', required: true });
|
register({ name: 'query_type', required: true });
|
||||||
|
|
@ -270,30 +296,44 @@ export const LookingGlass: React.FC = () => {
|
||||||
<FormField name="query_location" label={web.text.query_location}>
|
<FormField name="query_location" label={web.text.query_location}>
|
||||||
<QueryLocation onChange={handleChange} label={web.text.query_location} />
|
<QueryLocation onChange={handleChange} label={web.text.query_location} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField
|
<If c={availableGroups.length > 1}>
|
||||||
name="query_type"
|
<FormField label={web.text.query_vrf} name="query_group">
|
||||||
label={web.text.query_type}
|
<QueryGroup
|
||||||
labelAddOn={
|
label={web.text.query_vrf}
|
||||||
<HelpModal visible={isQueryContent(vrfContent)} item={vrfContent} name="query_type" />
|
groups={availableGroups.value}
|
||||||
}
|
onChange={handleChange}
|
||||||
>
|
/>
|
||||||
<QueryType onChange={handleChange} label={web.text.query_type} />
|
|
||||||
</FormField>
|
|
||||||
</FormRow>
|
|
||||||
<FormRow>
|
|
||||||
<If c={availVrfs.length > 1}>
|
|
||||||
<FormField label={web.text.query_vrf} name="query_vrf">
|
|
||||||
<QueryVrf label={web.text.query_vrf} vrfs={availVrfs.value} onChange={handleChange} />
|
|
||||||
</FormField>
|
</FormField>
|
||||||
</If>
|
</If>
|
||||||
<FormField name="query_target" label={web.text.query_target}>
|
</FormRow>
|
||||||
<QueryTarget
|
<FormRow>
|
||||||
name="query_target"
|
<SlideFade offsetX={-100} in={availableTypes.length > 1} unmountOnExit>
|
||||||
register={register}
|
<FormField
|
||||||
onChange={handleChange}
|
name="query_type"
|
||||||
placeholder={web.text.query_target}
|
label={web.text.query_type}
|
||||||
/>
|
labelAddOn={
|
||||||
</FormField>
|
<HelpModal
|
||||||
|
visible={selectedDirective?.info !== null}
|
||||||
|
item={selectedDirective?.info ?? null}
|
||||||
|
name="query_type"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<QueryType onChange={handleChange} label={web.text.query_type} />
|
||||||
|
</FormField>
|
||||||
|
</SlideFade>
|
||||||
|
<SlideFade offsetX={100} in={selectedDirective !== null} unmountOnExit>
|
||||||
|
{selectedDirective !== null && (
|
||||||
|
<FormField name="query_target" label={web.text.query_target}>
|
||||||
|
<QueryTarget
|
||||||
|
name="query_target"
|
||||||
|
register={register}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={selectedDirective.description}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
</SlideFade>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow mt={0} justifyContent="flex-end">
|
<FormRow mt={0} justifyContent="flex-end">
|
||||||
<Flex
|
<Flex
|
||||||
|
|
@ -305,7 +345,9 @@ export const LookingGlass: React.FC = () => {
|
||||||
flexDir="column"
|
flexDir="column"
|
||||||
mr={{ base: 0, lg: 2 }}
|
mr={{ base: 0, lg: 2 }}
|
||||||
>
|
>
|
||||||
<SubmitButton handleChange={handleChange} />
|
<ScaleFade initialScale={0.5} in={queryTarget.value !== ''}>
|
||||||
|
<SubmitButton handleChange={handleChange} />
|
||||||
|
</ScaleFade>
|
||||||
</Flex>
|
</Flex>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</AnimatedDiv>
|
</AnimatedDiv>
|
||||||
|
|
|
||||||
|
|
@ -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 } = useLGState();
|
const { queryLocation, queryTarget, queryType, queryVrf, queryGroup } = useLGState();
|
||||||
|
|
||||||
const getDevice = useDevice();
|
const getDevice = useDevice();
|
||||||
|
|
||||||
|
|
@ -49,6 +49,7 @@ export const Results: React.FC = () => {
|
||||||
queryLocation={loc.value}
|
queryLocation={loc.value}
|
||||||
queryVrf={queryVrf.value}
|
queryVrf={queryVrf.value}
|
||||||
queryType={queryType.value}
|
queryType={queryType.value}
|
||||||
|
queryGroup={queryGroup.value}
|
||||||
queryTarget={queryTarget.value}
|
queryTarget={queryTarget.value}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 } = props;
|
const { index, device, queryVrf, 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();
|
||||||
|
|
@ -57,6 +57,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||||
queryTarget,
|
queryTarget,
|
||||||
queryType,
|
queryType,
|
||||||
queryVrf,
|
queryVrf,
|
||||||
|
queryGroup,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [
|
const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [
|
||||||
|
|
|
||||||
|
|
@ -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, TQueryTypes } from '~/types';
|
import type { TDevice } from '~/types';
|
||||||
|
|
||||||
export interface TResultHeader {
|
export interface TResultHeader {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -21,9 +21,10 @@ export interface TResult {
|
||||||
index: number;
|
index: number;
|
||||||
device: TDevice;
|
device: TDevice;
|
||||||
queryVrf: string;
|
queryVrf: string;
|
||||||
|
queryGroup: string;
|
||||||
queryTarget: string;
|
queryTarget: string;
|
||||||
queryLocation: string;
|
queryLocation: string;
|
||||||
queryType: TQueryTypes;
|
queryType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TErrorLevels = 'success' | 'warning' | 'error';
|
export type TErrorLevels = 'success' | 'warning' | 'error';
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import type {
|
||||||
TDeviceVrf,
|
TDeviceVrf,
|
||||||
TQueryTypes,
|
TQueryTypes,
|
||||||
TSelectOption,
|
TSelectOption,
|
||||||
|
TDirective,
|
||||||
} from '~/types';
|
} from '~/types';
|
||||||
|
|
||||||
export interface TOpposingOptions {
|
export interface TOpposingOptions {
|
||||||
|
|
@ -56,6 +57,7 @@ export interface TSelections {
|
||||||
queryLocation: TSelectOption[] | [];
|
queryLocation: TSelectOption[] | [];
|
||||||
queryType: TSelectOption | null;
|
queryType: TSelectOption | null;
|
||||||
queryVrf: TSelectOption | null;
|
queryVrf: TSelectOption | null;
|
||||||
|
queryGroup: TSelectOption | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TMethodsExtension {
|
export interface TMethodsExtension {
|
||||||
|
|
@ -69,14 +71,19 @@ export interface TMethodsExtension {
|
||||||
|
|
||||||
export type TLGState = {
|
export type TLGState = {
|
||||||
queryVrf: string;
|
queryVrf: string;
|
||||||
|
queryGroup: string;
|
||||||
families: Families;
|
families: Families;
|
||||||
queryTarget: string;
|
queryTarget: string;
|
||||||
btnLoading: boolean;
|
btnLoading: boolean;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
displayTarget: string;
|
displayTarget: string;
|
||||||
queryType: TQueryTypes;
|
directive: TDirective | null;
|
||||||
|
// queryType: TQueryTypes;
|
||||||
|
queryType: string;
|
||||||
queryLocation: string[];
|
queryLocation: string[];
|
||||||
availVrfs: TDeviceVrf[];
|
availVrfs: TDeviceVrf[];
|
||||||
|
availableGroups: string[];
|
||||||
|
availableTypes: string[];
|
||||||
resolvedIsOpen: boolean;
|
resolvedIsOpen: boolean;
|
||||||
selections: TSelections;
|
selections: TSelections;
|
||||||
responses: { [d: string]: TQueryResponse };
|
responses: { [d: string]: TQueryResponse };
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,14 @@ class MethodsInstance {
|
||||||
public resolvedOpen(state: State<TLGState>) {
|
public resolvedOpen(state: State<TLGState>) {
|
||||||
state.resolvedIsOpen.set(true);
|
state.resolvedIsOpen.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the DNS resolver Popover to closed.
|
* Set the DNS resolver Popover to closed.
|
||||||
*/
|
*/
|
||||||
public resolvedClose(state: State<TLGState>) {
|
public resolvedClose(state: State<TLGState>) {
|
||||||
state.resolvedIsOpen.set(false);
|
state.resolvedIsOpen.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a response based on the device ID.
|
* Find a response based on the device ID.
|
||||||
*/
|
*/
|
||||||
|
|
@ -34,6 +36,7 @@ class MethodsInstance {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the form is ready for submission, e.g. all fields have values and isSubmitting
|
* Determine if the form is ready for submission, e.g. all fields have values and isSubmitting
|
||||||
* has been set to true. This ultimately controls the UI layout.
|
* has been set to true. This ultimately controls the UI layout.
|
||||||
|
|
@ -45,12 +48,14 @@ class MethodsInstance {
|
||||||
...[
|
...[
|
||||||
state.queryVrf.value !== '',
|
state.queryVrf.value !== '',
|
||||||
state.queryType.value !== '',
|
state.queryType.value !== '',
|
||||||
|
state.queryGroup.value !== '',
|
||||||
state.queryTarget.value !== '',
|
state.queryTarget.value !== '',
|
||||||
state.queryLocation.length !== 0,
|
state.queryLocation.length !== 0,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset form values affected by the form state to their default values.
|
* Reset form values affected by the form state to their default values.
|
||||||
*/
|
*/
|
||||||
|
|
@ -59,6 +64,7 @@ class MethodsInstance {
|
||||||
queryVrf: '',
|
queryVrf: '',
|
||||||
families: [],
|
families: [],
|
||||||
queryType: '',
|
queryType: '',
|
||||||
|
queryGroup: '',
|
||||||
responses: {},
|
responses: {},
|
||||||
queryTarget: '',
|
queryTarget: '',
|
||||||
queryLocation: [],
|
queryLocation: [],
|
||||||
|
|
@ -67,9 +73,12 @@ class MethodsInstance {
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
resolvedIsOpen: false,
|
resolvedIsOpen: false,
|
||||||
availVrfs: [],
|
availVrfs: [],
|
||||||
selections: { queryLocation: [], queryType: null, queryVrf: null },
|
availableGroups: [],
|
||||||
|
availableTypes: [],
|
||||||
|
selections: { queryLocation: [], queryType: null, queryVrf: null, queryGroup: null },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public stateExporter<O extends unknown>(obj: O): O | null {
|
public stateExporter<O extends unknown>(obj: O): O | null {
|
||||||
let result = null;
|
let result = null;
|
||||||
if (obj === null) {
|
if (obj === null) {
|
||||||
|
|
@ -125,13 +134,17 @@ function Methods(inst?: State<TLGState>): Plugin | TMethodsExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
const LGState = createState<TLGState>({
|
const LGState = createState<TLGState>({
|
||||||
selections: { queryLocation: [], queryType: null, queryVrf: null },
|
selections: { queryLocation: [], queryType: null, queryVrf: null, queryGroup: null },
|
||||||
resolvedIsOpen: false,
|
resolvedIsOpen: false,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
|
availableGroups: [],
|
||||||
|
availableTypes: [],
|
||||||
|
directive: null,
|
||||||
displayTarget: '',
|
displayTarget: '',
|
||||||
queryLocation: [],
|
queryLocation: [],
|
||||||
btnLoading: false,
|
btnLoading: false,
|
||||||
queryTarget: '',
|
queryTarget: '',
|
||||||
|
queryGroup: '',
|
||||||
queryType: '',
|
queryType: '',
|
||||||
availVrfs: [],
|
availVrfs: [],
|
||||||
responses: {},
|
responses: {},
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,7 @@ module.exports = {
|
||||||
future: {
|
future: {
|
||||||
webpack5: true,
|
webpack5: true,
|
||||||
},
|
},
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -115,10 +115,30 @@ export interface TDeviceVrf extends TDeviceVrfBase {
|
||||||
ipv6: boolean;
|
ipv6: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TDirectiveBase {
|
||||||
|
name: string;
|
||||||
|
field_type: 'text' | 'select' | null;
|
||||||
|
description: string;
|
||||||
|
groups: string[];
|
||||||
|
info: TQueryContent | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TDirectiveOption {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TDirectiveSelect extends TDirectiveBase {
|
||||||
|
options: TDirectiveOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TDirective = TDirectiveBase | TDirectiveSelect;
|
||||||
|
|
||||||
interface TDeviceBase {
|
interface TDeviceBase {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
network: string;
|
network: string;
|
||||||
|
directives: TDirective[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TDevice extends TDeviceBase {
|
export interface TDevice extends TDeviceBase {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export interface TFormData {
|
||||||
query_type: TQueryTypes;
|
query_type: TQueryTypes;
|
||||||
query_vrf: string;
|
query_vrf: string;
|
||||||
query_target: string;
|
query_target: string;
|
||||||
|
query_group: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TFormState {
|
export interface TFormState {
|
||||||
|
|
@ -13,6 +14,7 @@ export interface TFormState {
|
||||||
queryType: TQueryTypes;
|
queryType: TQueryTypes;
|
||||||
queryVrf: string;
|
queryVrf: string;
|
||||||
queryTarget: string;
|
queryTarget: string;
|
||||||
|
queryGroup: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TFormQuery extends Omit<TFormState, 'queryLocation'> {
|
export interface TFormQuery extends Omit<TFormState, 'queryLocation'> {
|
||||||
|
|
|
||||||
2
poetry.lock
generated
2
poetry.lock
generated
|
|
@ -1407,7 +1407,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = ">=3.6.1,<4.0"
|
python-versions = ">=3.6.1,<4.0"
|
||||||
content-hash = "3e44c4a83c82c220179b98b16f7b27e524c4461903bfa6f81a25781ee1514166"
|
content-hash = "39564830e6fe6f4ba7253c516dd9d0dc0089e60512cd0c94ae798a4464be4505"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiocontextvars = [
|
aiocontextvars = [
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ scrapli = {extras = ["asyncssh"], version = "^2021.1.30"}
|
||||||
uvicorn = {extras = ["standard"], version = "^0.13.4"}
|
uvicorn = {extras = ["standard"], version = "^0.13.4"}
|
||||||
uvloop = "^0.14.0"
|
uvloop = "^0.14.0"
|
||||||
xmltodict = "^0.12.0"
|
xmltodict = "^0.12.0"
|
||||||
|
typing-extensions = "^3.7.4"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
bandit = "^1.6.2"
|
bandit = "^1.6.2"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue