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.
log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config)
params: Params = validate_config(config=user_config, importer=Params)
params = validate_config(config=user_config, importer=Params)
# Re-evaluate debug state after config is validated
log_level = current_log_level(log)

View file

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

View file

@ -1,4 +1,6 @@
"""All Data Models used by hyperglass."""
# 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 .tnsr import TNSRCommands
from .vyos import VyosCommands
from ..main import HyperglassModelExtra
from ..main import HyperglassModel
from .common import CommandGroup
from .huawei import HuaweiCommands
from .juniper import JuniperCommands
@ -34,7 +34,7 @@ _NOS_MAP = {
}
class Commands(HyperglassModelExtra):
class Commands(HyperglassModel, extra="allow", validate_all=False):
"""Base class for command definitions."""
arista_eos: CommandGroup = AristaEOSCommands()
@ -69,8 +69,3 @@ class Commands(HyperglassModelExtra):
nos_cmds = nos_cmd_set(**cmds)
setattr(obj, nos, nos_cmds)
return obj
class Config:
"""Override pydantic config."""
validate_all = False

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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.
*

View file

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

View file

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

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"
integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==
type-fest@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.3.2.tgz#bb91f7ff24788ed81e28463eb94e5a1306f5bab3"
integrity sha512-cfvZ1nOC/VqAt8bVOIlFz8x+HdDASpiFYrSi0U0nzcAFlOnzzQ/gsPg2PP1uqjreO7sQCtraYJHMduXSewQsSA==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"

View file

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