Implement UI configuration response model

This commit is contained in:
thatmattlove 2021-09-10 23:13:27 -07:00
parent 0e6c5e02ad
commit 76bf5eb380
65 changed files with 422 additions and 478 deletions

View file

@ -141,7 +141,7 @@ set_log_level(logger=log, debug=user_config.get("debug", True))
# Map imported user configuration to expected schema. # Map imported user configuration to expected schema.
log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config) log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config)
params: Params = validate_config(config=user_config, importer=Params) params = validate_config(config=user_config, importer=Params)
# Re-evaluate debug state after config is validated # Re-evaluate debug state after config is validated
log_level = current_log_level(log) log_level = current_log_level(log)

View file

@ -3,38 +3,20 @@
Some validations need to occur across multiple config files. Some validations need to occur across multiple config files.
""" """
# Standard Library # Standard Library
from typing import Dict, List, Union, Callable from typing import Any, Dict, List, Union, TypeVar
# Third Party # Third Party
from pydantic import ValidationError from pydantic import ValidationError
# Project # Project
from hyperglass.models import HyperglassModel from hyperglass.exceptions.private import ConfigInvalid
from hyperglass.constants import TRANSPORT_REST, SUPPORTED_STRUCTURED_OUTPUT
from hyperglass.models.commands import Commands Importer = TypeVar("Importer")
from hyperglass.exceptions.private import ConfigError, ConfigInvalid
def validate_nos_commands(all_nos: List[str], commands: Commands) -> bool: def validate_config(
"""Ensure defined devices have associated commands.""" config: Union[Dict[str, Any], List[Any]], importer: Importer
custom_commands = commands.dict().keys() ) -> Importer:
for nos in all_nos:
valid = False
if nos in (*SUPPORTED_STRUCTURED_OUTPUT, *TRANSPORT_REST, *custom_commands):
valid = True
if not valid:
raise ConfigError(
'"{nos}" is used on a device, '
+ 'but no command profile for "{nos}" is defined.',
nos=nos,
)
return True
def validate_config(config: Union[Dict, List], importer: Callable) -> HyperglassModel:
"""Validate a config dict against a model.""" """Validate a config dict against a model."""
validated = None validated = None
try: try:

View file

@ -1,4 +1,6 @@
"""All Data Models used by hyperglass.""" """All Data Models used by hyperglass."""
# Local # Local
from .main import HyperglassModel, HyperglassModelExtra from .main import HyperglassModel
__all__ = ("HyperglassModel",)

View file

@ -5,7 +5,7 @@ from .frr import FRRCommands
from .bird import BIRDCommands from .bird import BIRDCommands
from .tnsr import TNSRCommands from .tnsr import TNSRCommands
from .vyos import VyosCommands from .vyos import VyosCommands
from ..main import HyperglassModelExtra from ..main import HyperglassModel
from .common import CommandGroup from .common import CommandGroup
from .huawei import HuaweiCommands from .huawei import HuaweiCommands
from .juniper import JuniperCommands from .juniper import JuniperCommands
@ -34,7 +34,7 @@ _NOS_MAP = {
} }
class Commands(HyperglassModelExtra): class Commands(HyperglassModel, extra="allow", validate_all=False):
"""Base class for command definitions.""" """Base class for command definitions."""
arista_eos: CommandGroup = AristaEOSCommands() arista_eos: CommandGroup = AristaEOSCommands()
@ -69,8 +69,3 @@ class Commands(HyperglassModelExtra):
nos_cmds = nos_cmd_set(**cmds) nos_cmds = nos_cmd_set(**cmds)
setattr(obj, nos, nos_cmds) setattr(obj, nos, nos_cmds)
return obj return obj
class Config:
"""Override pydantic config."""
validate_all = False

View file

@ -4,7 +4,7 @@
from pydantic import StrictStr from pydantic import StrictStr
# Local # Local
from ..main import HyperglassModel, HyperglassModelExtra from ..main import HyperglassModel
class CommandSet(HyperglassModel): class CommandSet(HyperglassModel):
@ -17,7 +17,7 @@ class CommandSet(HyperglassModel):
traceroute: StrictStr traceroute: StrictStr
class CommandGroup(HyperglassModelExtra): class CommandGroup(HyperglassModel, extra="allow"):
"""Validation model for all commands.""" """Validation model for all commands."""
ipv4_default: CommandSet ipv4_default: CommandSet

View file

@ -7,12 +7,12 @@ from typing import Optional
from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator
# Local # Local
from ..main import HyperglassModelExtra from ..main import HyperglassModel
Methods = constr(regex=r"(password|unencrypted_key|encrypted_key)") Methods = constr(regex=r"(password|unencrypted_key|encrypted_key)")
class Credential(HyperglassModelExtra): class Credential(HyperglassModel, extra="allow"):
"""Model for per-credential config in devices.yaml.""" """Model for per-credential config in devices.yaml."""
username: StrictStr username: StrictStr

View file

@ -26,7 +26,7 @@ from hyperglass.models.commands.generic import Directive
# Local # Local
from .ssl import Ssl from .ssl import Ssl
from ..main import HyperglassModel, HyperglassModelExtra from ..main import HyperglassModel
from .proxy import Proxy from .proxy import Proxy
from .params import Params from .params import Params
from ..fields import SupportedDriver from ..fields import SupportedDriver
@ -34,7 +34,7 @@ from .network import Network
from .credential import Credential from .credential import Credential
class Device(HyperglassModelExtra): class Device(HyperglassModel, extra="allow"):
"""Validation model for per-router config in devices.yaml.""" """Validation model for per-router config in devices.yaml."""
_id: StrictStr = PrivateAttr() _id: StrictStr = PrivateAttr()
@ -222,7 +222,7 @@ class Device(HyperglassModelExtra):
return get_driver(values["nos"], value) return get_driver(values["nos"], value)
class Devices(HyperglassModelExtra): class Devices(HyperglassModel, extra="allow"):
"""Validation model for device configurations.""" """Validation model for device configurations."""
_ids: List[StrictStr] = [] _ids: List[StrictStr] = []
@ -290,7 +290,7 @@ class Devices(HyperglassModelExtra):
"directives": [c.frontend(params) for c in device.commands], "directives": [c.frontend(params) for c in device.commands],
} }
for device in self.objects for device in self.objects
if device.network.display_name in names if device.network.display_name == name
], ],
} }
for name in names for name in names

View file

@ -24,7 +24,7 @@ from pydantic import (
from hyperglass.constants import __version__ from hyperglass.constants import __version__
# Local # Local
from ..main import HyperglassModel, HyperglassModelExtra from ..main import HyperglassModel
HttpAuthMode = constr(regex=r"(basic|api_key)") HttpAuthMode = constr(regex=r"(basic|api_key)")
HttpProvider = constr(regex=r"(msteams|slack|generic)") HttpProvider = constr(regex=r"(msteams|slack|generic)")
@ -55,7 +55,7 @@ class HttpAuth(HyperglassModel):
return (self.username, self.password.get_secret_value()) return (self.username, self.password.get_secret_value())
class Http(HyperglassModelExtra): class Http(HyperglassModel, extra="allow"):
"""HTTP logging parameters.""" """HTTP logging parameters."""
enable: StrictBool = True enable: StrictBool = True

View file

@ -191,7 +191,7 @@ class Params(ParamsPublic, HyperglassModel):
def frontend(self) -> Dict[str, Any]: def frontend(self) -> Dict[str, Any]:
"""Export UI-specific parameters.""" """Export UI-specific parameters."""
return self.dict( return self.export_dict(
include={ include={
"cache": {"show_text", "timeout"}, "cache": {"show_text", "timeout"},
"debug": ..., "debug": ...,

View file

@ -22,7 +22,7 @@ from pydantic import (
from hyperglass.log import log from hyperglass.log import log
# Local # Local
from ..main import HyperglassModel, HyperglassModelExtra from ..main import HyperglassModel
ACLAction = constr(regex=r"permit|deny") ACLAction = constr(regex=r"permit|deny")
AddressFamily = Union[Literal[4], Literal[6]] AddressFamily = Union[Literal[4], Literal[6]]
@ -125,7 +125,7 @@ class AccessList6(HyperglassModel):
return value return value
class InfoConfigParams(HyperglassModelExtra): class InfoConfigParams(HyperglassModel, extra="allow"):
"""Validation model for per-help params.""" """Validation model for per-help params."""
title: Optional[StrictStr] title: Optional[StrictStr]
@ -197,7 +197,7 @@ class Info(HyperglassModel):
} }
class DeviceVrf4(HyperglassModelExtra): class DeviceVrf4(HyperglassModel, extra="allow"):
"""Validation model for IPv4 AFI definitions.""" """Validation model for IPv4 AFI definitions."""
source_address: IPv4Address source_address: IPv4Address
@ -205,7 +205,7 @@ class DeviceVrf4(HyperglassModelExtra):
force_cidr: StrictBool = True force_cidr: StrictBool = True
class DeviceVrf6(HyperglassModelExtra): class DeviceVrf6(HyperglassModel, extra="allow"):
"""Validation model for IPv6 AFI definitions.""" """Validation model for IPv6 AFI definitions."""
source_address: IPv6Address source_address: IPv6Address

View file

@ -2,51 +2,55 @@
# Standard Library # Standard Library
import re import re
from typing import Type, TypeVar from pathlib import Path
# Third Party # Third Party
from pydantic import HttpUrl, BaseModel from pydantic import HttpUrl, BaseModel, BaseConfig
# Project # Project
from hyperglass.log import log
from hyperglass.util import snake_to_camel from hyperglass.util import snake_to_camel
def clean_name(_name: str) -> str:
"""Remove unsupported characters from field names.
Converts any "desirable" seperators to underscore, then removes all
characters that are unsupported in Python class variable names.
Also removes leading numbers underscores.
"""
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", _name)
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
return _scrubbed.lower()
AsUIModel = TypeVar("AsUIModel", bound="BaseModel")
class HyperglassModel(BaseModel): class HyperglassModel(BaseModel):
"""Base model for all hyperglass configuration models.""" """Base model for all hyperglass configuration models."""
class Config: class Config(BaseConfig):
"""Pydantic model configuration. """Pydantic model configuration."""
See https://pydantic-docs.helpmanual.io/usage/model_config
"""
validate_all = True validate_all = True
extra = "forbid" extra = "forbid"
validate_assignment = True validate_assignment = True
alias_generator = clean_name allow_population_by_field_name = True
json_encoders = {HttpUrl: lambda v: str(v)} json_encoders = {HttpUrl: lambda v: str(v), Path: str}
@classmethod
def alias_generator(cls: "HyperglassModel", field: str) -> str:
"""Remove unsupported characters from field names.
Converts any "desirable" seperators to underscore, then removes all
characters that are unsupported in Python class variable names.
Also removes leading numbers underscores.
"""
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", field)
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
snake_field = _scrubbed.lower()
if snake_field != field:
log.debug(
"Model field '{}.{}' was converted from {} to {}",
cls.__module__,
snake_field,
repr(field),
repr(snake_field),
)
return snake_to_camel(snake_field)
def export_json(self, *args, **kwargs): def export_json(self, *args, **kwargs):
"""Return instance as JSON.""" """Return instance as JSON."""
export_kwargs = {"by_alias": True, "exclude_unset": False} export_kwargs = {"by_alias": False, "exclude_unset": False}
for key in export_kwargs.keys(): for key in kwargs.keys():
export_kwargs.pop(key, None) export_kwargs.pop(key, None)
return self.json(*args, **export_kwargs, **kwargs) return self.json(*args, **export_kwargs, **kwargs)
@ -54,9 +58,9 @@ class HyperglassModel(BaseModel):
def export_dict(self, *args, **kwargs): def export_dict(self, *args, **kwargs):
"""Return instance as dictionary.""" """Return instance as dictionary."""
export_kwargs = {"by_alias": True, "exclude_unset": False} export_kwargs = {"by_alias": False, "exclude_unset": False}
for key in export_kwargs.keys(): for key in kwargs.keys():
export_kwargs.pop(key, None) export_kwargs.pop(key, None)
return self.dict(*args, **export_kwargs, **kwargs) return self.dict(*args, **export_kwargs, **kwargs)
@ -71,34 +75,10 @@ class HyperglassModel(BaseModel):
import yaml import yaml
export_kwargs = { export_kwargs = {
"by_alias": kwargs.pop("by_alias", True), "by_alias": kwargs.pop("by_alias", False),
"exclude_unset": kwargs.pop("by_alias", False), "exclude_unset": kwargs.pop("exclude_unset", False),
} }
return yaml.safe_dump( return yaml.safe_dump(
json.loads(self.export_json(**export_kwargs)), *args, **kwargs json.loads(self.export_json(**export_kwargs)), *args, **kwargs
) )
class HyperglassModelExtra(HyperglassModel):
"""Model for hyperglass configuration models with dynamic fields."""
class Config:
"""Pydantic model configuration."""
extra = "allow"
class HyperglassUIModel(HyperglassModel):
"""Base class for UI configuration parameters."""
class Config:
"""Pydantic model configuration."""
alias_generator = snake_to_camel
allow_population_by_field_name = True
def as_ui_model(name: str, model: Type[AsUIModel]) -> Type[AsUIModel]:
"""Override a model's configuration to confirm to a UI model."""
return type(name, (model, HyperglassUIModel), {})

View file

@ -7,7 +7,7 @@ from typing import Any, Dict, List, Tuple, Union, Literal, Optional
from pydantic import StrictStr, StrictBool from pydantic import StrictStr, StrictBool
# Local # Local
from .main import HyperglassUIModel, as_ui_model from .main import HyperglassModel
from .config.web import WebPublic from .config.web import WebPublic
from .config.cache import CachePublic from .config.cache import CachePublic
from .config.params import ParamsPublic from .config.params import ParamsPublic
@ -16,12 +16,8 @@ from .config.messages import Messages
Alignment = Union[Literal["left"], Literal["center"], Literal["right"], None] Alignment = Union[Literal["left"], Literal["center"], Literal["right"], None]
StructuredDataField = Tuple[str, str, Alignment] StructuredDataField = Tuple[str, str, Alignment]
CacheUI = as_ui_model("CacheUI", CachePublic)
WebUI = as_ui_model("WebUI", WebPublic)
MessagesUI = as_ui_model("MessagesUI", Messages)
class UIDirectiveInfo(HyperglassModel):
class UIDirectiveInfo(HyperglassUIModel):
"""UI: Directive Info.""" """UI: Directive Info."""
enable: StrictBool enable: StrictBool
@ -29,7 +25,7 @@ class UIDirectiveInfo(HyperglassUIModel):
content: StrictStr content: StrictStr
class UIDirective(HyperglassUIModel): class UIDirective(HyperglassModel):
"""UI: Directive.""" """UI: Directive."""
id: StrictStr id: StrictStr
@ -41,7 +37,7 @@ class UIDirective(HyperglassUIModel):
options: Optional[List[Dict[str, Any]]] options: Optional[List[Dict[str, Any]]]
class UILocation(HyperglassUIModel): class UILocation(HyperglassModel):
"""UI: Location (Device).""" """UI: Location (Device)."""
id: StrictStr id: StrictStr
@ -50,26 +46,26 @@ class UILocation(HyperglassUIModel):
directives: List[UIDirective] = [] directives: List[UIDirective] = []
class UINetwork(HyperglassUIModel): class UINetwork(HyperglassModel):
"""UI: Network.""" """UI: Network."""
display_name: StrictStr display_name: StrictStr
locations: List[UILocation] = [] locations: List[UILocation] = []
class UIContent(HyperglassUIModel): class UIContent(HyperglassModel):
"""UI: Content.""" """UI: Content."""
credit: StrictStr credit: StrictStr
greeting: StrictStr greeting: StrictStr
class UIParameters(HyperglassUIModel, ParamsPublic): class UIParameters(ParamsPublic, HyperglassModel):
"""UI Configuration Parameters.""" """UI Configuration Parameters."""
cache: CacheUI cache: CachePublic
web: WebUI web: WebPublic
messages: MessagesUI messages: Messages
version: StrictStr version: StrictStr
networks: List[UINetwork] = [] networks: List[UINetwork] = []
parsed_data_fields: Tuple[StructuredDataField, ...] parsed_data_fields: Tuple[StructuredDataField, ...]

View file

@ -11,7 +11,7 @@ from pydantic import StrictStr, root_validator
from hyperglass.log import log from hyperglass.log import log
# Local # Local
from .main import HyperglassModel, HyperglassModelExtra from .main import HyperglassModel
_WEBHOOK_TITLE = "hyperglass received a valid query with the following data" _WEBHOOK_TITLE = "hyperglass received a valid query with the following data"
_ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png" _ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png"
@ -39,7 +39,7 @@ class WebhookHeaders(HyperglassModel):
} }
class WebhookNetwork(HyperglassModelExtra): class WebhookNetwork(HyperglassModel, extra="allow"):
"""Webhook data model.""" """Webhook data model."""
prefix: StrictStr = "Unknown" prefix: StrictStr = "Unknown"

View file

@ -4,13 +4,13 @@ import { Markdown } from '~/components';
import { useColorValue, useBreakpointValue, useConfig } from '~/context'; import { useColorValue, useBreakpointValue, useConfig } from '~/context';
import { useOpposingColor, useStrf } from '~/hooks'; import { useOpposingColor, useStrf } from '~/hooks';
import type { IConfig } from '~/types'; import type { Config } from '~/types';
import type { TFooterButton } from './types'; import type { TFooterButton } from './types';
/** /**
* Filter the configuration object based on values that are strings for formatting. * Filter the configuration object based on values that are strings for formatting.
*/ */
function getConfigFmt(config: IConfig): Record<string, string> { function getConfigFmt(config: Config): Record<string, string> {
const fmt = {} as Record<string, string>; const fmt = {} as Record<string, string>;
for (const [k, v] of Object.entries(config)) { for (const [k, v] of Object.entries(config)) {
if (typeof v === 'string') { if (typeof v === 'string') {

View file

@ -10,12 +10,12 @@ import { FooterLink } from './link';
import { isLink, isMenu } from './types'; import { isLink, isMenu } from './types';
import type { ButtonProps, LinkProps } from '@chakra-ui/react'; import type { ButtonProps, LinkProps } from '@chakra-ui/react';
import type { TLink, TMenu } from '~/types'; import type { Link, Menu } from '~/types';
const CodeIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCode)); const CodeIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCode));
const ExtIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/go').then(i => i.GoLinkExternal)); const ExtIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/go').then(i => i.GoLinkExternal));
function buildItems(links: TLink[], menus: TMenu[]): [(TLink | TMenu)[], (TLink | TMenu)[]] { function buildItems(links: Link[], menus: Menu[]): [(Link | Menu)[], (Link | Menu)[]] {
const leftLinks = links.filter(link => link.side === 'left'); const leftLinks = links.filter(link => link.side === 'left');
const leftMenus = menus.filter(menu => menu.side === 'left'); const leftMenus = menus.filter(menu => menu.side === 'left');
const rightLinks = links.filter(link => link.side === 'right'); const rightLinks = links.filter(link => link.side === 'right');
@ -27,7 +27,7 @@ function buildItems(links: TLink[], menus: TMenu[]): [(TLink | TMenu)[], (TLink
} }
export const Footer: React.FC = () => { export const Footer: React.FC = () => {
const { web, content, primary_asn } = useConfig(); const { web, content, primaryAsn } = useConfig();
const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100'); const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
const footerColor = useColorValue('black', 'white'); const footerColor = useColorValue('black', 'white');
@ -57,10 +57,10 @@ export const Footer: React.FC = () => {
> >
{left.map(item => { {left.map(item => {
if (isLink(item)) { if (isLink(item)) {
const url = strF(item.url, { primary_asn }, '/'); const url = strF(item.url, { primaryAsn }, '/');
const icon: Partial<ButtonProps & LinkProps> = {}; const icon: Partial<ButtonProps & LinkProps> = {};
if (item.show_icon) { if (item.showIcon) {
icon.rightIcon = <ExtIcon />; icon.rightIcon = <ExtIcon />;
} }
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />; return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
@ -73,10 +73,10 @@ export const Footer: React.FC = () => {
{!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />} {!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />}
{right.map(item => { {right.map(item => {
if (isLink(item)) { if (isLink(item)) {
const url = strF(item.url, { primary_asn }, '/'); const url = strF(item.url, { primaryAsn }, '/');
const icon: Partial<ButtonProps & LinkProps> = {}; const icon: Partial<ButtonProps & LinkProps> = {};
if (item.show_icon) { if (item.showIcon) {
icon.rightIcon = <ExtIcon />; icon.rightIcon = <ExtIcon />;
} }
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />; return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;

View file

@ -1,5 +1,5 @@
import type { ButtonProps, LinkProps, MenuListProps } from '@chakra-ui/react'; import type { ButtonProps, LinkProps, MenuListProps } from '@chakra-ui/react';
import type { TLink, TMenu } from '~/types'; import type { Link, Menu } from '~/types';
type TFooterSide = 'left' | 'right'; type TFooterSide = 'left' | 'right';
@ -17,10 +17,10 @@ export interface TColorModeToggle extends ButtonProps {
size?: string; size?: string;
} }
export function isLink(item: TLink | TMenu): item is TLink { export function isLink(item: Link | Menu): item is Link {
return 'url' in item; return 'url' in item;
} }
export function isMenu(item: TLink | TMenu): item is TMenu { export function isMenu(item: Link | Menu): item is Menu {
return 'content' in item; return 'content' in item;
} }

View file

@ -24,13 +24,13 @@ export const QueryGroup: React.FC<TQueryGroup> = (props: TQueryGroup) => {
} else { } else {
selections.queryGroup.set(null); selections.queryGroup.set(null);
} }
onChange({ field: 'query_group', value }); onChange({ field: 'queryGroup', value });
} }
return ( return (
<Select <Select
size="lg" size="lg"
name="query_group" name="queryGroup"
options={options} options={options}
aria-label={label} aria-label={label}
onChange={handleChange} onChange={handleChange}

View file

@ -4,18 +4,18 @@ import { Select } from '~/components';
import { useConfig } from '~/context'; import { useConfig } from '~/context';
import { useLGState, useLGMethods } from '~/hooks'; import { useLGState, useLGMethods } from '~/hooks';
import type { TNetwork, TSelectOption } from '~/types'; import type { Network, TSelectOption } from '~/types';
import type { TQuerySelectField } from './types'; import type { TQuerySelectField } from './types';
function buildOptions(networks: TNetwork[]) { function buildOptions(networks: Network[]) {
return networks return networks
.map(net => { .map(net => {
const label = net.display_name; const label = net.displayName;
const options = net.locations const options = net.locations
.map(loc => ({ .map(loc => ({
label: loc.name, label: loc.name,
value: loc._id, value: loc.id,
group: net.display_name, group: net.displayName,
})) }))
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
return { label, options }; return { label, options };
@ -43,7 +43,7 @@ export const QueryLocation: React.FC<TQuerySelectField> = (props: TQuerySelectFi
} }
if (Array.isArray(e)) { if (Array.isArray(e)) {
const value = e.map(sel => sel!.value); const value = e.map(sel => sel!.value);
onChange({ field: 'query_location', value }); onChange({ field: 'queryLocation', value });
selections.queryLocation.set(e); selections.queryLocation.set(e);
} }
} }
@ -54,11 +54,11 @@ export const QueryLocation: React.FC<TQuerySelectField> = (props: TQuerySelectFi
size="lg" size="lg"
options={options} options={options}
aria-label={label} aria-label={label}
name="query_location" name="queryLocation"
onChange={handleChange} onChange={handleChange}
closeMenuOnSelect={false} closeMenuOnSelect={false}
value={exportState(selections.queryLocation.value)} value={exportState(selections.queryLocation.value)}
isError={typeof errors.query_location !== 'undefined'} isError={typeof errors.queryLocation !== 'undefined'}
/> />
); );
}; };

View file

@ -7,10 +7,10 @@ import { useLGState, useDirective } from '~/hooks';
import { isSelectDirective } from '~/types'; import { isSelectDirective } from '~/types';
import type { OptionProps } from 'react-select'; import type { OptionProps } from 'react-select';
import type { TSelectOption, TDirective } from '~/types'; import type { TSelectOption, Directive } from '~/types';
import type { TQueryTarget } from './types'; import type { TQueryTarget } from './types';
function buildOptions(directive: Nullable<TDirective>): TSelectOption[] { function buildOptions(directive: Nullable<Directive>): TSelectOption[] {
if (directive !== null && isSelectDirective(directive)) { if (directive !== null && isSelectDirective(directive)) {
return directive.options.map(o => ({ return directive.options.map(o => ({
value: o.value, value: o.value,
@ -61,7 +61,7 @@ export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => {
return ( return (
<> <>
<input {...register('query_target')} hidden readOnly value={queryTarget.value} /> <input {...register('queryTarget')} hidden readOnly value={queryTarget.value} />
<If c={directive !== null && isSelectDirective(directive)}> <If c={directive !== null && isSelectDirective(directive)}>
<Select <Select
size="lg" size="lg"
@ -82,7 +82,7 @@ export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => {
aria-label={placeholder} aria-label={placeholder}
placeholder={placeholder} placeholder={placeholder}
value={displayTarget.value} value={displayTarget.value}
name="query_target_display" name="queryTargetDisplay"
onChange={handleInputChange} onChange={handleInputChange}
_placeholder={{ color: placeholderColor }} _placeholder={{ color: placeholderColor }}
/> />

View file

@ -28,18 +28,18 @@ export const QueryType: React.FC<TQuerySelectField> = (props: TQuerySelectField)
selections.queryType.set(null); selections.queryType.set(null);
queryType.set(''); queryType.set('');
} }
onChange({ field: 'query_type', value }); onChange({ field: 'queryType', value });
} }
return ( return (
<Select <Select
size="lg" size="lg"
name="query_type" name="queryType"
options={options} options={options}
aria-label={label} aria-label={label}
onChange={handleChange} onChange={handleChange}
value={exportState(selections.queryType.value)} value={exportState(selections.queryType.value)}
isError={'query_type' in errors} isError={'queryType' in errors}
/> />
); );
}; };

View file

@ -36,11 +36,11 @@ export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget
const query4 = Array.from(families.value).includes(4); const query4 = Array.from(families.value).includes(4);
const query6 = Array.from(families.value).includes(6); const query6 = Array.from(families.value).includes(6);
const tooltip4 = strF(web.text.fqdn_tooltip, { protocol: 'IPv4' }); const tooltip4 = strF(web.text.fqdnTooltip, { protocol: 'IPv4' });
const tooltip6 = strF(web.text.fqdn_tooltip, { protocol: 'IPv6' }); const tooltip6 = strF(web.text.fqdnTooltip, { protocol: 'IPv6' });
const [messageStart, messageEnd] = web.text.fqdn_message.split('{fqdn}'); const [messageStart, messageEnd] = web.text.fqdnMessage.split('{fqdn}');
const [errorStart, errorEnd] = web.text.fqdn_error.split('{fqdn}'); const [errorStart, errorEnd] = web.text.fqdnError.split('{fqdn}');
const { const {
data: data4, data: data4,
@ -63,7 +63,7 @@ export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget
const answer6 = useMemo(() => findAnswer(data6), [data6]); const answer6 = useMemo(() => findAnswer(data6), [data6]);
const handleOverride = useCallback( const handleOverride = useCallback(
(value: string): void => setTarget({ field: 'query_target', value }), (value: string): void => setTarget({ field: 'queryTarget', value }),
[setTarget], [setTarget],
); );
@ -137,7 +137,7 @@ export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget
onClick={errorClose} onClick={errorClose}
leftIcon={<LeftArrow />} leftIcon={<LeftArrow />}
> >
{web.text.fqdn_error_button} {web.text.fqdnErrorButton}
</Button> </Button>
</> </>
)} )}

View file

@ -1,6 +1,6 @@
import type { FormControlProps } from '@chakra-ui/react'; import type { FormControlProps } from '@chakra-ui/react';
import type { UseFormRegister } from 'react-hook-form'; import type { UseFormRegister } from 'react-hook-form';
import type { TBGPCommunity, OnChangeArgs, TFormData } from '~/types'; import type { OnChangeArgs, FormData } from '~/types';
export interface TField extends FormControlProps { export interface TField extends FormControlProps {
name: string; name: string;
@ -21,17 +21,10 @@ export interface TQueryGroup extends TQuerySelectField {
groups: string[]; groups: string[];
} }
export interface TCommunitySelect {
name: string;
onChange: OnChange;
communities: TBGPCommunity[];
register: UseFormRegister<TFormData>;
}
export interface TQueryTarget { export interface TQueryTarget {
name: string; name: string;
placeholder: string; placeholder: string;
register: UseFormRegister<TFormData>; register: UseFormRegister<FormData>;
onChange(e: OnChangeArgs): void; onChange(e: OnChangeArgs): void;
} }

View file

@ -9,9 +9,9 @@ import type { TLogo } from './types';
*/ */
function useLogo(): [string, () => void] { function useLogo(): [string, () => void] {
const { web } = useConfig(); const { web } = useConfig();
const { dark_format, light_format } = web.logo; const { darkFormat, lightFormat } = web.logo;
const src = useColorValue(`/images/dark${dark_format}`, `/images/light${light_format}`); const src = useColorValue(`/images/dark${darkFormat}`, `/images/light${lightFormat}`);
// Use the hyperglass logo if the user's logo can't be loaded for whatever reason. // Use the hyperglass logo if the user's logo can't be loaded for whatever reason.
const fallbackSrc = useColorValue( const fallbackSrc = useColorValue(

View file

@ -106,7 +106,7 @@ const All: React.FC = () => (
export const Title: React.FC<TTitle> = (props: TTitle) => { export const Title: React.FC<TTitle> = (props: TTitle) => {
const { fontSize, ...rest } = props; const { fontSize, ...rest } = props;
const { web } = useConfig(); const { web } = useConfig();
const titleMode = web.text.title_mode; const { titleMode } = web.text;
const { isSubmitting } = useLGState(); const { isSubmitting } = useLGState();
const { resetForm } = useLGMethods(); const { resetForm } = useLGMethods();

View file

@ -1,8 +1,8 @@
import type { ModalContentProps } from '@chakra-ui/react'; import type { ModalContentProps } from '@chakra-ui/react';
import type { TQueryContent, TQueryFields } from '~/types'; import type { QueryContent } from '~/types';
export interface THelpModal extends ModalContentProps { export interface THelpModal extends ModalContentProps {
item: TQueryContent | null; item: QueryContent | null;
name: TQueryFields; name: string;
visible: boolean; visible: boolean;
} }

View file

@ -11,7 +11,7 @@ import type { TFrame } from './types';
export const Frame = (props: TFrame): JSX.Element => { export const Frame = (props: TFrame): JSX.Element => {
const router = useRouter(); const router = useRouter();
const { developer_mode, google_analytics } = useConfig(); const { developerMode, googleAnalytics } = useConfig();
const { isSubmitting } = useLGState(); const { isSubmitting } = useLGState();
const { resetForm } = useLGMethods(); const { resetForm } = useLGMethods();
const { initialize, trackPage } = useGoogleAnalytics(); const { initialize, trackPage } = useGoogleAnalytics();
@ -25,7 +25,7 @@ export const Frame = (props: TFrame): JSX.Element => {
} }
useEffect(() => { useEffect(() => {
initialize(google_analytics, developer_mode); initialize(googleAnalytics, developerMode);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@ -62,10 +62,10 @@ export const Frame = (props: TFrame): JSX.Element => {
{...props} {...props}
/> />
<Footer /> <Footer />
<If c={developer_mode}> <If c={developerMode}>
<Debugger /> <Debugger />
</If> </If>
<ResetButton developerMode={developer_mode} resetForm={handleReset} /> <ResetButton developerMode={developerMode} resetForm={handleReset} />
</Flex> </Flex>
<Greeting /> <Greeting />
</> </>

View file

@ -20,9 +20,9 @@ import {
import { useConfig } from '~/context'; import { useConfig } from '~/context';
import { useStrf, useGreeting, useDevice, useLGState, useLGMethods } from '~/hooks'; import { useStrf, useGreeting, useDevice, useLGState, useLGMethods } from '~/hooks';
import { dedupObjectArray } from '~/util'; import { dedupObjectArray } from '~/util';
import { isString, isQueryField, TDirective } from '~/types'; import { isString, isQueryField, Directive } from '~/types';
import type { TFormData, OnChangeArgs } from '~/types'; import type { FormData, OnChangeArgs } from '~/types';
/** /**
* Don't set the global flag on this. * Don't set the global flag on this.
@ -50,13 +50,12 @@ export const LookingGlass: React.FC = () => {
const getDevice = useDevice(); const getDevice = useDevice();
const strF = useStrf(); const strF = useStrf();
const noQueryType = strF(messages.no_input, { field: web.text.query_type }); const noQueryType = strF(messages.noInput, { field: web.text.queryType });
const noQueryLoc = strF(messages.no_input, { field: web.text.query_location }); const noQueryLoc = strF(messages.noInput, { field: web.text.queryLocation });
const noQueryTarget = strF(messages.no_input, { field: web.text.query_target }); const noQueryTarget = strF(messages.noInput, { field: web.text.queryTarget });
const { const {
availableGroups, availableGroups,
queryVrf,
queryType, queryType,
directive, directive,
availableTypes, availableTypes,
@ -71,29 +70,28 @@ export const LookingGlass: React.FC = () => {
const queryTypes = useMemo(() => availableTypes.map(t => t.id.value), [availableTypes]); const queryTypes = useMemo(() => availableTypes.map(t => t.id.value), [availableTypes]);
const formSchema = vest.create((data: TFormData = {} as TFormData) => { const formSchema = vest.create((data: FormData = {} as FormData) => {
test('query_location', noQueryLoc, () => { test('queryLocation', noQueryLoc, () => {
enforce(data.query_location).isArrayOf(enforce.isString()).isNotEmpty(); enforce(data.queryLocation).isArrayOf(enforce.isString()).isNotEmpty();
}); });
test('query_target', noQueryTarget, () => { test('queryTarget', noQueryTarget, () => {
enforce(data.query_target).longerThan(1); enforce(data.queryTarget).longerThan(1);
}); });
test('query_type', noQueryType, () => { test('queryType', noQueryType, () => {
enforce(data.query_type).inside(queryTypes); enforce(data.queryType).inside(queryTypes);
}); });
test('query_group', 'Query Group is empty', () => { test('queryGroup', 'Query Group is empty', () => {
enforce(data.query_group).isString(); enforce(data.queryGroup).isString();
}); });
}); });
const formInstance = useForm<TFormData>({ const formInstance = useForm<FormData>({
resolver: vestResolver(formSchema), resolver: vestResolver(formSchema),
defaultValues: { defaultValues: {
// query_vrf: 'default', queryTarget: '',
query_target: '', queryLocation: [],
query_location: [], queryType: '',
query_type: '', queryGroup: '',
query_group: '',
}, },
}); });
@ -157,10 +155,10 @@ export const LookingGlass: React.FC = () => {
} }
function handleLocChange(locations: string[]): void { function handleLocChange(locations: string[]): void {
clearErrors('query_location'); clearErrors('queryLocation');
const locationNames = [] as string[]; const locationNames = [] as string[];
const allGroups = [] as string[][]; const allGroups = [] as string[][];
const allTypes = [] as TDirective[][]; const allTypes = [] as Directive[][];
const allDevices = []; const allDevices = [];
queryLocation.set(locations); queryLocation.set(locations);
@ -207,18 +205,17 @@ export const LookingGlass: React.FC = () => {
// If there is more than one location selected, but there are no intersecting VRFs, show an error. // If there is more than one location selected, but there are no intersecting VRFs, show an error.
if (locations.length > 1 && intersecting.length === 0) { if (locations.length > 1 && intersecting.length === 0) {
setError('query_location', { setError('queryLocation', {
// message: `${locationNames.join(', ')} have no VRFs in common.`, // message: `${locationNames.join(', ')} have no VRFs in common.`,
message: `${locationNames.join(', ')} have no groups in common.`, message: `${locationNames.join(', ')} have no groups in common.`,
}); });
} }
// If there is only one intersecting VRF, set it as the form value so the user doesn't have to. // If there is only one intersecting VRF, set it as the form value so the user doesn't have to.
else if (intersecting.length === 1) { else if (intersecting.length === 1) {
// queryVrf.set(intersecting[0]._id);
queryGroup.set(intersecting[0]); queryGroup.set(intersecting[0]);
} }
if (availableGroups.length > 1 && intersectingTypes.length === 0) { if (availableGroups.length > 1 && intersectingTypes.length === 0) {
setError('query_location', { setError('queryLocation', {
message: `${locationNames.join(', ')} have no query types in common.`, message: `${locationNames.join(', ')} have no query types in common.`,
}); });
} else if (intersectingTypes.length === 1) { } else if (intersectingTypes.length === 1) {
@ -228,7 +225,7 @@ export const LookingGlass: React.FC = () => {
function handleGroupChange(group: string): void { function handleGroupChange(group: string): void {
queryGroup.set(group); queryGroup.set(group);
let availTypes = new Array<TDirective>(); let availTypes = new Array<Directive>();
for (const loc of queryLocation) { for (const loc of queryLocation) {
const device = getDevice(loc.value); const device = getDevice(loc.value);
for (const directive of device.directives) { for (const directive of device.directives) {
@ -237,7 +234,7 @@ export const LookingGlass: React.FC = () => {
} }
} }
} }
availTypes = dedupObjectArray<TDirective>(availTypes, 'id'); availTypes = dedupObjectArray<Directive>(availTypes, 'id');
availableTypes.set(availTypes); availableTypes.set(availTypes);
if (availableTypes.length === 1) { if (availableTypes.length === 1) {
queryType.set(availableTypes[0].name.value); queryType.set(availableTypes[0].name.value);
@ -252,9 +249,9 @@ export const LookingGlass: React.FC = () => {
throw new Error(`Field '${e.field}' is not a valid form field.`); throw new Error(`Field '${e.field}' is not a valid form field.`);
} }
if (e.field === 'query_location' && Array.isArray(e.value)) { if (e.field === 'queryLocation' && Array.isArray(e.value)) {
handleLocChange(e.value); handleLocChange(e.value);
} else if (e.field === 'query_type' && isString(e.value)) { } else if (e.field === 'queryType' && isString(e.value)) {
queryType.set(e.value); queryType.set(e.value);
if (queryTarget.value !== '') { if (queryTarget.value !== '') {
// Reset queryTarget as well, so that, for example, selecting BGP Community, and selecting // Reset queryTarget as well, so that, for example, selecting BGP Community, and selecting
@ -263,21 +260,19 @@ export const LookingGlass: React.FC = () => {
queryTarget.set(''); queryTarget.set('');
displayTarget.set(''); displayTarget.set('');
} }
} else if (e.field === 'query_vrf' && isString(e.value)) { } else if (e.field === 'queryTarget' && isString(e.value)) {
queryVrf.set(e.value);
} else if (e.field === 'query_target' && isString(e.value)) {
queryTarget.set(e.value); queryTarget.set(e.value);
} else if (e.field === 'query_group' && isString(e.value)) { } else if (e.field === 'queryGroup' && isString(e.value)) {
// queryGroup.set(e.value); // queryGroup.set(e.value);
handleGroupChange(e.value); handleGroupChange(e.value);
} }
} }
useEffect(() => { useEffect(() => {
register('query_location', { required: true }); register('queryLocation', { required: true });
// register('query_target', { required: true }); // register('queryTarget', { required: true });
register('query_type', { required: true }); register('queryType', { required: true });
register('query_group'); register('queryGroup');
}, [register]); }, [register]);
return ( return (
@ -297,13 +292,13 @@ export const LookingGlass: React.FC = () => {
onSubmit={handleSubmit(submitHandler)} onSubmit={handleSubmit(submitHandler)}
> >
<FormRow> <FormRow>
<FormField name="query_location" label={web.text.query_location}> <FormField name="queryLocation" label={web.text.queryLocation}>
<QueryLocation onChange={handleChange} label={web.text.query_location} /> <QueryLocation onChange={handleChange} label={web.text.queryLocation} />
</FormField> </FormField>
<If c={availableGroups.length > 1}> <If c={availableGroups.length > 1}>
<FormField label={web.text.query_group} name="query_group"> <FormField label={web.text.queryGroup} name="queryGroup">
<QueryGroup <QueryGroup
label={web.text.query_group} label={web.text.queryGroup}
groups={availableGroups.value} groups={availableGroups.value}
onChange={handleChange} onChange={handleChange}
/> />
@ -313,24 +308,24 @@ export const LookingGlass: React.FC = () => {
<FormRow> <FormRow>
<SlideFade offsetX={-100} in={availableTypes.length > 1} unmountOnExit> <SlideFade offsetX={-100} in={availableTypes.length > 1} unmountOnExit>
<FormField <FormField
name="query_type" name="queryType"
label={web.text.query_type} label={web.text.queryType}
labelAddOn={ labelAddOn={
<HelpModal <HelpModal
visible={selectedDirective?.info.value !== null} visible={selectedDirective?.info.value !== null}
item={selectedDirective?.info.value ?? null} item={selectedDirective?.info.value ?? null}
name="query_type" name="queryType"
/> />
} }
> >
<QueryType onChange={handleChange} label={web.text.query_type} /> <QueryType onChange={handleChange} label={web.text.queryType} />
</FormField> </FormField>
</SlideFade> </SlideFade>
<SlideFade offsetX={100} in={selectedDirective !== null} unmountOnExit> <SlideFade offsetX={100} in={selectedDirective !== null} unmountOnExit>
{selectedDirective !== null && ( {selectedDirective !== null && (
<FormField name="query_target" label={web.text.query_target}> <FormField name="queryTarget" label={web.text.queryTarget}>
<QueryTarget <QueryTarget
name="query_target" name="queryTarget"
register={register} register={register}
onChange={handleChange} onChange={handleChange}
placeholder={selectedDirective.description.value} placeholder={selectedDirective.description.value}

View file

@ -10,9 +10,9 @@ export const Meta: React.FC = () => {
const [location, setLocation] = useState('/'); const [location, setLocation] = useState('/');
const { const {
site_title: title = 'hyperglass', siteTitle: title = 'hyperglass',
site_description: description = 'Network Looking Glass', siteDescription: description = 'Network Looking Glass',
site_keywords: keywords = [ siteKeywords: keywords = [
'hyperglass', 'hyperglass',
'looking glass', 'looking glass',
'lg', 'lg',
@ -53,7 +53,7 @@ export const Meta: React.FC = () => {
<meta property="og:image:alt" content={siteName} /> <meta property="og:image:alt" content={siteName} />
<meta name="og:description" content={description} /> <meta name="og:description" content={description} />
<meta name="keywords" content={keywords.join(', ')} /> <meta name="keywords" content={keywords.join(', ')} />
<meta name="hg-version" content={config.hyperglass_version} /> <meta name="hg-version" content={config.version} />
</Head> </Head>
); );
}; };

View file

@ -4,7 +4,7 @@ import type { TCell } from './types';
export const Cell: React.FC<TCell> = (props: TCell) => { export const Cell: React.FC<TCell> = (props: TCell) => {
const { data, rawData } = props; const { data, rawData } = props;
const cellId = data.column.id as keyof TRoute; const cellId = data.column.id as keyof Route;
const component = { const component = {
med: <MonoField v={data.value} />, med: <MonoField v={data.value} />,
age: <Age inSeconds={data.value} />, age: <Age inSeconds={data.value} />,

View file

@ -115,7 +115,7 @@ export const Communities: React.FC<TCommunities> = (props: TCommunities) => {
return ( return (
<> <>
<If c={communities.length === 0}> <If c={communities.length === 0}>
<Tooltip placement="right" hasArrow label={web.text.no_communities}> <Tooltip placement="right" hasArrow label={web.text.noCommunities}>
<Link> <Link>
<Icon as={Question} /> <Icon as={Question} />
</Link> </Link>
@ -166,10 +166,10 @@ const _RPKIState: React.ForwardRefRenderFunction<HTMLDivElement, TRPKIState> = (
const icon = [NotAllowed, Check, Warning, Question]; const icon = [NotAllowed, Check, Warning, Question];
const text = [ const text = [
web.text.rpki_invalid, web.text.rpkiInvalid,
web.text.rpki_valid, web.text.rpkiValid,
web.text.rpki_unknown, web.text.rpkiUnknown,
web.text.rpki_unverified, web.text.rpkiUnverified,
]; ];
return ( return (

View file

@ -3,10 +3,10 @@ import { useConfig } from '~/context';
import { Table } from '~/components'; import { Table } from '~/components';
import { Cell } from './cell'; import { Cell } from './cell';
import type { TColumn, TParsedDataField, TCellRender } from '~/types'; import type { TColumn, ParsedDataField, TCellRender } from '~/types';
import type { TBGPTable } from './types'; import type { TBGPTable } from './types';
function makeColumns(fields: TParsedDataField[]): TColumn[] { function makeColumns(fields: ParsedDataField[]): TColumn[] {
return fields.map(pair => { return fields.map(pair => {
const [header, accessor, align] = pair; const [header, accessor, align] = pair;
@ -27,8 +27,8 @@ function makeColumns(fields: TParsedDataField[]): TColumn[] {
export const BGPTable: React.FC<TBGPTable> = (props: TBGPTable) => { export const BGPTable: React.FC<TBGPTable> = (props: TBGPTable) => {
const { children: data, ...rest } = props; const { children: data, ...rest } = props;
const { parsed_data_fields } = useConfig(); const { parsedDataFields } = useConfig();
const columns = makeColumns(parsed_data_fields); const columns = makeColumns(parsedDataFields);
return ( return (
<Flex my={8} justify="center" maxW="100%" w="100%" {...rest}> <Flex my={8} justify="center" maxW="100%" w="100%" {...rest}>

View file

@ -42,9 +42,9 @@ export interface TRPKIState {
export interface TCell { export interface TCell {
data: TCellRender; data: TCellRender;
rawData: TStructuredResponse; rawData: StructuredResponse;
} }
export interface TBGPTable extends Omit<FlexProps, 'children'> { export interface TBGPTable extends Omit<FlexProps, 'children'> {
children: TStructuredResponse; children: StructuredResponse;
} }

View file

@ -9,11 +9,11 @@ import type { TChart, TNode, TNodeData } from './types';
export const Chart: React.FC<TChart> = (props: TChart) => { export const Chart: React.FC<TChart> = (props: TChart) => {
const { data } = props; const { data } = props;
const { primary_asn, org_name } = useConfig(); const { primaryAsn, orgName } = useConfig();
const dots = useColorToken('colors', 'blackAlpha.500', 'whiteAlpha.400'); const dots = useColorToken('colors', 'blackAlpha.500', 'whiteAlpha.400');
const elements = useElements({ asn: primary_asn, name: org_name }, data); const elements = useElements({ asn: primaryAsn, name: orgName }, data);
return ( return (
<ReactFlowProvider> <ReactFlowProvider>

View file

@ -21,7 +21,7 @@ export const Path: React.FC<TPath> = (props: TPath) => {
const { getResponse } = useLGMethods(); const { getResponse } = useLGMethods();
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const response = getResponse(device); const response = getResponse(device);
const output = response?.output as TStructuredResponse; const output = response?.output as StructuredResponse;
const bg = useColorValue('light.50', 'dark.900'); const bg = useColorValue('light.50', 'dark.900');
const centered = useBreakpointValue({ base: false, lg: true }) ?? true; const centered = useBreakpointValue({ base: false, lg: true }) ?? true;
return ( return (

View file

@ -1,7 +1,7 @@
import type { NodeProps } from 'react-flow-renderer'; import type { NodeProps } from 'react-flow-renderer';
export interface TChart { export interface TChart {
data: TStructuredResponse; data: StructuredResponse;
} }
export interface TPath { export interface TPath {

View file

@ -8,7 +8,7 @@ import type { BasePath } from './types';
const NODE_WIDTH = 200; const NODE_WIDTH = 200;
const NODE_HEIGHT = 48; const NODE_HEIGHT = 48;
export function useElements(base: BasePath, data: TStructuredResponse): FlowElement[] { export function useElements(base: BasePath, data: StructuredResponse): FlowElement[] {
return useMemo(() => { return useMemo(() => {
return [...buildElements(base, data)]; return [...buildElements(base, data)];
}, [base, data]); }, [base, data]);
@ -18,7 +18,7 @@ export function useElements(base: BasePath, data: TStructuredResponse): FlowElem
* Calculate the positions for each AS Path. * Calculate the positions for each AS Path.
* @see https://github.com/MrBlenny/react-flow-chart/issues/61 * @see https://github.com/MrBlenny/react-flow-chart/issues/61
*/ */
function* buildElements(base: BasePath, data: TStructuredResponse): Generator<FlowElement> { function* buildElements(base: BasePath, data: StructuredResponse): Generator<FlowElement> {
const { routes } = data; const { routes } = data;
// Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths. // Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths.
const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]); const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]);

View file

@ -7,7 +7,7 @@ import { Result } from './individual';
import { Tags } from './tags'; import { Tags } from './tags';
export const Results: React.FC = () => { export const Results: React.FC = () => {
const { queryLocation, queryTarget, queryType, queryVrf, queryGroup } = useLGState(); const { queryLocation, queryTarget, queryType, queryGroup } = useLGState();
const getDevice = useDevice(); const getDevice = useDevice();
@ -45,9 +45,8 @@ export const Results: React.FC = () => {
<Result <Result
index={i} index={i}
device={device} device={device}
key={device._id} key={device.id}
queryLocation={loc.value} queryLocation={loc.value}
queryVrf={queryVrf.value}
queryType={queryType.value} queryType={queryType.value}
queryGroup={queryGroup.value} queryGroup={queryGroup.value}
queryTarget={queryTarget.value} queryTarget={queryTarget.value}

View file

@ -9,13 +9,13 @@ export function isFetchError(error: any): error is Response {
return typeof error !== 'undefined' && error !== null && 'statusText' in error; return typeof error !== 'undefined' && error !== null && 'statusText' in error;
} }
export function isLGError(error: any): error is TQueryResponse { export function isLGError(error: any): error is QueryResponse {
return typeof error !== 'undefined' && error !== null && 'output' in error; return typeof error !== 'undefined' && error !== null && 'output' in error;
} }
/** /**
* Returns true if the response is an LG error, false if not. * Returns true if the response is an LG error, false if not.
*/ */
export function isLGOutputOrError(data: any): data is TQueryResponse { export function isLGOutputOrError(data: any): data is QueryResponse {
return typeof data !== 'undefined' && data !== null && data?.level !== 'success'; return typeof data !== 'undefined' && data !== null && data?.level !== 'success';
} }

View file

@ -24,7 +24,7 @@ export const ResultHeader: React.FC<TResultHeader> = (props: TResultHeader) => {
const { web } = useConfig(); const { web } = useConfig();
const strF = useStrf(); const strF = useStrf();
const text = strF(web.text.complete_time, { seconds: runtime }); const text = strF(web.text.completeTime, { seconds: runtime });
const label = useMemo(() => runtimeText(runtime, text), [runtime, text]); const label = useMemo(() => runtimeText(runtime, text), [runtime, text]);
const color = useOpposingColor(isError ? warning : defaultStatus); const color = useOpposingColor(isError ? warning : defaultStatus);

View file

@ -39,7 +39,7 @@ const AccordionHeaderWrapper = chakra('div', {
}); });
const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props: TResult, ref) => { const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props: TResult, ref) => {
const { index, device, queryVrf, queryType, queryTarget, queryLocation, queryGroup } = props; const { index, device, queryType, queryTarget, queryLocation, queryGroup } = props;
const { web, cache, messages } = useConfig(); const { web, cache, messages } = useConfig();
const { index: indices, setIndex } = useAccordionContext(); const { index: indices, setIndex } = useAccordionContext();
@ -56,17 +56,16 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
queryLocation, queryLocation,
queryTarget, queryTarget,
queryType, queryType,
queryVrf,
queryGroup, queryGroup,
}); });
const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [data, isFetchedAfterMount]); const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [data, isFetchedAfterMount]);
if (typeof data !== 'undefined') { if (typeof data !== 'undefined') {
responses.merge({ [device._id]: data }); responses.merge({ [device.id]: data });
} }
const strF = useStrf(); const strF = useStrf();
const cacheLabel = strF(web.text.cache_icon, { time: data?.timestamp }); const cacheLabel = strF(web.text.cacheIcon, { time: data?.timestamp });
const errorKeywords = useMemo(() => { const errorKeywords = useMemo(() => {
let kw = [] as string[]; let kw = [] as string[];
@ -85,13 +84,13 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
} else if (isFetchError(error)) { } else if (isFetchError(error)) {
return startCase(error.statusText); return startCase(error.statusText);
} else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) { } else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
return messages.request_timeout; return messages.requestTimeout;
} else if (isStackError(error)) { } else if (isStackError(error)) {
return startCase(error.message); return startCase(error.message);
} else { } else {
return messages.general; return messages.general;
} }
}, [error, data, messages.general, messages.request_timeout]); }, [error, data, messages.general, messages.requestTimeout]);
isError && console.error(error); isError && console.error(error);
@ -101,12 +100,12 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
warning: 'warning', warning: 'warning',
error: 'warning', error: 'warning',
danger: 'error', danger: 'error',
} as { [k in TResponseLevel]: 'success' | 'warning' | 'error' }; } as { [K in ResponseLevel]: 'success' | 'warning' | 'error' };
let e: TErrorLevels = 'error'; let e: TErrorLevels = 'error';
if (isLGError(error)) { if (isLGError(error)) {
const idx = error.level as TResponseLevel; const idx = error.level as ResponseLevel;
e = statusMap[idx]; e = statusMap[idx];
} }
return e; return e;
@ -146,7 +145,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
return ( return (
<AnimatedAccordionItem <AnimatedAccordionItem
ref={ref} ref={ref}
id={device._id} id={device.id}
isDisabled={isLoading} isDisabled={isLoading}
exit={{ opacity: 0, y: 300 }} exit={{ opacity: 0, y: 300 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -171,7 +170,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
</AccordionButton> </AccordionButton>
<HStack py={2} spacing={1}> <HStack py={2} spacing={1}>
{isStructuredOutput(data) && data.level === 'success' && tableComponent && ( {isStructuredOutput(data) && data.level === 'success' && tableComponent && (
<Path device={device._id} /> <Path device={device.id} />
)} )}
<CopyButton copyValue={copyValue} isDisabled={isLoading} /> <CopyButton copyValue={copyValue} isDisabled={isLoading} />
<RequeryButton requery={refetch} isDisabled={isLoading} /> <RequeryButton requery={refetch} isDisabled={isLoading} />
@ -230,9 +229,9 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
flex="1 0 auto" flex="1 0 auto"
justifyContent={{ base: 'flex-start', lg: 'flex-end' }} justifyContent={{ base: 'flex-start', lg: 'flex-end' }}
> >
<If c={cache.show_text && !isError && isCached}> <If c={cache.showText && !isError && isCached}>
<If c={!isMobile}> <If c={!isMobile}>
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} /> <Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
</If> </If>
<Tooltip hasArrow label={cacheLabel} placement="top"> <Tooltip hasArrow label={cacheLabel} placement="top">
<Box> <Box>
@ -240,7 +239,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
</Box> </Box>
</Tooltip> </Tooltip>
<If c={isMobile}> <If c={isMobile}>
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} /> <Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
</If> </If>
</If> </If>
</HStack> </HStack>

View file

@ -93,7 +93,7 @@ export const Tags: React.FC = () => {
> >
<Label <Label
bg={queryBg} bg={queryBg}
label={web.text.query_type} label={web.text.queryType}
fontSize={{ base: 'xs', md: 'sm' }} fontSize={{ base: 'xs', md: 'sm' }}
value={selectedDirective?.value.name ?? 'None'} value={selectedDirective?.value.name ?? 'None'}
/> />
@ -107,7 +107,7 @@ export const Tags: React.FC = () => {
<Label <Label
bg={targetBg} bg={targetBg}
value={queryTarget.value} value={queryTarget.value}
label={web.text.query_target} label={web.text.queryTarget}
fontSize={{ base: 'xs', md: 'sm' }} fontSize={{ base: 'xs', md: 'sm' }}
/> />
</motion.div> </motion.div>
@ -119,7 +119,7 @@ export const Tags: React.FC = () => {
> >
<Label <Label
bg={vrfBg} bg={vrfBg}
label={web.text.query_group} label={web.text.queryGroup}
value={queryGroup.value} value={queryGroup.value}
fontSize={{ base: 'xs', md: 'sm' }} fontSize={{ base: 'xs', md: 'sm' }}
/> />

View file

@ -1,7 +1,7 @@
import type { State } from '@hookstate/core'; import type { State } from '@hookstate/core';
import type { ButtonProps } from '@chakra-ui/react'; import type { ButtonProps } from '@chakra-ui/react';
import type { UseQueryResult } from 'react-query'; import type { UseQueryResult } from 'react-query';
import type { TDevice } from '~/types'; import type { Device } from '~/types';
export interface TResultHeader { export interface TResultHeader {
title: string; title: string;
@ -19,8 +19,7 @@ export interface TFormattedError {
export interface TResult { export interface TResult {
index: number; index: number;
device: TDevice; device: Device;
queryVrf: string;
queryGroup: string; queryGroup: string;
queryTarget: string; queryTarget: string;
queryLocation: string; queryLocation: string;
@ -34,7 +33,7 @@ export interface TCopyButton extends ButtonProps {
} }
export interface TRequeryButton extends ButtonProps { export interface TRequeryButton extends ButtonProps {
requery: UseQueryResult<TQueryResponse>['refetch']; requery: UseQueryResult<QueryResponse>['refetch'];
} }
export type TUseResults = { export type TUseResults = {

View file

@ -71,11 +71,11 @@ export const Table: React.FC<TTable> = (props: TTable) => {
defaultColumn, defaultColumn,
data, data,
initialState: { hiddenColumns }, initialState: { hiddenColumns },
} as TableOptions<TRoute>; } as TableOptions<Route>;
const plugins = [useSortBy, usePagination] as PluginHook<TRoute>[]; const plugins = [useSortBy, usePagination] as PluginHook<Route>[];
const instance = useTable<TRoute>(options, ...plugins); const instance = useTable<Route>(options, ...plugins);
const { const {
page, page,

View file

@ -3,14 +3,14 @@ import type { BoxProps, IconButtonProps } from '@chakra-ui/react';
import type { Theme, TColumn, TCellRender } from '~/types'; import type { Theme, TColumn, TCellRender } from '~/types';
export interface TTable { export interface TTable {
data: TRoute[]; data: Route[];
striped?: boolean; striped?: boolean;
columns: TColumn[]; columns: TColumn[];
heading?: React.ReactNode; heading?: React.ReactNode;
bordersVertical?: boolean; bordersVertical?: boolean;
bordersHorizontal?: boolean; bordersHorizontal?: boolean;
Cell?: React.FC<TCellRender>; Cell?: React.FC<TCellRender>;
rowHighlightProp?: keyof IRoute; rowHighlightProp?: keyof Route;
rowHighlightBg?: Theme.ColorNames; rowHighlightBg?: Theme.ColorNames;
} }

View file

@ -9,17 +9,17 @@ import {
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import { makeTheme, defaultTheme } from '~/util'; import { makeTheme, defaultTheme } from '~/util';
import type { IConfig, Theme } from '~/types'; import type { Config, Theme } from '~/types';
import type { THyperglassProvider } from './types'; import type { THyperglassProvider } from './types';
const HyperglassContext = createContext<IConfig>(Object()); const HyperglassContext = createContext<Config>(Object());
const queryClient = new QueryClient(); const queryClient = new QueryClient();
export const HyperglassProvider: React.FC<THyperglassProvider> = (props: THyperglassProvider) => { export const HyperglassProvider: React.FC<THyperglassProvider> = (props: THyperglassProvider) => {
const { config, children } = props; const { config, children } = props;
const value = useMemo(() => config, [config]); const value = useMemo(() => config, [config]);
const userTheme = value && makeTheme(value.web.theme, value.web.theme.default_color_mode); const userTheme = value && makeTheme(value.web.theme, value.web.theme.defaultColorMode);
const theme = value ? userTheme : defaultTheme; const theme = value ? userTheme : defaultTheme;
return ( return (
<ChakraProvider theme={theme}> <ChakraProvider theme={theme}>
@ -33,7 +33,7 @@ export const HyperglassProvider: React.FC<THyperglassProvider> = (props: THyperg
/** /**
* Get the current configuration. * Get the current configuration.
*/ */
export const useConfig = (): IConfig => useContext(HyperglassContext); export const useConfig = (): Config => useContext(HyperglassContext);
/** /**
* Get the current theme object. * Get the current theme object.

View file

@ -1,14 +1,14 @@
import type { State } from '@hookstate/core'; import type { State } from '@hookstate/core';
import type { IConfig, TFormData } from '~/types'; import type { Config, FormData } from '~/types';
export interface THyperglassProvider { export interface THyperglassProvider {
config: IConfig; config: Config;
children: React.ReactNode; children: React.ReactNode;
} }
export interface TGlobalState { export interface TGlobalState {
isSubmitting: boolean; isSubmitting: boolean;
formData: TFormData; formData: FormData;
} }
export interface TUseGlobalState { export interface TUseGlobalState {

View file

@ -1,6 +1,6 @@
import type { State } from '@hookstate/core'; import type { State } from '@hookstate/core';
import type * as ReactGA from 'react-ga'; import type * as ReactGA from 'react-ga';
import type { TDevice, Families, TFormQuery, TDeviceVrf, TSelectOption, TDirective } from '~/types'; import type { Device, Families, TFormQuery, TSelectOption, Directive } from '~/types';
export type LGQueryKey = [string, TFormQuery]; export type LGQueryKey = [string, TFormQuery];
export type DNSQueryKey = [string, { target: string | null; family: 4 | 6 }]; export type DNSQueryKey = [string, { target: string | null; family: 4 | 6 }];
@ -23,56 +23,50 @@ export type TUseDevice = (
* Device's ID, e.g. the device.name field. * Device's ID, e.g. the device.name field.
*/ */
deviceId: string, deviceId: string,
) => TDevice; ) => Device;
export type TUseVrf = (vrfId: string) => TDeviceVrf;
export interface TSelections { export interface TSelections {
queryLocation: TSelectOption[] | []; queryLocation: TSelectOption[] | [];
queryType: TSelectOption | null; queryType: TSelectOption | null;
queryVrf: TSelectOption | null;
queryGroup: TSelectOption | null; queryGroup: TSelectOption | null;
} }
export interface TMethodsExtension { export interface TMethodsExtension {
getResponse(d: string): TQueryResponse | null; getResponse(d: string): QueryResponse | null;
resolvedClose(): void; resolvedClose(): void;
resolvedOpen(): void; resolvedOpen(): void;
formReady(): boolean; formReady(): boolean;
resetForm(): void; resetForm(): void;
stateExporter<O extends unknown>(o: O): O | null; stateExporter<O extends unknown>(o: O): O | null;
getDirective(n: string): Nullable<State<TDirective>>; getDirective(n: string): Nullable<State<Directive>>;
} }
export type TLGState = { export type TLGState = {
queryVrf: string;
queryGroup: string; queryGroup: string;
families: Families; families: Families;
queryTarget: string; queryTarget: string;
btnLoading: boolean; btnLoading: boolean;
isSubmitting: boolean; isSubmitting: boolean;
displayTarget: string; displayTarget: string;
directive: TDirective | null; directive: Directive | null;
// queryType: TQueryTypes;
queryType: string; queryType: string;
queryLocation: string[]; queryLocation: string[];
availVrfs: TDeviceVrf[];
availableGroups: string[]; availableGroups: string[];
availableTypes: TDirective[]; availableTypes: Directive[];
resolvedIsOpen: boolean; resolvedIsOpen: boolean;
selections: TSelections; selections: TSelections;
responses: { [d: string]: TQueryResponse }; responses: { [d: string]: QueryResponse };
}; };
export type TLGStateHandlers = { export type TLGStateHandlers = {
exportState<S extends unknown | null>(s: S): S | null; exportState<S extends unknown | null>(s: S): S | null;
getResponse(d: string): TQueryResponse | null; getResponse(d: string): QueryResponse | null;
resolvedClose(): void; resolvedClose(): void;
resolvedOpen(): void; resolvedOpen(): void;
formReady(): boolean; formReady(): boolean;
resetForm(): void; resetForm(): void;
stateExporter<O extends unknown>(o: O): O | null; stateExporter<O extends unknown>(o: O): O | null;
getDirective(n: string): Nullable<State<TDirective>>; getDirective(n: string): Nullable<State<Directive>>;
}; };
export type UseStrfArgs = { [k: string]: unknown } | string; export type UseStrfArgs = { [k: string]: unknown } | string;
@ -89,7 +83,7 @@ export type TTableToStringFormatted = {
active: (v: boolean) => string; active: (v: boolean) => string;
as_path: (v: number[]) => string; as_path: (v: number[]) => string;
communities: (v: string[]) => string; communities: (v: string[]) => string;
rpki_state: (v: number, n: TRPKIStates) => string; rpki_state: (v: number, n: RPKIState) => string;
}; };
export type GAEffect = (ga: typeof ReactGA) => void; export type GAEffect = (ga: typeof ReactGA) => void;

View file

@ -59,7 +59,7 @@ export function useDNSQuery(
} }
return useQuery<DnsOverHttps.Response, unknown, DnsOverHttps.Response, DNSQueryKey>({ return useQuery<DnsOverHttps.Response, unknown, DnsOverHttps.Response, DNSQueryKey>({
queryKey: [web.dns_provider.url, { target, family }], queryKey: [web.dnsProvider.url, { target, family }],
queryFn: query, queryFn: query,
cacheTime: cache.timeout * 1000, cacheTime: cache.timeout * 1000,
}); });

View file

@ -1,7 +1,7 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useConfig } from '~/context'; import { useConfig } from '~/context';
import type { TDevice } from '~/types'; import type { Device } from '~/types';
import type { TUseDevice } from './types'; import type { TUseDevice } from './types';
/** /**
@ -12,8 +12,8 @@ export function useDevice(): TUseDevice {
const devices = useMemo(() => networks.map(n => n.locations).flat(), [networks]); const devices = useMemo(() => networks.map(n => n.locations).flat(), [networks]);
function getDevice(id: string): TDevice { function getDevice(id: string): Device {
return devices.filter(dev => dev._id === id)[0]; return devices.filter(dev => dev.id === id)[0];
} }
return useCallback(getDevice, [devices]); return useCallback(getDevice, [devices]);

View file

@ -1,13 +1,13 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useLGMethods, useLGState } from './useLGState'; import { useLGMethods, useLGState } from './useLGState';
import type { TDirective } from '~/types'; import type { Directive } from '~/types';
export function useDirective(): Nullable<TDirective> { export function useDirective(): Nullable<Directive> {
const { queryType, queryGroup } = useLGState(); const { queryType, queryGroup } = useLGState();
const { getDirective } = useLGMethods(); const { getDirective } = useLGMethods();
return useMemo((): Nullable<TDirective> => { return useMemo((): Nullable<Directive> => {
if (queryType.value === '') { if (queryType.value === '') {
return null; return null;
} }

View file

@ -4,9 +4,9 @@ import { getHyperglassConfig } from '~/util';
import type { UseQueryResult } from 'react-query'; import type { UseQueryResult } from 'react-query';
import type { ConfigLoadError } from '~/util'; import type { ConfigLoadError } from '~/util';
import type { IConfig } from '~/types'; import type { Config } from '~/types';
type UseHyperglassConfig = UseQueryResult<IConfig, ConfigLoadError> & { type UseHyperglassConfig = UseQueryResult<Config, ConfigLoadError> & {
/** /**
* Initial configuration load has failed. * Initial configuration load has failed.
*/ */
@ -37,7 +37,7 @@ export function useHyperglassConfig(): UseHyperglassConfig {
// will be displayed, which will also show the loading state. // will be displayed, which will also show the loading state.
const [initFailed, setInitFailed] = useState<boolean>(false); const [initFailed, setInitFailed] = useState<boolean>(false);
const query = useQuery<IConfig, ConfigLoadError>({ const query = useQuery<Config, ConfigLoadError>({
queryKey: 'hyperglass-ui-config', queryKey: 'hyperglass-ui-config',
queryFn: getHyperglassConfig, queryFn: getHyperglassConfig,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,

View file

@ -11,8 +11,8 @@ import type { LGQueryKey } from './types';
/** /**
* Custom hook handle submission of a query to the hyperglass backend. * Custom hook handle submission of a query to the hyperglass backend.
*/ */
export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryResponse> { export function useLGQuery(query: TFormQuery): QueryObserverResult<QueryResponse> {
const { request_timeout, cache } = useConfig(); const { requestTimeout, cache } = useConfig();
const controller = useMemo(() => new AbortController(), []); const controller = useMemo(() => new AbortController(), []);
const { trackEvent } = useGoogleAnalytics(); const { trackEvent } = useGoogleAnalytics();
@ -26,9 +26,9 @@ export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryRespons
dimension4: query.queryGroup, dimension4: query.queryGroup,
}); });
const runQuery: QueryFunction<TQueryResponse, LGQueryKey> = async ( const runQuery: QueryFunction<QueryResponse, LGQueryKey> = async (
ctx: QueryFunctionContext<LGQueryKey>, ctx: QueryFunctionContext<LGQueryKey>,
): Promise<TQueryResponse> => { ): Promise<QueryResponse> => {
const [url, data] = ctx.queryKey; const [url, data] = ctx.queryKey;
const { queryLocation, queryTarget, queryType, queryGroup } = data; const { queryLocation, queryTarget, queryType, queryGroup } = data;
const res = await fetchWithTimeout( const res = await fetchWithTimeout(
@ -44,7 +44,7 @@ export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryRespons
}), }),
mode: 'cors', mode: 'cors',
}, },
request_timeout * 1000, requestTimeout * 1000,
controller, controller,
); );
try { try {
@ -62,7 +62,7 @@ export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryRespons
[controller], [controller],
); );
return useQuery<TQueryResponse, Response | TQueryResponse | Error, TQueryResponse, LGQueryKey>({ return useQuery<QueryResponse, Response | QueryResponse | Error, QueryResponse, LGQueryKey>({
queryKey: ['/api/query/', query], queryKey: ['/api/query/', query],
queryFn: runQuery, queryFn: runQuery,
// Invalidate react-query's cache just shy of the configured cache timeout. // Invalidate react-query's cache just shy of the configured cache timeout.

View file

@ -5,7 +5,7 @@ import { all } from '~/util';
import type { State, PluginStateControl, Plugin } from '@hookstate/core'; import type { State, PluginStateControl, Plugin } from '@hookstate/core';
import type { TLGState, TLGStateHandlers, TMethodsExtension } from './types'; import type { TLGState, TLGStateHandlers, TMethodsExtension } from './types';
import { TDirective } from '~/types'; import type { Directive } from '~/types';
const MethodsId = Symbol('Methods'); const MethodsId = Symbol('Methods');
@ -30,7 +30,7 @@ class MethodsInstance {
/** /**
* Find a response based on the device ID. * Find a response based on the device ID.
*/ */
public getResponse(state: State<TLGState>, device: string): TQueryResponse | null { public getResponse(state: State<TLGState>, device: string): QueryResponse | null {
if (device in state.responses) { if (device in state.responses) {
return state.responses[device].value; return state.responses[device].value;
} else { } else {
@ -38,7 +38,7 @@ class MethodsInstance {
} }
} }
public getDirective(state: State<TLGState>, name: string): Nullable<State<TDirective>> { public getDirective(state: State<TLGState>, name: string): Nullable<State<Directive>> {
const [directive] = state.availableTypes.filter(t => t.id.value === name); const [directive] = state.availableTypes.filter(t => t.id.value === name);
if (typeof directive !== 'undefined') { if (typeof directive !== 'undefined') {
return directive; return directive;
@ -55,7 +55,6 @@ class MethodsInstance {
state.isSubmitting.value && state.isSubmitting.value &&
all( all(
...[ ...[
// state.queryVrf.value !== '',
state.queryType.value !== '', state.queryType.value !== '',
state.queryGroup.value !== '', state.queryGroup.value !== '',
state.queryTarget.value !== '', state.queryTarget.value !== '',
@ -70,7 +69,6 @@ class MethodsInstance {
*/ */
public resetForm(state: State<TLGState>) { public resetForm(state: State<TLGState>) {
state.merge({ state.merge({
queryVrf: '',
families: [], families: [],
queryType: '', queryType: '',
queryGroup: '', queryGroup: '',
@ -81,10 +79,9 @@ class MethodsInstance {
btnLoading: false, btnLoading: false,
isSubmitting: false, isSubmitting: false,
resolvedIsOpen: false, resolvedIsOpen: false,
availVrfs: [],
availableGroups: [], availableGroups: [],
availableTypes: [], availableTypes: [],
selections: { queryLocation: [], queryType: null, queryVrf: null, queryGroup: null }, selections: { queryLocation: [], queryType: null, queryGroup: null },
}); });
} }
@ -150,7 +147,7 @@ function Methods(inst?: State<TLGState>): Plugin | TMethodsExtension {
} }
const LGState = createState<TLGState>({ const LGState = createState<TLGState>({
selections: { queryLocation: [], queryType: null, queryVrf: null, queryGroup: null }, selections: { queryLocation: [], queryType: null, queryGroup: null },
resolvedIsOpen: false, resolvedIsOpen: false,
isSubmitting: false, isSubmitting: false,
availableGroups: [], availableGroups: [],
@ -162,9 +159,7 @@ const LGState = createState<TLGState>({
queryTarget: '', queryTarget: '',
queryGroup: '', queryGroup: '',
queryType: '', queryType: '',
availVrfs: [],
responses: {}, responses: {},
queryVrf: '',
families: [], families: [],
}); });

View file

@ -42,17 +42,17 @@ function formatTime(val: number): string {
*/ */
export function useTableToString( export function useTableToString(
target: string, target: string,
data: TQueryResponse | undefined, data: QueryResponse | undefined,
...deps: unknown[] ...deps: unknown[]
): () => string { ): () => string {
const { web, parsed_data_fields, messages } = useConfig(); const { web, parsedDataFields, messages } = useConfig();
function formatRpkiState(val: number): string { function formatRpkiState(val: number): string {
const rpkiStates = [ const rpkiStates = [
web.text.rpki_invalid, web.text.rpkiInvalid,
web.text.rpki_valid, web.text.rpkiValid,
web.text.rpki_unknown, web.text.rpkiUnknown,
web.text.rpki_unverified, web.text.rpkiUnverified,
]; ];
return rpkiStates[val]; return rpkiStates[val];
} }
@ -69,7 +69,7 @@ export function useTableToString(
return key in tableFormatMap; return key in tableFormatMap;
} }
function getFmtFunc(accessor: keyof TRoute): TTableToStringFormatter { function getFmtFunc(accessor: keyof Route): TTableToStringFormatter {
if (isFormatted(accessor)) { if (isFormatted(accessor)) {
return tableFormatMap[accessor]; return tableFormatMap[accessor];
} else { } else {
@ -77,13 +77,13 @@ export function useTableToString(
} }
} }
function doFormat(target: string, data: TQueryResponse | undefined): string { function doFormat(target: string, data: QueryResponse | undefined): string {
let result = messages.no_output; let result = messages.noOutput;
try { try {
if (typeof data !== 'undefined' && isStructuredOutput(data)) { if (typeof data !== 'undefined' && isStructuredOutput(data)) {
const tableStringParts = [`Routes For: ${target}`, `Timestamp: ${data.timestamp} UTC`]; const tableStringParts = [`Routes For: ${target}`, `Timestamp: ${data.timestamp} UTC`];
for (const route of data.output.routes) { for (const route of data.output.routes) {
for (const field of parsed_data_fields) { for (const field of parsedDataFields) {
const [header, accessor, align] = field; const [header, accessor, align] = field;
if (align !== null) { if (align !== null) {
let value = route[accessor]; let value = route[accessor];

View file

@ -69,6 +69,7 @@
"http-proxy-middleware": "0.20.0", "http-proxy-middleware": "0.20.0",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"prettier-eslint": "^13.0.0", "prettier-eslint": "^13.0.0",
"type-fest": "^2.3.2",
"typescript": "^4.4.2" "typescript": "^4.4.2"
} }
} }

View file

@ -1,10 +1,13 @@
import type { Theme } from './theme'; import type { Theme } from './theme';
import type { CamelCasedPropertiesDeep, CamelCasedProperties } from 'type-fest';
export type TQueryFields = 'query_type' | 'query_target' | 'query_location' | 'query_vrf'; // export type QueryFields = 'query_type' | 'query_target' | 'query_location' | 'query_vrf';
type TSide = 'left' | 'right'; type Side = 'left' | 'right';
export interface IConfigMessages { export type ParsedDataField = [string, keyof Route, 'left' | 'right' | 'center' | null];
interface _Messages {
no_input: string; no_input: string;
acl_denied: string; acl_denied: string;
acl_not_allowed: string; acl_not_allowed: string;
@ -20,13 +23,13 @@ export interface IConfigMessages {
parsing_error: string; parsing_error: string;
} }
export interface IConfigTheme { interface _ThemeConfig {
colors: { [k: string]: string }; colors: Record<string, string>;
default_color_mode: 'light' | 'dark' | null; default_color_mode: 'light' | 'dark' | null;
fonts: Theme.Fonts; fonts: Theme.Fonts;
} }
export interface IConfigWebText { interface _Text {
title_mode: string; title_mode: string;
title: string; title: string;
subtitle: string; subtitle: string;
@ -48,145 +51,150 @@ export interface IConfigWebText {
no_communities: string; no_communities: string;
} }
export interface TConfigGreeting { interface _Greeting {
enable: boolean; enable: boolean;
title: string; title: string;
button: string; button: string;
required: boolean; required: boolean;
} }
export interface TConfigWebLogo { interface _Logo {
width: string; width: string;
height: string | null; height: string | null;
light_format: string; light_format: string;
dark_format: string; dark_format: string;
} }
export interface TLink { interface _Link {
title: string; title: string;
url: string; url: string;
show_icon: boolean; show_icon: boolean;
side: TSide; side: Side;
order: number; order: number;
} }
export interface TMenu { interface _Menu {
title: string; title: string;
content: string; content: string;
side: TSide; side: Side;
order: number; order: number;
} }
export interface IConfigWeb { interface _Credit {
credit: { enable: boolean };
dns_provider: { name: string; url: string };
links: TLink[];
menus: TMenu[];
greeting: TConfigGreeting;
help_menu: { enable: boolean; title: string };
logo: TConfigWebLogo;
terms: { enable: boolean; title: string };
text: IConfigWebText;
theme: IConfigTheme;
}
export interface TQuery {
name: string;
enable: boolean; enable: boolean;
display_name: string;
} }
export interface TBGPCommunity { interface _Web {
community: string; credit: _Credit;
display_name: string; dns_provider: { name: string; url: string };
description: string; links: _Link[];
menus: _Menu[];
greeting: _Greeting;
help_menu: { enable: boolean; title: string };
logo: _Logo;
terms: { enable: boolean; title: string };
text: _Text;
theme: _ThemeConfig;
} }
export interface IQueryBGPRoute extends TQuery {} // export interface Query {
export interface IQueryBGPASPath extends TQuery {} // name: string;
export interface IQueryPing extends TQuery {} // enable: boolean;
export interface IQueryTraceroute extends TQuery {} // display_name: string;
export interface IQueryBGPCommunity extends TQuery { // }
mode: 'input' | 'select';
communities: TBGPCommunity[];
}
export interface TConfigQueries { // export interface BGPCommunity {
bgp_route: IQueryBGPRoute; // community: string;
bgp_community: IQueryBGPCommunity; // display_name: string;
bgp_aspath: IQueryBGPASPath; // description: string;
ping: IQueryPing; // }
traceroute: IQueryTraceroute;
list: TQuery[];
}
interface TDeviceVrfBase { // export interface QueryBGPRoute extends Query {}
_id: string; // export interface QueryBGPASPath extends Query {}
display_name: string; // export interface QueryPing extends Query {}
default: boolean; // export interface QueryTraceroute extends Query {}
} // export interface QueryBGPCommunity extends Query {
// mode: 'input' | 'select';
// communities: BGPCommunity[];
// }
export interface TDeviceVrf extends TDeviceVrfBase { // export interface Queries {
ipv4: boolean; // bgp_route: QueryBGPRoute;
ipv6: boolean; // bgp_community: QueryBGPCommunity;
} // bgp_aspath: QueryBGPASPath;
// ping: QueryPing;
// traceroute: QueryTraceroute;
// list: Query[];
// }
type TDirectiveBase = { type _DirectiveBase = {
id: string; id: string;
name: string; name: string;
field_type: 'text' | 'select' | null; field_type: 'text' | 'select' | null;
description: string; description: string;
groups: string[]; groups: string[];
info: TQueryContent | null; info: _QueryContent | null;
}; };
export type TDirectiveOption = { type _DirectiveOption = {
name: string; name: string;
value: string; value: string;
description: string | null; description: string | null;
}; };
export type TDirectiveSelect = TDirectiveBase & { type _DirectiveSelect = _DirectiveBase & {
options: TDirectiveOption[]; options: _DirectiveOption[];
}; };
export type TDirective = TDirectiveBase | TDirectiveSelect; type _Directive = _DirectiveBase | _DirectiveSelect;
export interface TDevice { interface _Device {
_id: string; id: string;
name: string; name: string;
network: string; network: string;
directives: TDirective[]; directives: _Directive[];
} }
export interface TNetworkLocation extends TDevice {} interface _Network {
export interface TNetwork {
display_name: string; display_name: string;
locations: TDevice[]; locations: _Device[];
} }
export type TParsedDataField = [string, keyof TRoute, 'left' | 'right' | 'center' | null]; interface _QueryContent {
export interface TQueryContent {
content: string; content: string;
enable: boolean; enable: boolean;
params: { params: {
primary_asn: IConfig['primary_asn']; primary_asn: _Config['primary_asn'];
org_name: IConfig['org_name']; org_name: _Config['org_name'];
site_title: IConfig['site_title']; site_title: _Config['site_title'];
title: string; title: string;
[k: string]: string; [k: string]: string;
}; };
} }
export interface IConfigContent { interface _Content {
credit: string; credit: string;
greeting: string; greeting: string;
} }
export interface IConfig { interface _Cache {
cache: { show_text: boolean; timeout: number }; show_text: boolean;
timeout: number;
}
type _Config = _ConfigDeep & _ConfigShallow;
interface _ConfigDeep {
cache: _Cache;
web: _Web;
messages: _Messages;
// queries: Queries;
devices: _Device[];
networks: _Network[];
content: _Content;
}
interface _ConfigShallow {
debug: boolean; debug: boolean;
developer_mode: boolean; developer_mode: boolean;
primary_asn: string; primary_asn: string;
@ -196,14 +204,8 @@ export interface IConfig {
site_title: string; site_title: string;
site_keywords: string[]; site_keywords: string[];
site_description: string; site_description: string;
web: IConfigWeb; version: string;
messages: IConfigMessages; parsed_data_fields: ParsedDataField[];
hyperglass_version: string;
queries: TConfigQueries;
devices: TDevice[];
networks: TNetwork[];
parsed_data_fields: TParsedDataField[];
content: IConfigContent;
} }
export interface Favicon { export interface Favicon {
@ -218,3 +220,19 @@ export interface FaviconComponent {
href: string; href: string;
type: string; type: string;
} }
export type Config = CamelCasedPropertiesDeep<_ConfigDeep> & CamelCasedProperties<_ConfigShallow>;
export type ThemeConfig = CamelCasedProperties<_ThemeConfig>;
export type Content = CamelCasedProperties<_Content>;
export type QueryContent = CamelCasedPropertiesDeep<_QueryContent>;
export type Network = CamelCasedPropertiesDeep<_Network>;
export type Device = CamelCasedPropertiesDeep<_Device>;
export type Directive = CamelCasedPropertiesDeep<_Directive>;
export type DirectiveSelect = CamelCasedPropertiesDeep<_DirectiveSelect>;
export type DirectiveOption = CamelCasedPropertiesDeep<_DirectiveOption>;
export type Text = CamelCasedProperties<_Text>;
export type Web = CamelCasedPropertiesDeep<_Web>;
export type Greeting = CamelCasedProperties<_Greeting>;
export type Logo = CamelCasedProperties<_Logo>;
export type Link = CamelCasedProperties<_Link>;
export type Menu = CamelCasedProperties<_Menu>;

View file

@ -1,30 +1,21 @@
export type TQueryTypes = '' | TValidQueryTypes; export type TQueryTypes = '' | TValidQueryTypes;
export type TValidQueryTypes = 'bgp_route' | 'bgp_community' | 'bgp_aspath' | 'ping' | 'traceroute'; export type TValidQueryTypes = 'bgp_route' | 'bgp_community' | 'bgp_aspath' | 'ping' | 'traceroute';
export interface TFormData { export interface FormData {
query_location: string[];
query_type: TQueryTypes;
query_vrf: string;
query_target: string;
query_group: string;
}
export interface TFormState {
queryLocation: string[]; queryLocation: string[];
queryType: string; queryType: string;
queryVrf: string;
queryTarget: string; queryTarget: string;
queryGroup: string; queryGroup: string;
} }
export interface TFormQuery extends Omit<TFormState, 'queryLocation'> { export interface TFormQuery extends Omit<FormData, 'queryLocation'> {
queryLocation: string; queryLocation: string;
} }
export interface TStringTableData extends Omit<TQueryResponse, 'output'> { export interface TStringTableData extends Omit<QueryResponse, 'output'> {
output: TStructuredResponse; output: StructuredResponse;
} }
export interface TQueryResponseString extends Omit<TQueryResponse, 'output'> { export interface TQueryResponseString extends Omit<QueryResponse, 'output'> {
output: string; output: string;
} }

View file

@ -6,11 +6,11 @@ declare global {
type Nullable<T> = T | null; type Nullable<T> = T | null;
type TRPKIStates = 0 | 1 | 2 | 3; type RPKIState = 0 | 1 | 2 | 3;
type TResponseLevel = 'success' | 'warning' | 'error' | 'danger'; type ResponseLevel = 'success' | 'warning' | 'error' | 'danger';
interface IRoute { type Route = {
prefix: string; prefix: string;
active: boolean; active: boolean;
age: number; age: number;
@ -23,40 +23,26 @@ declare global {
source_as: number; source_as: number;
source_rid: string; source_rid: string;
peer_rid: string; peer_rid: string;
rpki_state: TRPKIStates; rpki_state: RPKIState;
}
type TRoute = {
prefix: string;
active: boolean;
age: number;
weight: number;
med: number;
local_preference: number;
as_path: number[];
communities: string[];
next_hop: string;
source_as: number;
source_rid: string;
peer_rid: string;
rpki_state: TRPKIStates;
}; };
type TRouteField = { [k in keyof TRoute]: ValueOf<TRoute> };
type TStructuredResponse = { type RouteField = { [K in keyof Route]: Route[K] };
type StructuredResponse = {
vrf: string; vrf: string;
count: number; count: number;
routes: TRoute[]; routes: Route[];
winning_weight: 'high' | 'low'; winning_weight: 'high' | 'low';
}; };
type TQueryResponse = {
type QueryResponse = {
random: string; random: string;
cached: boolean; cached: boolean;
runtime: number; runtime: number;
level: TResponseLevel; level: ResponseLevel;
timestamp: string; timestamp: string;
keywords: string[]; keywords: string[];
output: string | TStructuredResponse; output: string | StructuredResponse;
format: 'text/plain' | 'application/json'; format: 'text/plain' | 'application/json';
}; };
type ReactRef<T = HTMLElement> = MutableRefObject<T>; type ReactRef<T = HTMLElement> = MutableRefObject<T>;

View file

@ -1,7 +1,7 @@
import type { State } from '@hookstate/core'; import type { State } from '@hookstate/core';
import type { TFormData, TStringTableData, TQueryResponseString } from './data'; import type { FormData, TStringTableData, TQueryResponseString } from './data';
import type { TSelectOption } from './common'; import type { TSelectOption } from './common';
import type { TQueryContent, TDirectiveSelect, TDirective } from './config'; import type { QueryContent, DirectiveSelect, Directive } from './config';
export function isString(a: unknown): a is string { export function isString(a: unknown): a is string {
return typeof a === 'string'; return typeof a === 'string';
@ -27,7 +27,7 @@ export function isStringOutput(data: unknown): data is TQueryResponseString {
); );
} }
export function isQueryContent(content: unknown): content is TQueryContent { export function isQueryContent(content: unknown): content is QueryContent {
return isObject(content) && 'content' in content; return isObject(content) && 'content' in content;
} }
@ -52,13 +52,13 @@ export function isState<S>(a: unknown): a is State<NonNullable<S>> {
/** /**
* Determine if a form field name is a valid form key name. * Determine if a form field name is a valid form key name.
*/ */
export function isQueryField(field: string): field is keyof TFormData { export function isQueryField(field: string): field is keyof FormData {
return ['query_location', 'query_type', 'query_group', 'query_target'].includes(field); return ['queryLocation', 'queryType', 'queryGroup', 'queryTarget'].includes(field);
} }
/** /**
* Determine if a directive is a select directive. * Determine if a directive is a select directive.
*/ */
export function isSelectDirective(directive: TDirective): directive is TDirectiveSelect { export function isSelectDirective(directive: Directive): directive is DirectiveSelect {
return directive.field_type === 'select'; return directive.fieldType === 'select';
} }

View file

@ -2,13 +2,13 @@ import type { CellProps } from 'react-table';
export interface TColumn { export interface TColumn {
Header: string; Header: string;
accessor: keyof TRoute; accessor: keyof Route;
align: string; align: string;
hidden: boolean; hidden: boolean;
} }
export type TCellRender = { export type TCellRender = {
column: CellProps<TRouteField>['column']; column: CellProps<RouteField>['column'];
row: CellProps<TRouteField>['row']; row: CellProps<RouteField>['row'];
value: CellProps<TRouteField>['value']; value: CellProps<RouteField>['value'];
}; };

View file

@ -73,6 +73,18 @@ export function arrangeIntoTree<P extends unknown>(paths: P[][]): PathPart[] {
} }
} }
/**
* Strictly typed version of `Object.entries()`.
*/
export function entries<O, K extends keyof O = keyof O>(obj: O): [K, O[K]][] {
const _entries = [] as [K, O[K]][];
const keys = Object.keys(obj) as K[];
for (const key of keys) {
_entries.push([key, obj[key]]);
}
return _entries;
}
/** /**
* Fetch Wrapper that incorporates a timeout via a passed AbortController instance. * Fetch Wrapper that incorporates a timeout via a passed AbortController instance.
* *

View file

@ -1,6 +1,6 @@
import { isObject } from '~/types'; import { isObject } from '~/types';
import type { IConfig, FaviconComponent } from '~/types'; import type { Config, FaviconComponent } from '~/types';
export class ConfigLoadError extends Error { export class ConfigLoadError extends Error {
public url: string = '/ui/props/'; public url: string = '/ui/props/';
@ -23,7 +23,7 @@ export class ConfigLoadError extends Error {
} }
} }
export async function getHyperglassConfig(): Promise<IConfig> { export async function getHyperglassConfig(): Promise<Config> {
let mode: RequestInit['mode']; let mode: RequestInit['mode'];
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
@ -40,7 +40,7 @@ export async function getHyperglassConfig(): Promise<IConfig> {
throw response; throw response;
} }
if (isObject(data)) { if (isObject(data)) {
return data as IConfig; return data as Config;
} }
} catch (error) { } catch (error) {
if (error instanceof TypeError) { if (error instanceof TypeError) {

View file

@ -3,7 +3,7 @@ import { mode } from '@chakra-ui/theme-tools';
import { generateFontFamily, generatePalette } from 'palette-by-numbers'; import { generateFontFamily, generatePalette } from 'palette-by-numbers';
import type { ChakraTheme } from '@chakra-ui/react'; import type { ChakraTheme } from '@chakra-ui/react';
import type { IConfigTheme, Theme } from '~/types'; import type { ThemeConfig, Theme } from '~/types';
function importFonts(userFonts: Theme.Fonts): ChakraTheme['fonts'] { function importFonts(userFonts: Theme.Fonts): ChakraTheme['fonts'] {
const { body: userBody, mono: userMono } = userFonts; const { body: userBody, mono: userMono } = userFonts;
@ -15,9 +15,9 @@ function importFonts(userFonts: Theme.Fonts): ChakraTheme['fonts'] {
}; };
} }
function importColors(userColors: IConfigTheme['colors']): Theme.Colors { function importColors(userColors: ThemeConfig['colors']): Theme.Colors {
const generatedColors = {} as Theme.Colors; const generatedColors = {} as Theme.Colors;
for (const [k, v] of Object.entries(userColors)) { for (const [k, v] of Object.entries<string>(userColors)) {
generatedColors[k] = generatePalette(v); generatedColors[k] = generatePalette(v);
} }
@ -54,7 +54,7 @@ function importColors(userColors: IConfigTheme['colors']): Theme.Colors {
} }
export function makeTheme( export function makeTheme(
userTheme: IConfigTheme, userTheme: ThemeConfig,
defaultColorMode: 'dark' | 'light' | null, defaultColorMode: 'dark' | 'light' | null,
): Theme.Full { ): Theme.Full {
const fonts = importFonts(userTheme.fonts); const fonts = importFonts(userTheme.fonts);

View file

@ -5995,6 +5995,11 @@ type-fest@^0.7.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48"
integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==
type-fest@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.3.2.tgz#bb91f7ff24788ed81e28463eb94e5a1306f5bab3"
integrity sha512-cfvZ1nOC/VqAt8bVOIlFz8x+HdDASpiFYrSi0U0nzcAFlOnzzQ/gsPg2PP1uqjreO7sQCtraYJHMduXSewQsSA==
type-is@~1.6.17, type-is@~1.6.18: type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18" version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"

View file

@ -93,6 +93,7 @@ reportMissingImports = true
reportMissingTypeStubs = true reportMissingTypeStubs = true
[tool.taskipy.tasks] [tool.taskipy.tasks]
check = {cmd = "task lint && task ui-lint", help = "Run all lint checks"}
lint = {cmd = "flake8 hyperglass", help = "Run Flake8"} lint = {cmd = "flake8 hyperglass", help = "Run Flake8"}
sort = {cmd = "isort hyperglass", help = "Run iSort"} sort = {cmd = "isort hyperglass", help = "Run iSort"}
start = {cmd = "uvicorn hyperglass.api:app", help = "Start hyperglass via Uvicorn"} start = {cmd = "uvicorn hyperglass.api:app", help = "Start hyperglass via Uvicorn"}
@ -103,3 +104,4 @@ ui-format = {cmd = "yarn --cwd ./hyperglass/ui/ format", help = "Run Prettier"}
ui-lint = {cmd = "yarn --cwd ./hyperglass/ui/ lint", help = "Run ESLint"} ui-lint = {cmd = "yarn --cwd ./hyperglass/ui/ lint", help = "Run ESLint"}
ui-typecheck = {cmd = "yarn --cwd ./hyperglass/ui/ typecheck", help = "Run TypeScript Check"} ui-typecheck = {cmd = "yarn --cwd ./hyperglass/ui/ typecheck", help = "Run TypeScript Check"}
upgrade = {cmd = "python3 version.py", help = "Upgrade hyperglass version"} upgrade = {cmd = "python3 version.py", help = "Upgrade hyperglass version"}
yarn = {cmd = "yarn --cwd ./hyperglass/ui/", help = "Run a yarn command from the UI directory"}