diff --git a/.flake8 b/.flake8
index a94e7e0..0934026 100644
--- a/.flake8
+++ b/.flake8
@@ -3,16 +3,18 @@ max-line-length=88
count=True
show-source=False
statistics=True
-exclude=.git, __pycache__, hyperglass/api/examples/*.py, hyperglass/compat/_sshtunnel.py, hyperglass/test.py
+exclude=.git, __pycache__, hyperglass/api/examples/*.py, hyperglass/compat/_sshtunnel.py, test.py
filename=*.py
per-file-ignores=
hyperglass/main.py:E402
# Disable redefinition warning for exception handlers
hyperglass/api.py:F811
# Disable classmethod warning for validator decorators
+ hyperglass/models.py:N805,E0213,R0903
hyperglass/api/models/*.py:N805,E0213,R0903
- hyperglass/configuration/models/*.py:N805,E0213,R0903,E501,C0301
hyperglass/api/models/response.py:E501,C0301
+ hyperglass/parsing/models/*.py:N805,E0213,R0903
+ hyperglass/configuration/models/*.py:N805,E0213,R0903,E501,C0301
ignore=W503,C0330,R504,D202,S403,S301
select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W
disable-noqa=False
diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py
index 03f3afc..c94a8dd 100644
--- a/hyperglass/configuration/__init__.py
+++ b/hyperglass/configuration/__init__.py
@@ -24,6 +24,7 @@ from hyperglass.constants import (
DEFAULT_TERMS,
DEFAULT_DETAILS,
SUPPORTED_QUERY_TYPES,
+ PARSED_RESPONSE_FIELDS,
__version__,
)
from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing
@@ -425,6 +426,7 @@ _frontend_params.update(
"devices": frontend_devices,
"networks": networks,
"vrfs": vrfs,
+ "parsed_data_fields": PARSED_RESPONSE_FIELDS,
"content": {
"help_menu": content_help,
"terms": content_terms,
diff --git a/hyperglass/configuration/models/web.py b/hyperglass/configuration/models/web.py
index 426f7b1..52fd176 100644
--- a/hyperglass/configuration/models/web.py
+++ b/hyperglass/configuration/models/web.py
@@ -141,6 +141,10 @@ class Text(HyperglassModel):
cache_prefix: StrictStr = "Results cached for "
cache_icon: StrictStr = "Cached from {time} UTC" # Formatted by Javascript
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript
+ rpki_invalid: StrictStr = "Invalid"
+ rpki_valid: StrictStr = "Valid"
+ rpki_unknown: StrictStr = "No ROAs Exist"
+ rpki_unverified: StrictStr = "Not Verified"
@validator("title_mode")
def validate_title_mode(cls, value):
diff --git a/hyperglass/constants.py b/hyperglass/constants.py
index 069b915..68918bb 100644
--- a/hyperglass/constants.py
+++ b/hyperglass/constants.py
@@ -26,6 +26,21 @@ DNS_OVER_HTTPS = {
"cloudflare": "https://cloudflare-dns.com/dns-query",
}
+PARSED_RESPONSE_FIELDS = (
+ ("Active", "active", None),
+ ("RPKI State", "rpki_state", "center"),
+ ("AS Path", "as_path", "left"),
+ ("Next Hop", "next_hop", "left"),
+ ("Origin", "source_as", "right"),
+ ("Weight", "weight", "center"),
+ ("Local Preference", "local_preference", "center"),
+ ("MED", "med", "center"),
+ ("Communities", "communities", "center"),
+ ("Originator", "source_rid", "right"),
+ ("Peer", "peer_rid", "right"),
+ ("Age", "age", "right"),
+)
+
CREDIT = """
Powered by [**hyperglass**](https://github.com/checktheroads/hyperglass) version \
{version}. Source code licensed \
diff --git a/hyperglass/exceptions.py b/hyperglass/exceptions.py
index 2dd46ea..0b4dc60 100644
--- a/hyperglass/exceptions.py
+++ b/hyperglass/exceptions.py
@@ -193,3 +193,9 @@ class ResponseEmpty(_UnformattedHyperglassError):
class UnsupportedDevice(_UnformattedHyperglassError):
"""Raised when an input NOS is not in the supported NOS list."""
+
+
+class ParsingError(_UnformattedHyperglassError):
+ """Raised when there is a problem parsing a structured response."""
+
+ _level = "danger"
diff --git a/hyperglass/models.py b/hyperglass/models.py
index bd3b680..50086b0 100644
--- a/hyperglass/models.py
+++ b/hyperglass/models.py
@@ -35,7 +35,14 @@ class HyperglassModel(BaseModel):
Returns:
{str} -- Stringified JSON.
"""
- return self.json(by_alias=True, exclude_unset=False, *args, **kwargs)
+
+ export_kwargs = {
+ "by_alias": True,
+ "exclude_unset": False,
+ **kwargs,
+ }
+
+ return self.json(*args, **export_kwargs)
def export_dict(self, *args, **kwargs):
"""Return instance as dictionary.
@@ -43,7 +50,13 @@ class HyperglassModel(BaseModel):
Returns:
{dict} -- Python dictionary.
"""
- return self.dict(by_alias=True, exclude_unset=False, *args, **kwargs)
+ export_kwargs = {
+ "by_alias": True,
+ "exclude_unset": False,
+ **kwargs,
+ }
+
+ return self.dict(*args, **export_kwargs)
def export_yaml(self, *args, **kwargs):
"""Return instance as YAML.
@@ -54,7 +67,14 @@ class HyperglassModel(BaseModel):
import json
import yaml
- return yaml.safe_dump(json.loads(self.export_json()), *args, **kwargs)
+ export_kwargs = {
+ "by_alias": kwargs.pop("by_alias", True),
+ "exclude_unset": kwargs.pop("by_alias", False),
+ }
+
+ return yaml.safe_dump(
+ json.loads(self.export_json(**export_kwargs)), *args, **kwargs
+ )
class HyperglassModelExtra(HyperglassModel):
diff --git a/hyperglass/parsing/juniper.py b/hyperglass/parsing/juniper.py
new file mode 100644
index 0000000..68dae3d
--- /dev/null
+++ b/hyperglass/parsing/juniper.py
@@ -0,0 +1,25 @@
+"""Parse Juniper XML Response to Structured Data."""
+
+# Third Party
+import xmltodict
+
+# Project
+from hyperglass.log import log
+from hyperglass.exceptions import ParsingError
+from hyperglass.parsing.models.juniper import JuniperRoute
+
+
+def parse_juniper(output):
+ """Parse a Juniper BGP XML response."""
+ try:
+ parsed = xmltodict.parse(output)["rpc-reply"]["route-information"][
+ "route-table"
+ ]
+ validated = JuniperRoute(**parsed)
+ return validated.serialize().export_dict()
+ except xmltodict.expat.ExpatError as err:
+ log.critical(str(err))
+ raise ParsingError("Error parsing response data")
+ except KeyError as err:
+ log.critical(f"'{str(err)}' was not found in the response")
+ raise ParsingError("Error parsing response data")
diff --git a/hyperglass/parsing/models/__init__.py b/hyperglass/parsing/models/__init__.py
new file mode 100644
index 0000000..9ce8b52
--- /dev/null
+++ b/hyperglass/parsing/models/__init__.py
@@ -0,0 +1 @@
+"""Data models for parsed responses."""
diff --git a/hyperglass/parsing/models/juniper.py b/hyperglass/parsing/models/juniper.py
new file mode 100644
index 0000000..7813076
--- /dev/null
+++ b/hyperglass/parsing/models/juniper.py
@@ -0,0 +1,167 @@
+"""Data Models for Parsing Juniper XML Response."""
+
+# Standard Library
+from typing import List
+
+# Third Party
+from pydantic import StrictInt, StrictStr, StrictBool, validator, root_validator
+
+# Project
+from hyperglass.log import log
+from hyperglass.models import HyperglassModel
+from hyperglass.parsing.models.serialized import ParsedRoutes
+
+RPKI_STATE_MAP = {
+ "invalid": 0,
+ "valid": 1,
+ "unknown": 2,
+ "unverified": 3,
+}
+
+
+def _alias_generator(field):
+ return field.replace("_", "-")
+
+
+class _JuniperBase(HyperglassModel):
+ class Config:
+ alias_generator = _alias_generator
+ extra = "ignore"
+
+
+class JuniperRouteTableEntry(_JuniperBase):
+ """Parse Juniper rt-entry data."""
+
+ active_tag: StrictBool
+ preference: int
+ age: StrictInt
+ local_preference: int
+ metric: int = 0
+ as_path: List[StrictInt] = []
+ validation_state: StrictInt = 3
+ next_hop: StrictStr
+ peer_rid: StrictStr
+ peer_as: int
+ source_as: int
+ source_rid: StrictStr
+ communities: List[StrictStr]
+
+ @root_validator(pre=True)
+ def validate_optional_flags(cls, values):
+ """Flatten & rename keys prior to validation."""
+ values["next-hop"] = values.pop("nh").get("to", "")
+ _path_attr = values.get("bgp-path-attributes", {})
+ _path_attr_agg = _path_attr.get("attr-aggregator", {}).get("attr-value", {})
+ values["as-path"] = _path_attr.get("attr-as-path-effective", {}).get(
+ "attr-value", ""
+ )
+ values["source-as"] = _path_attr_agg.get("aggr-as-number", 0)
+ values["source-rid"] = _path_attr_agg.get("aggr-router-id", "")
+ values["peer-rid"] = values["peer-id"]
+ return values
+
+ @validator("validation_state", pre=True, always=True)
+ def validate_rpki_state(cls, value):
+ """Convert string RPKI state to standard integer mapping."""
+ return RPKI_STATE_MAP.get(value, 3)
+
+ @validator("active_tag", pre=True, always=True)
+ def validate_active_tag(cls, value):
+ """Convert active-tag from string/null to boolean."""
+ if value == "*":
+ value = True
+ else:
+ value = False
+ return value
+
+ @validator("age", pre=True, always=True)
+ def validate_age(cls, value):
+ """Get age as seconds."""
+ if not isinstance(value, dict):
+ try:
+ value = int(value)
+ except ValueError:
+ raise ValueError(f"Age field is in an unexpected format. Got: {value}")
+ else:
+ value = value.get("@junos:seconds", 0)
+ return int(value)
+
+ @validator("as_path", pre=True, always=True)
+ def validate_as_path(cls, value):
+ """Remove origin flags from AS_PATH."""
+ disallowed = ("E", "I", "?")
+ return [int(a) for a in value.split() if a not in disallowed]
+
+ @validator("communities", pre=True, always=True)
+ def validate_communities(cls, value):
+ """Flatten community list."""
+ return value.get("community", [])
+
+
+class JuniperRouteTable(_JuniperBase):
+ """Validation model for Juniper rt data."""
+
+ rt_destination: StrictStr
+ rt_prefix_length: int
+ rt_entry_count: int
+ rt_announced_count: int
+ rt_entry: List[JuniperRouteTableEntry]
+
+ @validator("rt_entry_count", pre=True, always=True)
+ def validate_entry_count(cls, value):
+ """Flatten & convert entry-count to integer."""
+ return int(value.get("#text"))
+
+
+class JuniperRoute(_JuniperBase):
+ """Validation model for route-table data."""
+
+ table_name: StrictStr
+ destination_count: int
+ total_route_count: int
+ active_route_count: int
+ hidden_route_count: int
+ rt: JuniperRouteTable
+
+ def serialize(self):
+ """Convert the Juniper-specific fields to standard parsed data model."""
+ vrf_parts = self.table_name.split(".")
+ if len(vrf_parts) == 2:
+ vrf = "default"
+ else:
+ vrf = vrf_parts[0]
+
+ prefix = "/".join(
+ str(i) for i in (self.rt.rt_destination, self.rt.rt_prefix_length)
+ )
+
+ structure = {
+ "vrf": vrf,
+ "prefix": prefix,
+ "count": self.rt.rt_entry_count,
+ "winning_weight": "low",
+ }
+
+ routes = []
+ for route in self.rt.rt_entry:
+ routes.append(
+ {
+ "active": route.active_tag,
+ "age": route.age,
+ "weight": route.preference,
+ "med": route.metric,
+ "local_preference": route.local_preference,
+ "as_path": route.as_path,
+ "communities": route.communities,
+ "next_hop": route.next_hop,
+ "source_as": route.source_as,
+ "source_rid": route.source_rid,
+ "peer_rid": route.peer_rid,
+ "rpki_state": route.validation_state,
+ }
+ )
+
+ serialized = ParsedRoutes(routes=routes, **structure)
+
+ log.info("Serialized Juniper response: {}", serialized)
+ return serialized
diff --git a/hyperglass/parsing/models/serialized.py b/hyperglass/parsing/models/serialized.py
new file mode 100644
index 0000000..da8b2ba
--- /dev/null
+++ b/hyperglass/parsing/models/serialized.py
@@ -0,0 +1,37 @@
+"""Device-Agnostic Parsed Response Data Model."""
+
+# Standard Library
+from typing import List
+
+# Third Party
+from pydantic import StrictInt, StrictStr, StrictBool, constr
+
+# Project
+from hyperglass.models import HyperglassModel
+
+
+class ParsedRouteEntry(HyperglassModel):
+ """Per-Route Response Model."""
+
+ active: StrictBool
+ age: StrictInt
+ weight: StrictInt
+ med: StrictInt
+ local_preference: StrictInt
+ as_path: List[StrictInt]
+ communities: List[StrictStr]
+ next_hop: StrictStr
+ source_as: StrictInt
+ source_rid: StrictStr
+ peer_rid: StrictStr
+ rpki_state: StrictInt
+
+
+class ParsedRoutes(HyperglassModel):
+ """Parsed Response Model."""
+
+ vrf: StrictStr
+ prefix: StrictStr
+ count: StrictInt = 0
+ routes: List[ParsedRouteEntry]
+ winning_weight: constr(regex=r"(low|high)")
diff --git a/hyperglass/ui/components/Card/CardFooter.js b/hyperglass/ui/components/Card/CardFooter.js
new file mode 100644
index 0000000..edf6c4e
--- /dev/null
+++ b/hyperglass/ui/components/Card/CardFooter.js
@@ -0,0 +1,25 @@
+import * as React from "react";
+import { Flex } from "@chakra-ui/core";
+
+const CardFooter = ({ children, ...props }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+CardFooter.displayName = "CardFooter";
+
+export default CardFooter;
diff --git a/hyperglass/ui/components/Card/CardHeader.js b/hyperglass/ui/components/Card/CardHeader.js
new file mode 100644
index 0000000..75627b7
--- /dev/null
+++ b/hyperglass/ui/components/Card/CardHeader.js
@@ -0,0 +1,25 @@
+import * as React from "react";
+import { Flex, Text, useColorMode } from "@chakra-ui/core";
+
+const bg = { light: "blackAlpha.50", dark: "whiteAlpha.100" };
+
+const CardHeader = ({ children, ...props }) => {
+ const { colorMode } = useColorMode();
+ return (
+
+ {children}
+
+ );
+};
+
+CardHeader.displayName = "CardHeader";
+
+export default CardHeader;
diff --git a/hyperglass/ui/components/Card/index.js b/hyperglass/ui/components/Card/index.js
new file mode 100644
index 0000000..fd99e3f
--- /dev/null
+++ b/hyperglass/ui/components/Card/index.js
@@ -0,0 +1,28 @@
+import * as React from "react";
+import { Flex, useColorMode } from "@chakra-ui/core";
+
+const bg = { light: "white", dark: "black" };
+const color = { light: "black", dark: "white" };
+
+const Card = ({ onClick = () => false, children, ...props }) => {
+ const { colorMode } = useColorMode();
+ return (
+
+ {children}
+
+ );
+};
+
+Card.displayName = "Card";
+
+export default Card;
diff --git a/hyperglass/ui/components/Header.js b/hyperglass/ui/components/Header.js
index e4ab523..6c4869b 100644
--- a/hyperglass/ui/components/Header.js
+++ b/hyperglass/ui/components/Header.js
@@ -3,158 +3,175 @@ import { Flex, IconButton, useColorMode } from "@chakra-ui/core";
import { motion, AnimatePresence } from "framer-motion";
import ResetButton from "~/components/ResetButton";
import useMedia from "~/components/MediaProvider";
-import useConfig from "~/components/HyperglassProvider";
+import useConfig, { useHyperglassState } from "~/components/HyperglassProvider";
import Title from "~/components/Title";
const AnimatedFlex = motion.custom(Flex);
const AnimatedResetButton = motion.custom(ResetButton);
const titleVariants = {
- sm: {
- fullSize: { scale: 1, marginLeft: 0 },
- smallLogo: { marginLeft: "auto" },
- smallText: { marginLeft: "auto" }
- },
- md: {
- fullSize: { scale: 1 },
- smallLogo: { scale: 0.5 },
- smallText: { scale: 0.8 }
- },
- lg: {
- fullSize: { scale: 1 },
- smallLogo: { scale: 0.5 },
- smallText: { scale: 0.8 }
- },
- xl: {
- fullSize: { scale: 1 },
- smallLogo: { scale: 0.5 },
- smallText: { scale: 0.8 }
- }
+ sm: {
+ fullSize: { scale: 1, marginLeft: 0 },
+ smallLogo: { marginLeft: "auto" },
+ smallText: { marginLeft: "auto" }
+ },
+ md: {
+ fullSize: { scale: 1 },
+ smallLogo: { scale: 0.5 },
+ smallText: { scale: 0.8 }
+ },
+ lg: {
+ fullSize: { scale: 1 },
+ smallLogo: { scale: 0.5 },
+ smallText: { scale: 0.8 }
+ },
+ xl: {
+ fullSize: { scale: 1 },
+ smallLogo: { scale: 0.5 },
+ smallText: { scale: 0.8 }
+ }
};
const icon = { light: "moon", dark: "sun" };
const bg = { light: "white", dark: "black" };
-const colorSwitch = { dark: "Switch to light mode", light: "Switch to dark mode" };
-const headerTransition = { type: "spring", ease: "anticipate", damping: 15, stiffness: 100 };
+const colorSwitch = {
+ dark: "Switch to light mode",
+ light: "Switch to dark mode"
+};
+const headerTransition = {
+ type: "spring",
+ ease: "anticipate",
+ damping: 15,
+ stiffness: 100
+};
const titleJustify = {
- true: ["flex-end", "flex-end", "center", "center"],
- false: ["flex-start", "flex-start", "center", "center"]
+ true: ["flex-end", "flex-end", "center", "center"],
+ false: ["flex-start", "flex-start", "center", "center"]
};
const titleHeight = {
- true: null,
- false: [null, "20vh", "20vh", "20vh"]
+ true: null,
+ false: [null, "20vh", "20vh", "20vh"]
};
const resetButtonMl = { true: [null, 2, 2, 2], false: null };
const widthMap = {
- text_only: "100%",
- logo_only: ["90%", "90%", "25%", "25%"],
- logo_subtitle: ["90%", "90%", "25%", "25%"],
- all: ["90%", "90%", "25%", "25%"]
+ text_only: "100%",
+ logo_only: ["90%", "90%", "25%", "25%"],
+ logo_subtitle: ["90%", "90%", "25%", "25%"],
+ all: ["90%", "90%", "25%", "25%"]
};
-export default ({ isSubmitting, handleFormReset, ...props }) => {
- const { colorMode, toggleColorMode } = useColorMode();
- const { web } = useConfig();
- const { mediaSize } = useMedia();
- const resetButton = (
-
-
-
-
-
- );
- const title = (
-
-
-
- );
- const colorModeToggle = (
-
-
-
- );
- const layout = {
- false: {
- sm: [title, resetButton, colorModeToggle],
- md: [resetButton, title, colorModeToggle],
- lg: [resetButton, title, colorModeToggle],
- xl: [resetButton, title, colorModeToggle]
- },
- true: {
- sm: [resetButton, colorModeToggle, title],
- md: [resetButton, title, colorModeToggle],
- lg: [resetButton, title, colorModeToggle],
- xl: [resetButton, title, colorModeToggle]
- }
- };
- return (
-
-
- {layout[isSubmitting][mediaSize]}
-
-
- );
+export default ({ layoutRef, ...props }) => {
+ const { colorMode, toggleColorMode } = useColorMode();
+ const { web } = useConfig();
+ const { mediaSize } = useMedia();
+ const { isSubmitting, resetForm } = useHyperglassState();
+ const handleFormReset = () => {
+ resetForm(layoutRef);
+ };
+ const resetButton = (
+
+
+
+
+
+ );
+ const title = (
+
+
+
+ );
+ const colorModeToggle = (
+
+
+
+ );
+ const layout = {
+ false: {
+ sm: [title, resetButton, colorModeToggle],
+ md: [resetButton, title, colorModeToggle],
+ lg: [resetButton, title, colorModeToggle],
+ xl: [resetButton, title, colorModeToggle]
+ },
+ true: {
+ sm: [resetButton, colorModeToggle, title],
+ md: [resetButton, title, colorModeToggle],
+ lg: [resetButton, title, colorModeToggle],
+ xl: [resetButton, title, colorModeToggle]
+ }
+ };
+ return (
+
+
+ {layout[isSubmitting][mediaSize]}
+
+
+ );
};
diff --git a/hyperglass/ui/components/HyperglassProvider.js b/hyperglass/ui/components/HyperglassProvider.js
index 98a352b..858ca04 100644
--- a/hyperglass/ui/components/HyperglassProvider.js
+++ b/hyperglass/ui/components/HyperglassProvider.js
@@ -1,7 +1,12 @@
-import React, { createContext, useContext, useMemo } from "react";
+import * as React from "react";
+import { createContext, useContext, useMemo, useState } from "react";
import dynamic from "next/dynamic";
import { CSSReset, ThemeProvider } from "@chakra-ui/core";
-import { MediaProvider } from "~/components/MediaProvider";
+import _useMedia, { MediaProvider } from "~/components/MediaProvider";
+import {
+ StateProvider,
+ useHyperglassState as _useHyperglassState
+} from "~/components/StateProvider";
import { makeTheme, defaultTheme } from "~/theme";
// Disable SSR for ColorModeProvider
@@ -21,7 +26,9 @@ export const HyperglassProvider = ({ config, children }) => {
- {children}
+
+ {children}
+
@@ -29,3 +36,7 @@ export const HyperglassProvider = ({ config, children }) => {
};
export default () => useContext(HyperglassContext);
+
+export const useHyperglassState = _useHyperglassState;
+
+export const useMedia = _useMedia;
diff --git a/hyperglass/ui/components/Layout.js b/hyperglass/ui/components/Layout.js
index 04144fc..6a0d2e6 100644
--- a/hyperglass/ui/components/Layout.js
+++ b/hyperglass/ui/components/Layout.js
@@ -1,38 +1,22 @@
-import React, { useRef, useState } from "react";
-import { Flex, useColorMode, useDisclosure } from "@chakra-ui/core";
-import { motion, AnimatePresence } from "framer-motion";
-import HyperglassForm from "~/components/HyperglassForm";
-import Results from "~/components/Results";
+import * as React from "react";
+import { useRef } from "react";
+import { Flex, useColorMode } from "@chakra-ui/core";
import Header from "~/components/Header";
import Footer from "~/components/Footer";
import Greeting from "~/components/Greeting";
import Meta from "~/components/Meta";
-import useConfig from "~/components/HyperglassProvider";
+import useConfig, { useHyperglassState } from "~/components/HyperglassProvider";
import Debugger from "~/components/Debugger";
-import useSessionStorage from "~/hooks/useSessionStorage";
-
-const AnimatedForm = motion.custom(HyperglassForm);
const bg = { light: "white", dark: "black" };
const color = { light: "black", dark: "white" };
-const Layout = () => {
+const Layout = ({ children }) => {
const config = useConfig();
const { colorMode } = useColorMode();
- const [isSubmitting, setSubmitting] = useState(false);
- const [formData, setFormData] = useState({});
- const [greetingAck, setGreetingAck] = useSessionStorage(
- "hyperglass-greeting-ack",
- false
- );
+ const { greetingAck, setGreetingAck } = useHyperglassState();
const containerRef = useRef(null);
- const handleFormReset = () => {
- containerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
- setSubmitting(false);
- setFormData({});
- };
-
return (
<>
@@ -45,10 +29,7 @@ const Layout = () => {
color={color[colorMode]}
>
-
+
{
justifyContent="start"
flexDirection="column"
>
- {isSubmitting && formData && (
-
- )}
-
- {!isSubmitting && (
-
- )}
-
+ {children}
{config.developer_mode && }
diff --git a/hyperglass/ui/components/LookingGlass.js b/hyperglass/ui/components/LookingGlass.js
new file mode 100644
index 0000000..1578918
--- /dev/null
+++ b/hyperglass/ui/components/LookingGlass.js
@@ -0,0 +1,51 @@
+import * as React from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import Layout from "~/components/Layout";
+import HyperglassForm from "~/components/HyperglassForm";
+import Results from "~/components/Results";
+import { useHyperglassState } from "~/components/HyperglassProvider";
+
+const AnimatedForm = motion.custom(HyperglassForm);
+
+const LookingGlass = () => {
+ const {
+ isSubmitting,
+ setSubmitting,
+ formData,
+ setFormData,
+ greetingAck,
+ setGreetingAck
+ } = useHyperglassState();
+
+ return (
+
+ {isSubmitting && formData && (
+
+ )}
+
+ {!isSubmitting && (
+
+ )}
+
+
+ );
+};
+
+LookingGlass.displayName = "LookingGlass";
+export default LookingGlass;
diff --git a/hyperglass/ui/components/MediaProvider.js b/hyperglass/ui/components/MediaProvider.js
index 6dc0c51..8af5cb3 100644
--- a/hyperglass/ui/components/MediaProvider.js
+++ b/hyperglass/ui/components/MediaProvider.js
@@ -4,31 +4,39 @@ import { useMediaLayout } from "use-media";
const MediaContext = createContext(null);
export const MediaProvider = ({ theme, children }) => {
- const { sm, md, lg, xl } = theme.breakpoints;
- const isSm = useMediaLayout({ maxWidth: md });
- const isMd = useMediaLayout({ minWidth: md, maxWidth: lg });
- const isLg = useMediaLayout({ minWidth: lg, maxWidth: xl });
- const isXl = useMediaLayout({ minWidth: xl });
- let mediaSize = false;
- switch (true) {
- case isSm:
- mediaSize = "sm";
- break;
- case isMd:
- mediaSize = "md";
- break;
- case isLg:
- mediaSize = "lg";
- break;
- case isXl:
- mediaSize = "xl";
- break;
- }
- const value = useMemo(
- () => ({ isSm: isSm, isMd: isMd, isLg: isLg, isXl: isXl, mediaSize: mediaSize }),
- [isSm, isMd, isLg, isXl, mediaSize]
- );
- return {children};
+ const { sm, md, lg, xl } = theme.breakpoints;
+ const isSm = useMediaLayout({ maxWidth: md });
+ const isMd = useMediaLayout({ minWidth: md, maxWidth: lg });
+ const isLg = useMediaLayout({ minWidth: lg, maxWidth: xl });
+ const isXl = useMediaLayout({ minWidth: xl });
+ let mediaSize = false;
+ switch (true) {
+ case isSm:
+ mediaSize = "sm";
+ break;
+ case isMd:
+ mediaSize = "md";
+ break;
+ case isLg:
+ mediaSize = "lg";
+ break;
+ case isXl:
+ mediaSize = "xl";
+ break;
+ }
+ const value = useMemo(
+ () => ({
+ isSm: isSm,
+ isMd: isMd,
+ isLg: isLg,
+ isXl: isXl,
+ mediaSize: mediaSize
+ }),
+ [isSm, isMd, isLg, isXl, mediaSize]
+ );
+ return (
+ {children}
+ );
};
export default () => useContext(MediaContext);
diff --git a/hyperglass/ui/components/StateProvider.js b/hyperglass/ui/components/StateProvider.js
new file mode 100644
index 0000000..47a1d61
--- /dev/null
+++ b/hyperglass/ui/components/StateProvider.js
@@ -0,0 +1,33 @@
+import * as React from "react";
+import { createContext, useContext, useMemo, useState } from "react";
+import useSessionStorage from "~/hooks/useSessionStorage";
+
+const StateContext = createContext(null);
+
+export const StateProvider = ({ children }) => {
+ const [isSubmitting, setSubmitting] = useState(false);
+ const [formData, setFormData] = useState({});
+ const [greetingAck, setGreetingAck] = useSessionStorage(
+ "hyperglass-greeting-ack",
+ false
+ );
+ const resetForm = layoutRef => {
+ layoutRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
+ setSubmitting(false);
+ setFormData({});
+ };
+ const value = useMemo(() => ({
+ isSubmitting,
+ setSubmitting,
+ formData,
+ setFormData,
+ greetingAck,
+ setGreetingAck,
+ resetForm
+ }));
+ return (
+ {children}
+ );
+};
+
+export const useHyperglassState = () => useContext(StateContext);
diff --git a/hyperglass/ui/components/Table/MainTable.js b/hyperglass/ui/components/Table/MainTable.js
new file mode 100644
index 0000000..af9b236
--- /dev/null
+++ b/hyperglass/ui/components/Table/MainTable.js
@@ -0,0 +1,39 @@
+/*@jsx jsx*/
+import { jsx } from "@emotion/core";
+import { Box, css, useTheme, useColorMode } from "@chakra-ui/core";
+
+const scrollbar = { dark: "whiteAlpha.300", light: "blackAlpha.300" };
+const scrollbarHover = { dark: "whiteAlpha.400", light: "blackAlpha.400" };
+const scrollbarBg = { dark: "whiteAlpha.50", light: "blackAlpha.50" };
+
+const MainTable = ({ children, ...props }) => {
+ const theme = useTheme();
+ const { colorMode } = useColorMode();
+ return (
+
+ {children}
+
+ );
+};
+
+MainTable.displayName = "MainTable";
+export default MainTable;
diff --git a/hyperglass/ui/components/Table/TableBody.js b/hyperglass/ui/components/Table/TableBody.js
new file mode 100644
index 0000000..28d9e68
--- /dev/null
+++ b/hyperglass/ui/components/Table/TableBody.js
@@ -0,0 +1,24 @@
+import * as React from "react";
+import { Box } from "@chakra-ui/core";
+import css from "@styled-system/css";
+
+const TableBody = ({ children, ...props }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+TableBody.displayName = "TableBody";
+
+export default TableBody;
diff --git a/hyperglass/ui/components/Table/TableCell.js b/hyperglass/ui/components/Table/TableCell.js
new file mode 100644
index 0000000..71cc783
--- /dev/null
+++ b/hyperglass/ui/components/Table/TableCell.js
@@ -0,0 +1,52 @@
+import * as React from "react";
+import { Box, useColorMode } from "@chakra-ui/core";
+
+// export const TableCell = styled("div")`
+// ${space};
+// ${color};
+// ${justifyContent};
+// flex: 1;
+// display: flex;
+// min-width: 150px;
+// align-items: center;
+// border-bottom-width: 1px;
+// overflow: hidden;
+// text-overflow: ellipsis;
+// `;
+
+const cellBorder = {
+ dark: { borderLeft: "1px", borderLeftColor: "whiteAlpha.100" },
+ light: { borderLeft: "1px", borderLeftColor: "blackAlpha.100" }
+};
+
+const TableCell = ({
+ bordersVertical = [false, 0, 0],
+ align,
+ cell,
+ children,
+ ...props
+}) => {
+ const { colorMode } = useColorMode();
+ const [doVerticalBorders, index] = bordersVertical;
+ let borderProps = {};
+ if (doVerticalBorders && index !== 0) {
+ borderProps = cellBorder[colorMode];
+ }
+ return (
+
+ {children}
+
+ );
+};
+
+TableCell.displayName = "TableCell";
+
+export default TableCell;
diff --git a/hyperglass/ui/components/Table/TableHead.js b/hyperglass/ui/components/Table/TableHead.js
new file mode 100644
index 0000000..1b49a7b
--- /dev/null
+++ b/hyperglass/ui/components/Table/TableHead.js
@@ -0,0 +1,29 @@
+import * as React from "react";
+import { Box, useColorMode } from "@chakra-ui/core";
+
+// export const TableHead = styled.div`
+// ${space};
+// display: flex;
+// flex-direction: row;
+// `;
+
+const bg = { dark: "whiteAlpha.100", light: "blackAlpha.100" };
+
+const TableHead = ({ children, ...props }) => {
+ const { colorMode } = useColorMode();
+ return (
+
+ {children}
+
+ );
+};
+
+TableHead.displayName = "TableHead";
+
+export default TableHead;
diff --git a/hyperglass/ui/components/Table/TableIconButton.js b/hyperglass/ui/components/Table/TableIconButton.js
new file mode 100644
index 0000000..91eadfa
--- /dev/null
+++ b/hyperglass/ui/components/Table/TableIconButton.js
@@ -0,0 +1,51 @@
+import * as React from "react";
+import { IconButton } from "@chakra-ui/core";
+
+// export const TableIconButton = ({ icon, onClick, isDisabled, children, variantColor, ...rest }) => {
+// return (
+//
+// {children}
+//
+// );
+// };
+
+// TableIconButton.defaultProps = {
+// variantColor: "gray",
+// };
+
+const TableIconButton = ({
+ icon,
+ onClick,
+ isDisabled,
+ color,
+ children,
+ ...props
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+TableIconButton.displayName = "TableIconButton";
+
+export default TableIconButton;
diff --git a/hyperglass/ui/components/Table/TableRow.js b/hyperglass/ui/components/Table/TableRow.js
new file mode 100644
index 0000000..8407793
--- /dev/null
+++ b/hyperglass/ui/components/Table/TableRow.js
@@ -0,0 +1,65 @@
+import * as React from "react";
+import { PseudoBox, useColorMode, useTheme } from "@chakra-ui/core";
+import { opposingColor } from "~/util";
+
+// export const TableRow = styled(Flex)`
+// &:hover {
+// cursor: pointer;
+// background-color: rgba(0, 0, 0, 0.01);
+// }
+// `;
+
+const hoverBg = { dark: "whiteAlpha.100", light: "blackAlpha.100" };
+const bgStripe = { dark: "whiteAlpha.50", light: "blackAlpha.50" };
+const rowBorder = {
+ dark: { borderTop: "1px", borderTopColor: "whiteAlpha.100" },
+ light: { borderTop: "1px", borderTopColor: "blackAlpha.100" }
+};
+const alphaMap = { dark: "200", light: "100" };
+const alphaMapHover = { dark: "100", light: "200" };
+
+const TableRow = ({
+ highlight = false,
+ highlightBg = "primary",
+ doStripe = false,
+ doHorizontalBorders = false,
+ index = 0,
+ children = false,
+ ...props
+}) => {
+ const { colorMode } = useColorMode();
+ const theme = useTheme();
+
+ let bg = null;
+ if (highlight) {
+ bg = `${highlightBg}.${alphaMap[colorMode]}`;
+ } else if (doStripe && index % 2 !== 0) {
+ bg = bgStripe[colorMode];
+ }
+ const color = highlight ? opposingColor(theme, bg) : null;
+
+ const borderProps =
+ doHorizontalBorders && index !== 0 ? rowBorder[colorMode] : {};
+ return (
+
+ {children}
+
+ );
+};
+
+TableRow.displayName = "TableRow";
+
+export default TableRow;
diff --git a/hyperglass/ui/components/Table/TableSelectShow.js b/hyperglass/ui/components/Table/TableSelectShow.js
new file mode 100644
index 0000000..a61040f
--- /dev/null
+++ b/hyperglass/ui/components/Table/TableSelectShow.js
@@ -0,0 +1,32 @@
+import * as React from "react";
+import { Select } from "@chakra-ui/core";
+
+{
+ /* */
+}
+
+const TableSelectShow = ({ value, onChange, children, ...props }) => {
+ return (
+
+ );
+};
+
+TableSelectShow.displayName = "TableSelectShow";
+
+export default TableSelectShow;
diff --git a/hyperglass/ui/components/Table/index.js b/hyperglass/ui/components/Table/index.js
new file mode 100644
index 0000000..784c3f9
--- /dev/null
+++ b/hyperglass/ui/components/Table/index.js
@@ -0,0 +1,191 @@
+import * as React from "react";
+import { useMemo } from "react";
+import { Flex, Icon, Text } from "@chakra-ui/core";
+import useMedia from "~/components/MediaProvider";
+import { usePagination, useSortBy, useTable } from "react-table";
+import Card from "~/components/Card";
+import BottomSection from "~/components/Card/CardFooter";
+import TopSection from "~/components/Card/CardHeader";
+import MainTable from "./MainTable";
+import TableCell from "./TableCell";
+import TableHead from "./TableHead";
+import TableRow from "./TableRow";
+import TableBody from "./TableBody";
+import TableIconButton from "./TableIconButton";
+import TableSelectShow from "./TableSelectShow";
+
+const Table = ({
+ columns,
+ data,
+ tableHeading,
+ initialPageSize = 10,
+ onRowClick,
+ striped = false,
+ bordersVertical = false,
+ bordersHorizontal = false,
+ cellRender = null,
+ rowHighlightProp,
+ rowHighlightBg,
+ rowHighlightColor
+}) => {
+ const tableColumns = useMemo(() => columns, [columns]);
+
+ const { isSm, isMd } = useMedia();
+
+ const isTabletOrMobile = isSm ? true : isMd ? true : false;
+
+ const defaultColumn = useMemo(
+ () => ({
+ minWidth: 100,
+ width: 150,
+ maxWidth: 300
+ }),
+ []
+ );
+
+ const {
+ getTableProps,
+ headerGroups,
+ prepareRow,
+ page,
+ canPreviousPage,
+ canNextPage,
+ pageOptions,
+ pageCount,
+ gotoPage,
+ nextPage,
+ previousPage,
+ setPageSize,
+ state: { pageIndex, pageSize }
+ } = useTable(
+ {
+ columns: tableColumns,
+ defaultColumn,
+ data,
+ initialState: { pageIndex: 0, pageSize: initialPageSize }
+ },
+ useSortBy,
+ usePagination
+ );
+
+ return (
+
+ {!!tableHeading && {tableHeading}}
+
+
+ {headerGroups.map(headerGroup => (
+
+ {headerGroup.headers.map(column => (
+
+
+ {column.render("Header")}
+
+ {column.isSorted ? (
+ column.isSortedDesc ? (
+
+ ) : (
+
+ )
+ ) : (
+ ""
+ )}
+
+ ))}
+
+ ))}
+
+
+ {page.map(
+ (row, key) =>
+ prepareRow(row) || (
+ onRowClick && onRowClick(row)}
+ key={key}
+ highlight={row.values[rowHighlightProp] ?? false}
+ highlightBg={rowHighlightBg}
+ highlightColor={rowHighlightColor}
+ {...row.getRowProps()}
+ >
+ {row.cells.map((cell, i) => {
+ return (
+
+ {cell.render(cellRender ?? "Cell")}
+
+ );
+ })}
+
+ )
+ )}
+
+
+
+
+ gotoPage(0)}
+ isDisabled={!canPreviousPage}
+ icon={() => }
+ />
+ previousPage()}
+ isDisabled={!canPreviousPage}
+ icon={() => }
+ />
+
+
+
+ Page{" "}
+
+ {pageIndex + 1} of {pageOptions.length}
+ {" "}
+
+ {!isTabletOrMobile && (
+ {
+ setPageSize(Number(e.target.value));
+ }}
+ />
+ )}
+
+
+ nextPage()}
+ icon={() => }
+ />
+ gotoPage(pageCount ? pageCount - 1 : 1)}
+ isDisabled={!canNextPage}
+ icon={() => }
+ />
+
+
+
+ );
+};
+
+Table.displayName = "Table";
+
+export default Table;
diff --git a/hyperglass/ui/components/Table/makeData.js b/hyperglass/ui/components/Table/makeData.js
new file mode 100644
index 0000000..d45081f
--- /dev/null
+++ b/hyperglass/ui/components/Table/makeData.js
@@ -0,0 +1,35 @@
+import { generate } from "namor";
+
+const range = (len) => {
+ const arr = [];
+ for (let i = 0; i < len; i++) {
+ arr.push(i);
+ }
+ return arr;
+};
+
+export const newPerson = () => {
+ const statusChance = Math.random();
+ return {
+ name: generate({ words: 2, numbers: 0 }),
+ age: Math.floor(Math.random() * 30),
+ visits: Math.floor(Math.random() * 100),
+ progress: Math.floor(Math.random() * 100),
+ status:
+ statusChance > 0.66 ? "relationship" : statusChance > 0.33 ? "complicated" : "single",
+ };
+};
+
+export default function makeData(...lens) {
+ const makeDataLevel = (depth = 0) => {
+ const len = lens[depth];
+ return range(len).map((d) => {
+ return {
+ ...newPerson(),
+ subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
+ };
+ });
+ };
+
+ return makeDataLevel();
+}
diff --git a/hyperglass/ui/components/Table/styles.js b/hyperglass/ui/components/Table/styles.js
new file mode 100644
index 0000000..fa3eef5
--- /dev/null
+++ b/hyperglass/ui/components/Table/styles.js
@@ -0,0 +1,70 @@
+import React from "react";
+import { Flex, IconButton } from "@chakra-ui/core";
+import styled from "@emotion/styled";
+import {
+ color,
+ ColorProps,
+ justifyContent,
+ JustifyContentProps,
+ space,
+ SpaceProps,
+} from "styled-system";
+
+export const StyledTable = styled.div`
+ ${space};
+ flex: 1;
+ width: 100%;
+ display: flex;
+ max-width: 100%;
+ overflow-x: auto;
+ border-radius: 4px;
+ flex-direction: column;
+ box-sizing: border-box;
+`;
+
+export const TableHead = styled.div`
+ ${space};
+ display: flex;
+ flex-direction: row;
+`;
+
+export const TableCell = styled("div")`
+ ${space};
+ ${color};
+ ${justifyContent};
+ flex: 1;
+ display: flex;
+ min-width: 150px;
+ align-items: center;
+ border-bottom-width: 1px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+
+export const TableRow = styled(Flex)`
+ &:hover {
+ cursor: pointer;
+ background-color: rgba(0, 0, 0, 0.01);
+ }
+`;
+
+export const TableIconButton = ({ icon, onClick, isDisabled, children, variantColor, ...rest }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+TableIconButton.defaultProps = {
+ variantColor: "gray",
+};
diff --git a/hyperglass/ui/package.json b/hyperglass/ui/package.json
index eac339b..5a564ba 100644
--- a/hyperglass/ui/package.json
+++ b/hyperglass/ui/package.json
@@ -18,9 +18,11 @@
"axios": "^0.19.2",
"axios-hooks": "^1.9.0",
"chroma-js": "^2.1.0",
+ "dayjs": "^1.8.25",
"emotion-theming": "^10.0.27",
"framer-motion": "^1.10.0",
"lodash": "^4.17.15",
+ "namor": "^2.0.2",
"next": "^9.3.1",
"react": "^16.13.1",
"react-countdown": "^2.2.1",
@@ -30,6 +32,7 @@
"react-markdown": "^4.3.1",
"react-select": "^3.0.8",
"react-string-replace": "^0.4.4",
+ "react-table": "^7.0.4",
"react-textfit": "^1.1.0",
"string-format": "^2.0.0",
"styled-system": "^5.1.5",
diff --git a/hyperglass/ui/pages/index.js b/hyperglass/ui/pages/index.js
index c19ceae..6949270 100644
--- a/hyperglass/ui/pages/index.js
+++ b/hyperglass/ui/pages/index.js
@@ -1,8 +1,10 @@
-import React from "react";
+import * as React from "react";
import dynamic from "next/dynamic";
import Loading from "~/components/Loading";
-const Layout = dynamic(() => import("~/components/Layout"), { loading: Loading });
+const LookingGlass = dynamic(() => import("~/components/LookingGlass"), {
+ loading: Loading
+});
-const Index = () => ;
+const Index = () => ;
export default Index;
diff --git a/hyperglass/ui/pages/structured.js b/hyperglass/ui/pages/structured.js
new file mode 100644
index 0000000..9fb7142
--- /dev/null
+++ b/hyperglass/ui/pages/structured.js
@@ -0,0 +1,336 @@
+import * as React from "react";
+import {
+ Flex,
+ Icon,
+ Popover,
+ PopoverArrow,
+ PopoverContent,
+ PopoverTrigger,
+ Text,
+ Tooltip,
+ useColorMode
+} from "@chakra-ui/core";
+import dayjs from "dayjs";
+import relativeTimePlugin from "dayjs/plugin/relativeTime";
+import utcPlugin from "dayjs/plugin/utc";
+import useConfig from "~/components/HyperglassProvider";
+import Layout from "~/components/Layout";
+import Table from "~/components/Table/index";
+
+dayjs.extend(relativeTimePlugin);
+dayjs.extend(utcPlugin);
+
+const data = {
+ vrf: "default",
+ prefix: "1.1.1.0/24",
+ count: 4,
+ routes: [
+ {
+ active: true,
+ age: 578857,
+ weight: 170,
+ med: 0,
+ local_preference: 150,
+ as_path: [1299, 13335],
+ communities: [
+ "1299:35000",
+ "14525:0",
+ "14525:40",
+ "14525:1021",
+ "14525:2840",
+ "14525:3001",
+ "14525:4001",
+ "14525:9003"
+ ],
+ next_hop: "62.115.189.136",
+ source_as: 13335,
+ source_rid: "162.158.140.1",
+ peer_rid: "2.255.254.51",
+ rpki_state: 1
+ },
+ {
+ active: false,
+ age: 787213,
+ weight: 170,
+ med: 2020,
+ local_preference: 150,
+ as_path: [174, 13335],
+ communities: [
+ "174:21001",
+ "174:22013",
+ "14525:0",
+ "14525:20",
+ "14525:1021",
+ "14525:2840",
+ "14525:3001",
+ "14525:4001",
+ "14525:9001"
+ ],
+ next_hop: "100.64.0.122",
+ source_as: 13335,
+ source_rid: "162.158.140.1",
+ peer_rid: "199.34.92.1",
+ rpki_state: 0
+ },
+ {
+ active: false,
+ age: 616677,
+ weight: 200,
+ med: 0,
+ local_preference: 150,
+ as_path: [6939, 13335],
+ communities: [
+ "6939:7107",
+ "6939:8840",
+ "6939:9001",
+ "14525:0",
+ "14525:40",
+ "14525:1021",
+ "14525:2840",
+ "14525:3002",
+ "14525:4003",
+ "14525:9002"
+ ],
+ next_hop: "100.64.0.122",
+ source_as: 13335,
+ source_rid: "172.68.129.1",
+ peer_rid: "199.34.92.6",
+ rpki_state: 2
+ },
+ {
+ active: false,
+ age: 1284244,
+ weight: 200,
+ med: 25090,
+ local_preference: 150,
+ as_path: [174, 13335],
+ communities: [],
+ next_hop: "100.64.0.122",
+ source_as: 13335,
+ source_rid: "108.162.239.1",
+ peer_rid: "199.34.92.7",
+ rpki_state: 3
+ }
+ ],
+ winning_weight: "low"
+};
+
+const hiddenCols = ["active", "source_as"];
+
+const isActiveColor = {
+ true: { dark: "green.300", light: "green.500" },
+ false: { dark: "gray.300", light: "gray.500" }
+};
+
+const arrowColor = {
+ true: { dark: "blackAlpha.500", light: "blackAlpha.500" },
+ false: { dark: "whiteAlpha.300", light: "blackAlpha.500" }
+};
+
+const rpkiIcon = ["not-allowed", "check-circle", "warning", "question"];
+
+const rpkiColor = {
+ true: {
+ dark: ["red.500", "green.600", "yellow.500", "gray.800"],
+ light: ["red.500", "green.500", "yellow.500", "gray.600"]
+ },
+ false: {
+ dark: ["red.300", "green.300", "yellow.300", "gray.300"],
+ light: ["red.400", "green.500", "yellow.400", "gray.500"]
+ }
+};
+
+const makeColumns = fields => {
+ return fields.map(pair => {
+ const [header, accessor, align] = pair;
+ return { Header: header, accessor: accessor, align: align };
+ });
+};
+
+const longestASNLength = asPath => {
+ const longest = asPath.reduce((l, c) => {
+ const strLongest = String(l);
+ const strCurrent = String(c);
+ return strCurrent.length > strLongest.length ? strCurrent : strLongest;
+ });
+ return longest.length;
+};
+
+const MonoField = ({ v, ...props }) => (
+
+ {v}
+
+);
+
+const Active = ({ isActive }) => {
+ const { colorMode } = useColorMode();
+ return (
+
+ );
+};
+
+const Age = ({ inSeconds }) => {
+ const now = dayjs.utc();
+ const then = now.subtract(inSeconds, "seconds");
+ return (
+
+ {now.to(then, true)}
+
+ );
+};
+
+const Weight = ({ weight, winningWeight }) => {
+ const fixMeText =
+ winningWeight === "low"
+ ? "Lower Weight is Preferred"
+ : "Higher Weight is Preferred";
+ return (
+
+
+ {weight}
+
+
+ );
+};
+
+const ASPath = ({ path, active, longestASN }) => {
+ const { colorMode } = useColorMode();
+ let paths = [];
+ path.map((asn, i) => {
+ const asnStr = String(asn);
+ i !== 0 &&
+ paths.push(
+
+ );
+ paths.push(
+
+ {asnStr}
+
+ );
+ });
+ return paths;
+};
+
+const Communities = ({ communities }) => {
+ const { colorMode } = useColorMode();
+ let component;
+ communities.length === 0
+ ? (component = (
+
+
+
+ ))
+ : (component = (
+
+
+
+
+
+
+ {communities.map(c => (
+
+ ))}
+
+
+ ));
+ return component;
+};
+
+const RPKIState = ({ state, active }) => {
+ const { web } = useConfig();
+ const { colorMode } = useColorMode();
+ const stateText = [
+ web.text.rpki_invalid,
+ web.text.rpki_valid,
+ web.text.rpki_unknown,
+ web.text.rpki_unverified
+ ];
+ return (
+
+
+
+ );
+};
+
+const Cell = ({ data, rawData, longestASN }) => {
+ hiddenCols.includes(data.column.id) &&
+ data.setHiddenColumns(old => [...old, data.column.id]);
+ const component = {
+ active: ,
+ age: ,
+ weight: (
+
+ ),
+ med: ,
+ local_preference: ,
+ as_path: (
+
+ ),
+ communities: ,
+ next_hop: ,
+ source_as: ,
+ source_rid: ,
+ peer_rid: ,
+ rpki_state:
+ };
+ return component[data.column.id] ?? <> >;
+};
+
+const Structured = () => {
+ const config = useConfig();
+ const columns = makeColumns(config.parsed_data_fields);
+ const allASN = data.routes.map(r => r.as_path).flat();
+ const asLength = longestASNLength(allASN);
+
+ return (
+
+
+ (
+ |
+ )}
+ bordersHorizontal
+ rowHighlightBg="green"
+ />
+
+
+ );
+};
+
+export default Structured;
diff --git a/hyperglass/ui/theme.js b/hyperglass/ui/theme.js
index 8dd4961..d6123ca 100644
--- a/hyperglass/ui/theme.js
+++ b/hyperglass/ui/theme.js
@@ -2,238 +2,159 @@ import { theme as chakraTheme } from "@chakra-ui/core";
import chroma from "chroma-js";
const alphaColors = color => ({
- 900: chroma(color)
- .alpha(0.92)
- .css(),
- 800: chroma(color)
- .alpha(0.8)
- .css(),
- 700: chroma(color)
- .alpha(0.6)
- .css(),
- 600: chroma(color)
- .alpha(0.48)
- .css(),
- 500: chroma(color)
- .alpha(0.38)
- .css(),
- 400: chroma(color)
- .alpha(0.24)
- .css(),
- 300: chroma(color)
- .alpha(0.16)
- .css(),
- 200: chroma(color)
- .alpha(0.12)
- .css(),
- 100: chroma(color)
- .alpha(0.08)
- .css(),
- 50: chroma(color)
- .alpha(0.04)
- .css()
+ 900: chroma(color)
+ .alpha(0.92)
+ .css(),
+ 800: chroma(color)
+ .alpha(0.8)
+ .css(),
+ 700: chroma(color)
+ .alpha(0.6)
+ .css(),
+ 600: chroma(color)
+ .alpha(0.48)
+ .css(),
+ 500: chroma(color)
+ .alpha(0.38)
+ .css(),
+ 400: chroma(color)
+ .alpha(0.24)
+ .css(),
+ 300: chroma(color)
+ .alpha(0.16)
+ .css(),
+ 200: chroma(color)
+ .alpha(0.12)
+ .css(),
+ 100: chroma(color)
+ .alpha(0.08)
+ .css(),
+ 50: chroma(color)
+ .alpha(0.04)
+ .css()
});
const generateColors = colorInput => {
- const colorMap = {};
+ const colorMap = {};
- const lightnessMap = [0.95, 0.85, 0.75, 0.65, 0.55, 0.45, 0.35, 0.25, 0.15, 0.05];
- const saturationMap = [0.32, 0.16, 0.08, 0.04, 0, 0, 0.04, 0.08, 0.16, 0.32];
+ const lightnessMap = [
+ 0.95,
+ 0.85,
+ 0.75,
+ 0.65,
+ 0.55,
+ 0.45,
+ 0.35,
+ 0.25,
+ 0.15,
+ 0.05
+ ];
+ const saturationMap = [0.32, 0.16, 0.08, 0.04, 0, 0, 0.04, 0.08, 0.16, 0.32];
- const validColor = chroma.valid(colorInput.trim()) ? chroma(colorInput.trim()) : chroma("#000");
+ const validColor = chroma.valid(colorInput.trim())
+ ? chroma(colorInput.trim())
+ : chroma("#000");
- const lightnessGoal = validColor.get("hsl.l");
- const closestLightness = lightnessMap.reduce((prev, curr) =>
- Math.abs(curr - lightnessGoal) < Math.abs(prev - lightnessGoal) ? curr : prev
- );
+ const lightnessGoal = validColor.get("hsl.l");
+ const closestLightness = lightnessMap.reduce((prev, curr) =>
+ Math.abs(curr - lightnessGoal) < Math.abs(prev - lightnessGoal)
+ ? curr
+ : prev
+ );
- const baseColorIndex = lightnessMap.findIndex(l => l === closestLightness);
+ const baseColorIndex = lightnessMap.findIndex(l => l === closestLightness);
- const colors = lightnessMap
- .map(l => validColor.set("hsl.l", l))
- .map(color => chroma(color))
- .map((color, i) => {
- const saturationDelta = saturationMap[i] - saturationMap[baseColorIndex];
- return saturationDelta >= 0
- ? color.saturate(saturationDelta)
- : color.desaturate(saturationDelta * -1);
- });
-
- const getColorNumber = index => (index === 0 ? 50 : index * 100);
-
- colors.map((color, i) => {
- const colorIndex = getColorNumber(i);
- colorMap[colorIndex] = color.hex();
+ const colors = lightnessMap
+ .map(l => validColor.set("hsl.l", l))
+ .map(color => chroma(color))
+ .map((color, i) => {
+ const saturationDelta = saturationMap[i] - saturationMap[baseColorIndex];
+ return saturationDelta >= 0
+ ? color.saturate(saturationDelta)
+ : color.desaturate(saturationDelta * -1);
});
- return colorMap;
+
+ const getColorNumber = index => (index === 0 ? 50 : index * 100);
+
+ colors.map((color, i) => {
+ const colorIndex = getColorNumber(i);
+ colorMap[colorIndex] = color.hex();
+ });
+ return colorMap;
};
-// const defaultBasePalette = {
-// black: "#262626",
-// white: "#f7f7f7",
-// gray: "#c1c7cc",
-// red: "#d84b4b",
-// orange: "ff6b35",
-// yellow: "#edae49",
-// green: "#35b246",
-// blue: "#314cb6",
-// teal: "#35b299",
-// cyan: "#118ab2",
-// pink: "#f2607d",
-// purple: "#8d30b5"
-// };
-
-// const defaultSwatchPalette = {
-// black: defaultBasePalette.black,
-// white: defaultBasePalette.white,
-// gray: generateColors(defaultBasePalette.gray),
-// red: generateColors(defaultBasePalette.red),
-// orange: generateColors(defaultBasePalette.orange),
-// yellow: generateColors(defaultBasePalette.yellow),
-// green: generateColors(defaultBasePalette.green),
-// blue: generateColors(defaultBasePalette.blue),
-// teal: generateColors(defaultBasePalette.teal),
-// cyan: generateColors(defaultBasePalette.cyan),
-// pink: generateColors(defaultBasePalette.pink),
-// purple: generateColors(defaultBasePalette.purple)
-// };
-
-// const defaultAlphaPalette = {
-// blackAlpha: alphaColors(defaultBasePalette.black),
-// whiteAlpha: alphaColors(defaultBasePalette.white)
-// };
-
-// const defaultFuncSwatchPalette = {
-// primary: generateColors(defaultBasePalette.cyan),
-// secondary: generateColors(defaultBasePalette.blue),
-// dark: generateColors(defaultBasePalette.black),
-// light: generateColors(defaultBasePalette.white),
-// success: generateColors(defaultBasePalette.green),
-// warning: generateColors(defaultBasePalette.yellow),
-// error: generateColors(defaultBasePalette.orange),
-// danger: generateColors(defaultBasePalette.red)
-// };
-
-// const defaultColors = {
-// transparent: "transparent",
-// current: "currentColor",
-// ...defaultFuncSwatchPalette,
-// ...defaultAlphaPalette,
-// ...defaultSwatchPalette
-// };
-
const defaultBodyFonts = [
- // "Nunito",
- "-apple-system",
- "BlinkMacSystemFont",
- '"Segoe UI"',
- "Helvetica",
- "Arial",
- "sans-serif",
- '"Apple Color Emoji"',
- '"Segoe UI Emoji"',
- '"Segoe UI Symbol"'
+ "-apple-system",
+ "BlinkMacSystemFont",
+ '"Segoe UI"',
+ "Helvetica",
+ "Arial",
+ "sans-serif",
+ '"Apple Color Emoji"',
+ '"Segoe UI Emoji"',
+ '"Segoe UI Symbol"'
];
const defaultMonoFonts = [
- // '"Fira Code"',
- "SFMono-Regular",
- "Melno",
- "Monaco",
- "Consolas",
- '"Liberation Mono"',
- '"Courier New"',
- "monospace"
+ "SFMono-Regular",
+ "Melno",
+ "Monaco",
+ "Consolas",
+ '"Liberation Mono"',
+ '"Courier New"',
+ "monospace"
];
-// const defaultFonts = {
-// body: defaultBodyFonts.join(", "),
-// heading: defaultBodyFonts.join(", "),
-// mono: defaultMonoFonts.join(", ")
-// };
-
-// const defaultTheme = {
-// ...chakraTheme,
-// colors: defaultColors,
-// fonts: defaultFonts
-// };
-
const generatePalette = palette => {
- const generatedPalette = {};
- Object.keys(palette).map(color => {
- if (!["black", "white"].includes(color)) {
- generatedPalette[color] = generateColors(palette[color]);
- } else {
- generatedPalette[color] = palette[color];
- generatedPalette[`${color}Alpha`] = alphaColors(palette[color]);
- }
- });
- return generatedPalette;
+ const generatedPalette = {};
+ Object.keys(palette).map(color => {
+ if (!["black", "white"].includes(color)) {
+ generatedPalette[color] = generateColors(palette[color]);
+ } else {
+ generatedPalette[color] = palette[color];
+ generatedPalette[`${color}Alpha`] = alphaColors(palette[color]);
+ }
+ });
+ return generatedPalette;
};
-// const generateFuncPalette = palette => ({
-// primary: generateColors(palette.cyan),
-// secondary: generateColors(palette.blue),
-// dark: generateColors(palette.black),
-// light: generateColors(palette.white),
-// success: generateColors(palette.green),
-// warning: generateColors(palette.yellow),
-// error: generateColors(palette.orange),
-// danger: generateColors(palette.red)
-// });
-
-// const generateAlphaPalette = palette => ({
-// blackAlpha: alphaColors(palette.black),
-// whiteAlpha: alphaColors(palette.white)
-// });
-
const formatFont = font => {
- const fontList = font.split(" ");
- const fontFmt = fontList.length >= 2 ? `'${fontList.join(" ")}'` : fontList.join(" ");
- return fontFmt;
+ const fontList = font.split(" ");
+ const fontFmt =
+ fontList.length >= 2 ? `'${fontList.join(" ")}'` : fontList.join(" ");
+ return fontFmt;
};
const importFonts = userFonts => {
- const [body, mono] = [defaultBodyFonts, defaultMonoFonts];
- const bodyFmt = formatFont(userFonts.body);
- const monoFmt = formatFont(userFonts.mono);
- if (userFonts.body && !body.includes(bodyFmt)) {
- body.unshift(bodyFmt);
- }
- if (userFonts.mono && !mono.includes(monoFmt)) {
- mono.unshift(monoFmt);
- }
- return {
- body: body.join(", "),
- heading: body.join(", "),
- mono: mono.join(", ")
- };
+ const [body, mono] = [defaultBodyFonts, defaultMonoFonts];
+ const bodyFmt = formatFont(userFonts.body);
+ const monoFmt = formatFont(userFonts.mono);
+ if (userFonts.body && !body.includes(bodyFmt)) {
+ body.unshift(bodyFmt);
+ }
+ if (userFonts.mono && !mono.includes(monoFmt)) {
+ mono.unshift(monoFmt);
+ }
+ return {
+ body: body.join(", "),
+ heading: body.join(", "),
+ mono: mono.join(", ")
+ };
};
const importColors = (userColors = {}) => {
- // const baseColors = {
- // ...defaultBasePalette,
- // ...userColors
- // };
-
- const generatedColors = generatePalette(userColors);
- // const swatchColors = generatePalette(baseColors);
- // const funcColors = generateFuncPalette(baseColors);
- // const bwAlphaColors = generateAlphaPalette(userColors);
- return {
- transparent: "transparent",
- current: "currentColor",
- // ...swatchColors,
- // ...funcColors,
- ...generatedColors
- // ...bwAlphaColors
- };
+ const generatedColors = generatePalette(userColors);
+ return {
+ transparent: "transparent",
+ current: "currentColor",
+ ...generatedColors
+ };
};
const makeTheme = userTheme => ({
- ...chakraTheme,
- colors: importColors(userTheme.colors),
- fonts: importFonts(userTheme.fonts)
+ ...chakraTheme,
+ colors: importColors(userTheme.colors),
+ fonts: importFonts(userTheme.fonts)
});
export { makeTheme, chakraTheme as defaultTheme };
diff --git a/hyperglass/ui/util.js b/hyperglass/ui/util.js
index 64dcbb6..24e9f77 100644
--- a/hyperglass/ui/util.js
+++ b/hyperglass/ui/util.js
@@ -1,28 +1,36 @@
import chroma from "chroma-js";
const isDark = color => {
- // YIQ equation from http://24ways.org/2010/calculating-color-contrast
- const rgb = chroma(color).rgb();
- const yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
- return yiq < 128;
+ // YIQ equation from http://24ways.org/2010/calculating-color-contrast
+ const rgb = chroma(color).rgb();
+ const yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
+ return yiq < 128;
};
const isLight = color => isDark(color);
const opposingColor = (theme, color) => {
- const opposing = isDark(color) ? theme.colors.white : theme.colors.black;
- return opposing;
+ if (color.includes(".")) {
+ const colorParts = color.split(".");
+ if (colorParts.length !== 2) {
+ throw Error(`Color is improperly formatted. Got '${color}'`);
+ }
+ const [colorName, colorOpacity] = colorParts;
+ color = theme.colors[colorName][colorOpacity];
+ }
+ const opposing = isDark(color) ? theme.colors.white : theme.colors.black;
+ return opposing;
};
const googleFontUrl = (fontFamily, weights = [300, 400, 700]) => {
- const urlWeights = weights.join(",");
- const fontName = fontFamily
- .split(/, /)[0]
- .trim()
- .replace(/'|"/g, "");
- const urlFont = fontName.split(/ /).join("+");
- const urlBase = `https://fonts.googleapis.com/css?family=${urlFont}:${urlWeights}&display=swap`;
- return urlBase;
+ const urlWeights = weights.join(",");
+ const fontName = fontFamily
+ .split(/, /)[0]
+ .trim()
+ .replace(/'|"/g, "");
+ const urlFont = fontName.split(/ /).join("+");
+ const urlBase = `https://fonts.googleapis.com/css?family=${urlFont}:${urlWeights}&display=swap`;
+ return urlBase;
};
export { isDark, isLight, opposingColor, googleFontUrl };
diff --git a/hyperglass/ui/yarn.lock b/hyperglass/ui/yarn.lock
index d153468..aae35db 100644
--- a/hyperglass/ui/yarn.lock
+++ b/hyperglass/ui/yarn.lock
@@ -1100,6 +1100,11 @@
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.1.4.tgz#0dc4ecedf523004337214187db70a46183bd945b"
integrity sha512-QHbzXjflSlCvDd6vJwdwx16mSB+vUCCQMiU/wK/CgVNPibtpEiIbisyxkpZc55DyDFNUIqP91rSUsNae+ogGDQ==
+"@scarf/scarf@^0.1.5":
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-0.1.5.tgz#fc4cc88294eca336eed9a91549180346de5e6946"
+ integrity sha512-Fx6atDc7JM1r0WkPCDhNetVZNp+DO21q/HGlomAKBG+k8vb1B8fg8Yige4oCf1P9OWTZWm5tM5i3jlXhrSbNOg==
+
"@styled-system/background@^5.0.5", "@styled-system/background@^5.1.2":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@styled-system/background/-/background-5.1.2.tgz#75c63d06b497ab372b70186c0bf608d62847a2ba"
@@ -2718,6 +2723,11 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
+crypto-extra@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/crypto-extra/-/crypto-extra-1.0.1.tgz#91d9e3d8d35f9a16d548c073c31b147f34eab11e"
+ integrity sha512-EDJ07p6UDdyX+7GL6Bb6FqCuCHY34GuII9esE/xXZtW7tlB+A7MYT9O/c1M/ov2aGTdwvw0b3z5OcO50zYBG5Q==
+
css-blank-pseudo@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
@@ -2821,6 +2831,11 @@ damerau-levenshtein@^1.0.4:
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==
+dayjs@^1.8.25:
+ version "1.8.25"
+ resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.25.tgz#d09a8696cee7191bc1289e739f96626391b9c73c"
+ integrity sha512-Pk36juDfQQGDCgr0Lqd1kw15w3OS6xt21JaLPE3lCfsEf8KrERGwDNwvK1tRjrjqFC0uZBJncT4smZQ4F+uV5g==
+
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -5180,6 +5195,13 @@ mute-stream@0.0.8:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
+namor@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/namor/-/namor-2.0.2.tgz#5b1f4e63b91d1fdc8b67d5b9962edd73f67cfcbe"
+ integrity sha512-2LNDTv2naxXm7j0op8ngH472iL5ftTlLUmnq7Uj0kapkNxP2Oa3IyDpo4KUlHcgdQBHBSrUpJLEyUWkMqjFkMQ==
+ dependencies:
+ crypto-extra "1.0.1"
+
nan@^2.12.1:
version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
@@ -6566,6 +6588,13 @@ react-string-replace@^0.4.4:
dependencies:
lodash "^4.17.4"
+react-table@^7.0.4:
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.4.tgz#456838661982c83c3682f156c59a41b4339a2120"
+ integrity sha512-Uqpj+VnUIvsNWNtNFD1z2i7OCHdlhoJtQt0DWx3XOkZnvDyI/eCghK8YBfA9mY4TW7vEgCDLaRCcREC/fmcx6Q==
+ dependencies:
+ "@scarf/scarf" "^0.1.5"
+
react-textfit@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-textfit/-/react-textfit-1.1.0.tgz#088855580f2e7aad269efc81b734bf636877d0e1"
diff --git a/poetry.lock b/poetry.lock
index 3267325..95fbb80 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -846,7 +846,7 @@ description = "Data validation and settings management using python 3.6 type hin
name = "pydantic"
optional = false
python-versions = ">=3.6"
-version = "1.4"
+version = "1.5.1"
[package.dependencies]
[package.dependencies.dataclasses]
@@ -1159,6 +1159,14 @@ version = "1.0.1"
[package.extras]
dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
+[[package]]
+category = "main"
+description = "Makes working with XML feel like you are working with JSON"
+name = "xmltodict"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "0.12.0"
+
[[package]]
category = "dev"
description = "Backport of pathlib-compatible object wrapper for zip files"
@@ -1173,7 +1181,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[metadata]
-content-hash = "7a4bbf22965a2af2a61fcc9d67126093e54986f11e9514d828db6e156534b865"
+content-hash = "7b58a797269e6f4220563b69300281662b8e97f1f8bde1d7551faab23f8327da"
python-versions = "^3.6"
[metadata.files]
@@ -1566,20 +1574,23 @@ pycparser = [
{file = "pycparser-2.19.tar.gz", hash = "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"},
]
pydantic = [
- {file = "pydantic-1.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:07911aab70f3bc52bb845ce1748569c5e70478ac977e106a150dd9d0465ebf04"},
- {file = "pydantic-1.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752"},
- {file = "pydantic-1.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:61d22d36808087d3184ed6ac0d91dd71c533b66addb02e4a9930e1e30833202f"},
- {file = "pydantic-1.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f863456d3d4bf817f2e5248553dee3974c5dc796f48e6ddb599383570f4215ac"},
- {file = "pydantic-1.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bbbed364376f4a0aebb9ea452ff7968b306499a9e74f4db69b28ff2cd4043a11"},
- {file = "pydantic-1.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e27559cedbd7f59d2375bfd6eea29a330ea1a5b0589c34d6b4e0d7bec6027bbf"},
- {file = "pydantic-1.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:50e4e948892a6815649ad5a9a9379ad1e5f090f17842ac206535dfaed75c6f2f"},
- {file = "pydantic-1.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8848b4eb458469739126e4c1a202d723dd092e087f8dbe3104371335f87ba5df"},
- {file = "pydantic-1.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:831a0265a9e3933b3d0f04d1a81bba543bafbe4119c183ff2771871db70524ab"},
- {file = "pydantic-1.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:47b8db7024ba3d46c3d4768535e1cf87b6c8cf92ccd81e76f4e1cb8ee47688b3"},
- {file = "pydantic-1.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:51f11c8bbf794a68086540da099aae4a9107447c7a9d63151edbb7d50110cf21"},
- {file = "pydantic-1.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6100d7862371115c40be55cc4b8d766a74b1d0dbaf99dbfe72bb4bac0faf89ed"},
- {file = "pydantic-1.4-py36.py37.py38-none-any.whl", hash = "sha256:72184c1421103cca128300120f8f1185fb42a9ea73a1c9845b1c53db8c026a7d"},
- {file = "pydantic-1.4.tar.gz", hash = "sha256:f17ec336e64d4583311249fb179528e9a2c27c8a2eaf590ec6ec2c6dece7cb3f"},
+ {file = "pydantic-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a6904e9f18dea58f76f16b95cba6a2f20b72d787abd84ecd67ebc526e61dce6"},
+ {file = "pydantic-1.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da8099fca5ee339d5572cfa8af12cf0856ae993406f0b1eb9bb38c8a660e7416"},
+ {file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:68dece67bff2b3a5cc188258e46b49f676a722304f1c6148ae08e9291e284d98"},
+ {file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ab863853cb502480b118187d670f753be65ec144e1654924bec33d63bc8b3ce2"},
+ {file = "pydantic-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:2007eb062ed0e57875ce8ead12760a6e44bf5836e6a1a7ea81d71eeecf3ede0f"},
+ {file = "pydantic-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:20a15a303ce1e4d831b4e79c17a4a29cb6740b12524f5bba3ea363bff65732bc"},
+ {file = "pydantic-1.5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:473101121b1bd454c8effc9fe66d54812fdc128184d9015c5aaa0d4e58a6d338"},
+ {file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:9be755919258d5d168aeffbe913ed6e8bd562e018df7724b68cabdee3371e331"},
+ {file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:b96ce81c4b5ca62ab81181212edfd057beaa41411cd9700fbcb48a6ba6564b4e"},
+ {file = "pydantic-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:93b9f265329d9827f39f0fca68f5d72cc8321881cdc519a1304fa73b9f8a75bd"},
+ {file = "pydantic-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2c753d355126ddd1eefeb167fa61c7037ecd30b98e7ebecdc0d1da463b4ea09"},
+ {file = "pydantic-1.5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8433dbb87246c0f562af75d00fa80155b74e4f6924b0db6a2078a3cd2f11c6c4"},
+ {file = "pydantic-1.5.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0a1cdf24e567d42dc762d3fed399bd211a13db2e8462af9dfa93b34c41648efb"},
+ {file = "pydantic-1.5.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:8be325fc9da897029ee48d1b5e40df817d97fe969f3ac3fd2434ba7e198c55d5"},
+ {file = "pydantic-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:3714a4056f5bdbecf3a41e0706ec9b228c9513eee2ad884dc2c568c4dfa540e9"},
+ {file = "pydantic-1.5.1-py36.py37.py38-none-any.whl", hash = "sha256:70f27d2f0268f490fe3de0a9b6fca7b7492b8fd6623f9fecd25b221ebee385e3"},
+ {file = "pydantic-1.5.1.tar.gz", hash = "sha256:f0018613c7a0d19df3240c2a913849786f21b6539b9f23d85ce4067489dfacfa"},
]
pydocstyle = [
{file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"},
@@ -1777,6 +1788,10 @@ win32-setctime = [
{file = "win32_setctime-1.0.1-py3-none-any.whl", hash = "sha256:568fd636c68350bcc54755213fe01966fe0a6c90b386c0776425944a0382abef"},
{file = "win32_setctime-1.0.1.tar.gz", hash = "sha256:b47e5023ec7f0b4962950902b15bc56464a380d869f59d27dbf9ab423b23e8f9"},
]
+xmltodict = [
+ {file = "xmltodict-0.12.0-py2.py3-none-any.whl", hash = "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"},
+ {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"},
+]
zipp = [
{file = "zipp-3.0.0-py3-none-any.whl", hash = "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2"},
{file = "zipp-3.0.0.tar.gz", hash = "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a"},
diff --git a/pyproject.toml b/pyproject.toml
index 09e745d..867dabb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,6 +34,7 @@ uvloop = "^0.14.0"
inquirer = "^2.6.3"
paramiko = "^2.7.1"
gunicorn = "^20.0.4"
+xmltodict = "^0.12.0"
[tool.poetry.dev-dependencies]
bandit = "^1.6.2"