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