mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
start adding response parsing & ui elements
This commit is contained in:
6
.flake8
6
.flake8
@@ -3,16 +3,18 @@ max-line-length=88
|
|||||||
count=True
|
count=True
|
||||||
show-source=False
|
show-source=False
|
||||||
statistics=True
|
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
|
filename=*.py
|
||||||
per-file-ignores=
|
per-file-ignores=
|
||||||
hyperglass/main.py:E402
|
hyperglass/main.py:E402
|
||||||
# Disable redefinition warning for exception handlers
|
# Disable redefinition warning for exception handlers
|
||||||
hyperglass/api.py:F811
|
hyperglass/api.py:F811
|
||||||
# Disable classmethod warning for validator decorators
|
# Disable classmethod warning for validator decorators
|
||||||
|
hyperglass/models.py:N805,E0213,R0903
|
||||||
hyperglass/api/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/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
|
ignore=W503,C0330,R504,D202,S403,S301
|
||||||
select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W
|
select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W
|
||||||
disable-noqa=False
|
disable-noqa=False
|
||||||
|
@@ -24,6 +24,7 @@ from hyperglass.constants import (
|
|||||||
DEFAULT_TERMS,
|
DEFAULT_TERMS,
|
||||||
DEFAULT_DETAILS,
|
DEFAULT_DETAILS,
|
||||||
SUPPORTED_QUERY_TYPES,
|
SUPPORTED_QUERY_TYPES,
|
||||||
|
PARSED_RESPONSE_FIELDS,
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing
|
from hyperglass.exceptions import ConfigError, ConfigInvalid, ConfigMissing
|
||||||
@@ -425,6 +426,7 @@ _frontend_params.update(
|
|||||||
"devices": frontend_devices,
|
"devices": frontend_devices,
|
||||||
"networks": networks,
|
"networks": networks,
|
||||||
"vrfs": vrfs,
|
"vrfs": vrfs,
|
||||||
|
"parsed_data_fields": PARSED_RESPONSE_FIELDS,
|
||||||
"content": {
|
"content": {
|
||||||
"help_menu": content_help,
|
"help_menu": content_help,
|
||||||
"terms": content_terms,
|
"terms": content_terms,
|
||||||
|
@@ -141,6 +141,10 @@ class Text(HyperglassModel):
|
|||||||
cache_prefix: StrictStr = "Results cached for "
|
cache_prefix: StrictStr = "Results cached for "
|
||||||
cache_icon: StrictStr = "Cached from {time} UTC" # Formatted by Javascript
|
cache_icon: StrictStr = "Cached from {time} UTC" # Formatted by Javascript
|
||||||
complete_time: StrictStr = "Completed in {seconds}" # 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")
|
@validator("title_mode")
|
||||||
def validate_title_mode(cls, value):
|
def validate_title_mode(cls, value):
|
||||||
|
@@ -26,6 +26,21 @@ DNS_OVER_HTTPS = {
|
|||||||
"cloudflare": "https://cloudflare-dns.com/dns-query",
|
"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 = """
|
CREDIT = """
|
||||||
Powered by [**hyperglass**](https://github.com/checktheroads/hyperglass) version \
|
Powered by [**hyperglass**](https://github.com/checktheroads/hyperglass) version \
|
||||||
{version}. Source code licensed \
|
{version}. Source code licensed \
|
||||||
|
@@ -193,3 +193,9 @@ class ResponseEmpty(_UnformattedHyperglassError):
|
|||||||
|
|
||||||
class UnsupportedDevice(_UnformattedHyperglassError):
|
class UnsupportedDevice(_UnformattedHyperglassError):
|
||||||
"""Raised when an input NOS is not in the supported NOS list."""
|
"""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"
|
||||||
|
@@ -35,7 +35,14 @@ class HyperglassModel(BaseModel):
|
|||||||
Returns:
|
Returns:
|
||||||
{str} -- Stringified JSON.
|
{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):
|
def export_dict(self, *args, **kwargs):
|
||||||
"""Return instance as dictionary.
|
"""Return instance as dictionary.
|
||||||
@@ -43,7 +50,13 @@ class HyperglassModel(BaseModel):
|
|||||||
Returns:
|
Returns:
|
||||||
{dict} -- Python dictionary.
|
{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):
|
def export_yaml(self, *args, **kwargs):
|
||||||
"""Return instance as YAML.
|
"""Return instance as YAML.
|
||||||
@@ -54,7 +67,14 @@ class HyperglassModel(BaseModel):
|
|||||||
import json
|
import json
|
||||||
import yaml
|
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):
|
class HyperglassModelExtra(HyperglassModel):
|
||||||
|
25
hyperglass/parsing/juniper.py
Normal file
25
hyperglass/parsing/juniper.py
Normal file
@@ -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")
|
1
hyperglass/parsing/models/__init__.py
Normal file
1
hyperglass/parsing/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Data models for parsed responses."""
|
167
hyperglass/parsing/models/juniper.py
Normal file
167
hyperglass/parsing/models/juniper.py
Normal file
@@ -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
|
37
hyperglass/parsing/models/serialized.py
Normal file
37
hyperglass/parsing/models/serialized.py
Normal file
@@ -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)")
|
25
hyperglass/ui/components/Card/CardFooter.js
Normal file
25
hyperglass/ui/components/Card/CardFooter.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Flex } from "@chakra-ui/core";
|
||||||
|
|
||||||
|
const CardFooter = ({ children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
p={4}
|
||||||
|
roundedBottomLeft={4}
|
||||||
|
roundedBottomRight={4}
|
||||||
|
direction="column"
|
||||||
|
borderTopWidth="1px"
|
||||||
|
overflowX="hidden"
|
||||||
|
overflowY="hidden"
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
|
export default CardFooter;
|
25
hyperglass/ui/components/Card/CardHeader.js
Normal file
25
hyperglass/ui/components/Card/CardHeader.js
Normal file
@@ -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 (
|
||||||
|
<Flex
|
||||||
|
bg={bg[colorMode]}
|
||||||
|
p={4}
|
||||||
|
direction="column"
|
||||||
|
roundedTopLeft={4}
|
||||||
|
roundedTopRight={4}
|
||||||
|
borderBottomWidth="1px"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Text fontWeight="bold">{children}</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
|
export default CardHeader;
|
28
hyperglass/ui/components/Card/index.js
Normal file
28
hyperglass/ui/components/Card/index.js
Normal file
@@ -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 (
|
||||||
|
<Flex
|
||||||
|
w="100%"
|
||||||
|
maxW="100%"
|
||||||
|
rounded="md"
|
||||||
|
borderWidth="1px"
|
||||||
|
direction="column"
|
||||||
|
onClick={onClick}
|
||||||
|
bg={bg[colorMode]}
|
||||||
|
color={color[colorMode]}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
|
export default Card;
|
@@ -3,7 +3,7 @@ import { Flex, IconButton, useColorMode } from "@chakra-ui/core";
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import ResetButton from "~/components/ResetButton";
|
import ResetButton from "~/components/ResetButton";
|
||||||
import useMedia from "~/components/MediaProvider";
|
import useMedia from "~/components/MediaProvider";
|
||||||
import useConfig from "~/components/HyperglassProvider";
|
import useConfig, { useHyperglassState } from "~/components/HyperglassProvider";
|
||||||
import Title from "~/components/Title";
|
import Title from "~/components/Title";
|
||||||
|
|
||||||
const AnimatedFlex = motion.custom(Flex);
|
const AnimatedFlex = motion.custom(Flex);
|
||||||
@@ -34,8 +34,16 @@ const titleVariants = {
|
|||||||
|
|
||||||
const icon = { light: "moon", dark: "sun" };
|
const icon = { light: "moon", dark: "sun" };
|
||||||
const bg = { light: "white", dark: "black" };
|
const bg = { light: "white", dark: "black" };
|
||||||
const colorSwitch = { dark: "Switch to light mode", light: "Switch to dark mode" };
|
const colorSwitch = {
|
||||||
const headerTransition = { type: "spring", ease: "anticipate", damping: 15, stiffness: 100 };
|
dark: "Switch to light mode",
|
||||||
|
light: "Switch to dark mode"
|
||||||
|
};
|
||||||
|
const headerTransition = {
|
||||||
|
type: "spring",
|
||||||
|
ease: "anticipate",
|
||||||
|
damping: 15,
|
||||||
|
stiffness: 100
|
||||||
|
};
|
||||||
const titleJustify = {
|
const titleJustify = {
|
||||||
true: ["flex-end", "flex-end", "center", "center"],
|
true: ["flex-end", "flex-end", "center", "center"],
|
||||||
false: ["flex-start", "flex-start", "center", "center"]
|
false: ["flex-start", "flex-start", "center", "center"]
|
||||||
@@ -53,10 +61,14 @@ const widthMap = {
|
|||||||
all: ["90%", "90%", "25%", "25%"]
|
all: ["90%", "90%", "25%", "25%"]
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ isSubmitting, handleFormReset, ...props }) => {
|
export default ({ layoutRef, ...props }) => {
|
||||||
const { colorMode, toggleColorMode } = useColorMode();
|
const { colorMode, toggleColorMode } = useColorMode();
|
||||||
const { web } = useConfig();
|
const { web } = useConfig();
|
||||||
const { mediaSize } = useMedia();
|
const { mediaSize } = useMedia();
|
||||||
|
const { isSubmitting, resetForm } = useHyperglassState();
|
||||||
|
const handleFormReset = () => {
|
||||||
|
resetForm(layoutRef);
|
||||||
|
};
|
||||||
const resetButton = (
|
const resetButton = (
|
||||||
<AnimatePresence key="resetButton">
|
<AnimatePresence key="resetButton">
|
||||||
<AnimatedFlex
|
<AnimatedFlex
|
||||||
@@ -69,7 +81,10 @@ export default ({ isSubmitting, handleFormReset, ...props }) => {
|
|||||||
ml={resetButtonMl[isSubmitting]}
|
ml={resetButtonMl[isSubmitting]}
|
||||||
display={isSubmitting ? "flex" : "none"}
|
display={isSubmitting ? "flex" : "none"}
|
||||||
>
|
>
|
||||||
<AnimatedResetButton isSubmitting={isSubmitting} onClick={handleFormReset} />
|
<AnimatedResetButton
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onClick={handleFormReset}
|
||||||
|
/>
|
||||||
</AnimatedFlex>
|
</AnimatedFlex>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
@@ -77,7 +92,9 @@ export default ({ isSubmitting, handleFormReset, ...props }) => {
|
|||||||
<AnimatedFlex
|
<AnimatedFlex
|
||||||
key="title"
|
key="title"
|
||||||
px={1}
|
px={1}
|
||||||
alignItems={isSubmitting ? "center" : ["center", "center", "flex-end", "flex-end"]}
|
alignItems={
|
||||||
|
isSubmitting ? "center" : ["center", "center", "flex-end", "flex-end"]
|
||||||
|
}
|
||||||
positionTransition={headerTransition}
|
positionTransition={headerTransition}
|
||||||
initial={{ scale: 0.5 }}
|
initial={{ scale: 0.5 }}
|
||||||
animate={
|
animate={
|
||||||
|
@@ -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 dynamic from "next/dynamic";
|
||||||
import { CSSReset, ThemeProvider } from "@chakra-ui/core";
|
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";
|
import { makeTheme, defaultTheme } from "~/theme";
|
||||||
|
|
||||||
// Disable SSR for ColorModeProvider
|
// Disable SSR for ColorModeProvider
|
||||||
@@ -21,7 +26,9 @@ export const HyperglassProvider = ({ config, children }) => {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<ColorModeProvider value={config.web.theme.default_color_mode ?? null}>
|
<ColorModeProvider value={config.web.theme.default_color_mode ?? null}>
|
||||||
<CSSReset />
|
<CSSReset />
|
||||||
<MediaProvider theme={theme}>{children}</MediaProvider>
|
<MediaProvider theme={theme}>
|
||||||
|
<StateProvider>{children}</StateProvider>
|
||||||
|
</MediaProvider>
|
||||||
</ColorModeProvider>
|
</ColorModeProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</HyperglassContext.Provider>
|
</HyperglassContext.Provider>
|
||||||
@@ -29,3 +36,7 @@ export const HyperglassProvider = ({ config, children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default () => useContext(HyperglassContext);
|
export default () => useContext(HyperglassContext);
|
||||||
|
|
||||||
|
export const useHyperglassState = _useHyperglassState;
|
||||||
|
|
||||||
|
export const useMedia = _useMedia;
|
||||||
|
@@ -1,38 +1,22 @@
|
|||||||
import React, { useRef, useState } from "react";
|
import * as React from "react";
|
||||||
import { Flex, useColorMode, useDisclosure } from "@chakra-ui/core";
|
import { useRef } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { Flex, useColorMode } from "@chakra-ui/core";
|
||||||
import HyperglassForm from "~/components/HyperglassForm";
|
|
||||||
import Results from "~/components/Results";
|
|
||||||
import Header from "~/components/Header";
|
import Header from "~/components/Header";
|
||||||
import Footer from "~/components/Footer";
|
import Footer from "~/components/Footer";
|
||||||
import Greeting from "~/components/Greeting";
|
import Greeting from "~/components/Greeting";
|
||||||
import Meta from "~/components/Meta";
|
import Meta from "~/components/Meta";
|
||||||
import useConfig from "~/components/HyperglassProvider";
|
import useConfig, { useHyperglassState } from "~/components/HyperglassProvider";
|
||||||
import Debugger from "~/components/Debugger";
|
import Debugger from "~/components/Debugger";
|
||||||
import useSessionStorage from "~/hooks/useSessionStorage";
|
|
||||||
|
|
||||||
const AnimatedForm = motion.custom(HyperglassForm);
|
|
||||||
|
|
||||||
const bg = { light: "white", dark: "black" };
|
const bg = { light: "white", dark: "black" };
|
||||||
const color = { light: "black", dark: "white" };
|
const color = { light: "black", dark: "white" };
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = ({ children }) => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
const [isSubmitting, setSubmitting] = useState(false);
|
const { greetingAck, setGreetingAck } = useHyperglassState();
|
||||||
const [formData, setFormData] = useState({});
|
|
||||||
const [greetingAck, setGreetingAck] = useSessionStorage(
|
|
||||||
"hyperglass-greeting-ack",
|
|
||||||
false
|
|
||||||
);
|
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
const handleFormReset = () => {
|
|
||||||
containerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
setSubmitting(false);
|
|
||||||
setFormData({});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta />
|
<Meta />
|
||||||
@@ -45,10 +29,7 @@ const Layout = () => {
|
|||||||
color={color[colorMode]}
|
color={color[colorMode]}
|
||||||
>
|
>
|
||||||
<Flex px={2} flex="0 1 auto" flexDirection="column">
|
<Flex px={2} flex="0 1 auto" flexDirection="column">
|
||||||
<Header
|
<Header layoutRef={containerRef} />
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
handleFormReset={handleFormReset}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
px={2}
|
px={2}
|
||||||
@@ -61,30 +42,7 @@ const Layout = () => {
|
|||||||
justifyContent="start"
|
justifyContent="start"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
>
|
>
|
||||||
{isSubmitting && formData && (
|
{children}
|
||||||
<Results
|
|
||||||
queryLocation={formData.query_location}
|
|
||||||
queryType={formData.query_type}
|
|
||||||
queryVrf={formData.query_vrf}
|
|
||||||
queryTarget={formData.query_target}
|
|
||||||
setSubmitting={setSubmitting}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<AnimatePresence>
|
|
||||||
{!isSubmitting && (
|
|
||||||
<AnimatedForm
|
|
||||||
initial={{ opacity: 0, y: 300 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
exit={{ opacity: 0, x: -300 }}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
setSubmitting={setSubmitting}
|
|
||||||
setFormData={setFormData}
|
|
||||||
greetingAck={greetingAck}
|
|
||||||
setGreetingAck={setGreetingAck}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Footer />
|
<Footer />
|
||||||
{config.developer_mode && <Debugger />}
|
{config.developer_mode && <Debugger />}
|
||||||
|
51
hyperglass/ui/components/LookingGlass.js
Normal file
51
hyperglass/ui/components/LookingGlass.js
Normal file
@@ -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 (
|
||||||
|
<Layout>
|
||||||
|
{isSubmitting && formData && (
|
||||||
|
<Results
|
||||||
|
queryLocation={formData.query_location}
|
||||||
|
queryType={formData.query_type}
|
||||||
|
queryVrf={formData.query_vrf}
|
||||||
|
queryTarget={formData.query_target}
|
||||||
|
setSubmitting={setSubmitting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<AnimatePresence>
|
||||||
|
{!isSubmitting && (
|
||||||
|
<AnimatedForm
|
||||||
|
initial={{ opacity: 0, y: 300 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
exit={{ opacity: 0, x: -300 }}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
setSubmitting={setSubmitting}
|
||||||
|
setFormData={setFormData}
|
||||||
|
greetingAck={greetingAck}
|
||||||
|
setGreetingAck={setGreetingAck}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LookingGlass.displayName = "LookingGlass";
|
||||||
|
export default LookingGlass;
|
@@ -25,10 +25,18 @@ export const MediaProvider = ({ theme, children }) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({ isSm: isSm, isMd: isMd, isLg: isLg, isXl: isXl, mediaSize: mediaSize }),
|
() => ({
|
||||||
|
isSm: isSm,
|
||||||
|
isMd: isMd,
|
||||||
|
isLg: isLg,
|
||||||
|
isXl: isXl,
|
||||||
|
mediaSize: mediaSize
|
||||||
|
}),
|
||||||
[isSm, isMd, isLg, isXl, mediaSize]
|
[isSm, isMd, isLg, isXl, mediaSize]
|
||||||
);
|
);
|
||||||
return <MediaContext.Provider value={value}>{children}</MediaContext.Provider>;
|
return (
|
||||||
|
<MediaContext.Provider value={value}>{children}</MediaContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default () => useContext(MediaContext);
|
export default () => useContext(MediaContext);
|
||||||
|
33
hyperglass/ui/components/StateProvider.js
Normal file
33
hyperglass/ui/components/StateProvider.js
Normal file
@@ -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 (
|
||||||
|
<StateContext.Provider value={value}>{children}</StateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHyperglassState = () => useContext(StateContext);
|
39
hyperglass/ui/components/Table/MainTable.js
Normal file
39
hyperglass/ui/components/Table/MainTable.js
Normal file
@@ -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 (
|
||||||
|
<Box
|
||||||
|
css={css({
|
||||||
|
"&::-webkit-scrollbar": { height: "5px" },
|
||||||
|
"&::-webkit-scrollbar-track": {
|
||||||
|
backgroundColor: scrollbarBg[colorMode]
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb": {
|
||||||
|
backgroundColor: scrollbar[colorMode]
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb:hover": {
|
||||||
|
backgroundColor: scrollbarHover[colorMode]
|
||||||
|
},
|
||||||
|
|
||||||
|
"-ms-overflow-style": { display: "none" }
|
||||||
|
})(theme)}
|
||||||
|
overflow="auto"
|
||||||
|
borderRadius="md"
|
||||||
|
boxSizing="border-box"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MainTable.displayName = "MainTable";
|
||||||
|
export default MainTable;
|
24
hyperglass/ui/components/Table/TableBody.js
Normal file
24
hyperglass/ui/components/Table/TableBody.js
Normal file
@@ -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 (
|
||||||
|
<Box
|
||||||
|
as="tbody"
|
||||||
|
overflowY="scroll"
|
||||||
|
css={css({
|
||||||
|
"&::-webkit-scrollbar": { display: "none" },
|
||||||
|
"&": { "-ms-overflow-style": "none" }
|
||||||
|
})}
|
||||||
|
overflowX="hidden"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
|
export default TableBody;
|
52
hyperglass/ui/components/Table/TableCell.js
Normal file
52
hyperglass/ui/components/Table/TableCell.js
Normal file
@@ -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 (
|
||||||
|
<Box
|
||||||
|
as="td"
|
||||||
|
p={4}
|
||||||
|
m={0}
|
||||||
|
w="1%"
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
{...borderProps}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
export default TableCell;
|
29
hyperglass/ui/components/Table/TableHead.js
Normal file
29
hyperglass/ui/components/Table/TableHead.js
Normal file
@@ -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 (
|
||||||
|
<Box
|
||||||
|
as="thead"
|
||||||
|
overflowX="hidden"
|
||||||
|
overflowY="auto"
|
||||||
|
bg={bg[colorMode]}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
|
export default TableHead;
|
51
hyperglass/ui/components/Table/TableIconButton.js
Normal file
51
hyperglass/ui/components/Table/TableIconButton.js
Normal file
@@ -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 (
|
||||||
|
// <IconButton
|
||||||
|
// size="sm"
|
||||||
|
// {...rest}
|
||||||
|
// icon={icon}
|
||||||
|
// borderWidth={1}
|
||||||
|
// onClick={onClick}
|
||||||
|
// variantColor={variantColor}
|
||||||
|
// isDisabled={isDisabled}
|
||||||
|
// aria-label="Table Icon button"
|
||||||
|
// >
|
||||||
|
// {children}
|
||||||
|
// </IconButton>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// TableIconButton.defaultProps = {
|
||||||
|
// variantColor: "gray",
|
||||||
|
// };
|
||||||
|
|
||||||
|
const TableIconButton = ({
|
||||||
|
icon,
|
||||||
|
onClick,
|
||||||
|
isDisabled,
|
||||||
|
color,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
icon={icon}
|
||||||
|
borderWidth={1}
|
||||||
|
onClick={onClick}
|
||||||
|
variantColor={color}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
aria-label="Table Icon Button"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TableIconButton.displayName = "TableIconButton";
|
||||||
|
|
||||||
|
export default TableIconButton;
|
65
hyperglass/ui/components/Table/TableRow.js
Normal file
65
hyperglass/ui/components/Table/TableRow.js
Normal file
@@ -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 (
|
||||||
|
<PseudoBox
|
||||||
|
as="tr"
|
||||||
|
_hover={{
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor: highlight
|
||||||
|
? `${highlightBg}.${alphaMapHover[colorMode]}`
|
||||||
|
: hoverBg[colorMode]
|
||||||
|
}}
|
||||||
|
bg={bg}
|
||||||
|
color={color}
|
||||||
|
fontWeight={highlight ? "bold" : null}
|
||||||
|
{...borderProps}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PseudoBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
|
export default TableRow;
|
32
hyperglass/ui/components/Table/TableSelectShow.js
Normal file
32
hyperglass/ui/components/Table/TableSelectShow.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Select } from "@chakra-ui/core";
|
||||||
|
|
||||||
|
{
|
||||||
|
/* <select
|
||||||
|
value={pageSize}
|
||||||
|
onChange={e => {setPageSize(Number(e.target.value))}}
|
||||||
|
>
|
||||||
|
{[5, 10, 20, 30, 40, 50].map(pageSize => (
|
||||||
|
<option key={pageSize} value={pageSize}>
|
||||||
|
Show {pageSize}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select> */
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableSelectShow = ({ value, onChange, children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<Select onChange={onChange} {...props}>
|
||||||
|
{[5, 10, 20, 30, 40, 50].map(value => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
Show {value}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
{children}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TableSelectShow.displayName = "TableSelectShow";
|
||||||
|
|
||||||
|
export default TableSelectShow;
|
191
hyperglass/ui/components/Table/index.js
Normal file
191
hyperglass/ui/components/Table/index.js
Normal file
@@ -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 (
|
||||||
|
<Card>
|
||||||
|
{!!tableHeading && <TopSection>{tableHeading}</TopSection>}
|
||||||
|
<MainTable {...getTableProps()}>
|
||||||
|
<TableHead>
|
||||||
|
{headerGroups.map(headerGroup => (
|
||||||
|
<TableRow
|
||||||
|
key={headerGroup.id}
|
||||||
|
{...headerGroup.getHeaderGroupProps()}
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map(column => (
|
||||||
|
<TableCell
|
||||||
|
as="th"
|
||||||
|
align={column.align}
|
||||||
|
key={column.id}
|
||||||
|
{...column.getHeaderProps()}
|
||||||
|
{...column.getSortByToggleProps()}
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" display="inline-block">
|
||||||
|
{column.render("Header")}
|
||||||
|
</Text>
|
||||||
|
{column.isSorted ? (
|
||||||
|
column.isSortedDesc ? (
|
||||||
|
<Icon name="chevron-down" size={4} ml={1} />
|
||||||
|
) : (
|
||||||
|
<Icon name="chevron-up" size={4} ml={1} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{page.map(
|
||||||
|
(row, key) =>
|
||||||
|
prepareRow(row) || (
|
||||||
|
<TableRow
|
||||||
|
index={key}
|
||||||
|
doStripe={striped}
|
||||||
|
doHorizontalBorders={bordersHorizontal}
|
||||||
|
onClick={() => onRowClick && onRowClick(row)}
|
||||||
|
key={key}
|
||||||
|
highlight={row.values[rowHighlightProp] ?? false}
|
||||||
|
highlightBg={rowHighlightBg}
|
||||||
|
highlightColor={rowHighlightColor}
|
||||||
|
{...row.getRowProps()}
|
||||||
|
>
|
||||||
|
{row.cells.map((cell, i) => {
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
align={cell.column.align}
|
||||||
|
cell={cell}
|
||||||
|
bordersVertical={[bordersVertical, i]}
|
||||||
|
key={cell.row.index}
|
||||||
|
{...cell.getCellProps()}
|
||||||
|
>
|
||||||
|
{cell.render(cellRender ?? "Cell")}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</MainTable>
|
||||||
|
<BottomSection>
|
||||||
|
<Flex direction="row">
|
||||||
|
<TableIconButton
|
||||||
|
mr={2}
|
||||||
|
onClick={() => gotoPage(0)}
|
||||||
|
isDisabled={!canPreviousPage}
|
||||||
|
icon={() => <Icon name="arrow-left" size={3} />}
|
||||||
|
/>
|
||||||
|
<TableIconButton
|
||||||
|
mr={2}
|
||||||
|
onClick={() => previousPage()}
|
||||||
|
isDisabled={!canPreviousPage}
|
||||||
|
icon={() => <Icon name="chevron-left" size={6} />}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="center" alignItems="center">
|
||||||
|
<Text mr={4} whiteSpace="nowrap">
|
||||||
|
Page{" "}
|
||||||
|
<strong>
|
||||||
|
{pageIndex + 1} of {pageOptions.length}
|
||||||
|
</strong>{" "}
|
||||||
|
</Text>
|
||||||
|
{!isTabletOrMobile && (
|
||||||
|
<TableSelectShow
|
||||||
|
value={pageSize}
|
||||||
|
onChange={e => {
|
||||||
|
setPageSize(Number(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Flex direction="row">
|
||||||
|
<TableIconButton
|
||||||
|
ml={2}
|
||||||
|
isDisabled={!canNextPage}
|
||||||
|
onClick={() => nextPage()}
|
||||||
|
icon={() => <Icon name="chevron-right" size={6} />}
|
||||||
|
/>
|
||||||
|
<TableIconButton
|
||||||
|
ml={2}
|
||||||
|
onClick={() => gotoPage(pageCount ? pageCount - 1 : 1)}
|
||||||
|
isDisabled={!canNextPage}
|
||||||
|
icon={() => <Icon name="arrow-right" size={3} />}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</BottomSection>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Table.displayName = "Table";
|
||||||
|
|
||||||
|
export default Table;
|
35
hyperglass/ui/components/Table/makeData.js
Normal file
35
hyperglass/ui/components/Table/makeData.js
Normal file
@@ -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();
|
||||||
|
}
|
70
hyperglass/ui/components/Table/styles.js
Normal file
70
hyperglass/ui/components/Table/styles.js
Normal file
@@ -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 (
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
{...rest}
|
||||||
|
icon={icon}
|
||||||
|
borderWidth={1}
|
||||||
|
onClick={onClick}
|
||||||
|
variantColor={variantColor}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
aria-label="Table Icon button"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TableIconButton.defaultProps = {
|
||||||
|
variantColor: "gray",
|
||||||
|
};
|
3
hyperglass/ui/package.json
vendored
3
hyperglass/ui/package.json
vendored
@@ -18,9 +18,11 @@
|
|||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"axios-hooks": "^1.9.0",
|
"axios-hooks": "^1.9.0",
|
||||||
"chroma-js": "^2.1.0",
|
"chroma-js": "^2.1.0",
|
||||||
|
"dayjs": "^1.8.25",
|
||||||
"emotion-theming": "^10.0.27",
|
"emotion-theming": "^10.0.27",
|
||||||
"framer-motion": "^1.10.0",
|
"framer-motion": "^1.10.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
"namor": "^2.0.2",
|
||||||
"next": "^9.3.1",
|
"next": "^9.3.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-countdown": "^2.2.1",
|
"react-countdown": "^2.2.1",
|
||||||
@@ -30,6 +32,7 @@
|
|||||||
"react-markdown": "^4.3.1",
|
"react-markdown": "^4.3.1",
|
||||||
"react-select": "^3.0.8",
|
"react-select": "^3.0.8",
|
||||||
"react-string-replace": "^0.4.4",
|
"react-string-replace": "^0.4.4",
|
||||||
|
"react-table": "^7.0.4",
|
||||||
"react-textfit": "^1.1.0",
|
"react-textfit": "^1.1.0",
|
||||||
"string-format": "^2.0.0",
|
"string-format": "^2.0.0",
|
||||||
"styled-system": "^5.1.5",
|
"styled-system": "^5.1.5",
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import React from "react";
|
import * as React from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Loading from "~/components/Loading";
|
import Loading from "~/components/Loading";
|
||||||
const Layout = dynamic(() => import("~/components/Layout"), { loading: Loading });
|
const LookingGlass = dynamic(() => import("~/components/LookingGlass"), {
|
||||||
|
loading: Loading
|
||||||
|
});
|
||||||
|
|
||||||
const Index = () => <Layout />;
|
const Index = () => <LookingGlass />;
|
||||||
|
|
||||||
export default Index;
|
export default Index;
|
||||||
|
336
hyperglass/ui/pages/structured.js
Normal file
336
hyperglass/ui/pages/structured.js
Normal file
@@ -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 }) => (
|
||||||
|
<Text fontSize="sm" fontFamily="mono" {...props}>
|
||||||
|
{v}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Active = ({ isActive }) => {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={isActive ? "check-circle" : "warning"}
|
||||||
|
color={isActiveColor[isActive][colorMode]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Age = ({ inSeconds }) => {
|
||||||
|
const now = dayjs.utc();
|
||||||
|
const then = now.subtract(inSeconds, "seconds");
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
hasArrow
|
||||||
|
label={then.toString().replace("GMT", "UTC")}
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm">{now.to(then, true)}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Weight = ({ weight, winningWeight }) => {
|
||||||
|
const fixMeText =
|
||||||
|
winningWeight === "low"
|
||||||
|
? "Lower Weight is Preferred"
|
||||||
|
: "Higher Weight is Preferred";
|
||||||
|
return (
|
||||||
|
<Tooltip hasArrow label={fixMeText} placement="right">
|
||||||
|
<Text fontSize="sm" fontFamily="mono">
|
||||||
|
{weight}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ASPath = ({ path, active, longestASN }) => {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
let paths = [];
|
||||||
|
path.map((asn, i) => {
|
||||||
|
const asnStr = String(asn);
|
||||||
|
i !== 0 &&
|
||||||
|
paths.push(
|
||||||
|
<Icon
|
||||||
|
name="chevron-right"
|
||||||
|
key={`separator-${i}`}
|
||||||
|
color={arrowColor[active][colorMode]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
paths.push(
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
as="span"
|
||||||
|
whiteSpace="pre"
|
||||||
|
fontFamily="mono"
|
||||||
|
key={`as-${asnStr}-${i}`}
|
||||||
|
>
|
||||||
|
{asnStr}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Communities = ({ communities }) => {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
let component;
|
||||||
|
communities.length === 0
|
||||||
|
? (component = (
|
||||||
|
<Tooltip placement="right" hasArrow label="No Communities">
|
||||||
|
<Icon name="question-outline" />
|
||||||
|
</Tooltip>
|
||||||
|
))
|
||||||
|
: (component = (
|
||||||
|
<Popover trigger="hover" placement="right">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Icon name="view" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
textAlign="left"
|
||||||
|
p={4}
|
||||||
|
maxW="fit-content"
|
||||||
|
color={colorMode === "dark" ? "white" : "black"}
|
||||||
|
>
|
||||||
|
<PopoverArrow />
|
||||||
|
{communities.map(c => (
|
||||||
|
<MonoField fontWeight="normal" v={c} key={c.replace(":", "-")} />
|
||||||
|
))}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
));
|
||||||
|
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 (
|
||||||
|
<Tooltip
|
||||||
|
hasArrow
|
||||||
|
placement="right"
|
||||||
|
label={stateText[state] ?? stateText[3]}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={rpkiIcon[state]}
|
||||||
|
color={rpkiColor[active][colorMode][state]}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Cell = ({ data, rawData, longestASN }) => {
|
||||||
|
hiddenCols.includes(data.column.id) &&
|
||||||
|
data.setHiddenColumns(old => [...old, data.column.id]);
|
||||||
|
const component = {
|
||||||
|
active: <Active isActive={data.value} />,
|
||||||
|
age: <Age inSeconds={data.value} />,
|
||||||
|
weight: (
|
||||||
|
<Weight weight={data.value} winningWeight={rawData.winning_weight} />
|
||||||
|
),
|
||||||
|
med: <MonoField v={data.value} />,
|
||||||
|
local_preference: <MonoField v={data.value} />,
|
||||||
|
as_path: (
|
||||||
|
<ASPath
|
||||||
|
path={data.value}
|
||||||
|
active={data.row.values.active}
|
||||||
|
longestASN={longestASN}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
communities: <Communities communities={data.value} />,
|
||||||
|
next_hop: <MonoField v={data.value} />,
|
||||||
|
source_as: <MonoField v={data.value} />,
|
||||||
|
source_rid: <MonoField v={data.value} />,
|
||||||
|
peer_rid: <MonoField v={data.value} />,
|
||||||
|
rpki_state: <RPKIState state={data.value} active={data.row.values.active} />
|
||||||
|
};
|
||||||
|
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 (
|
||||||
|
<Layout>
|
||||||
|
<Flex my={8} w="80%">
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
data={data.routes}
|
||||||
|
rowHighlightProp="active"
|
||||||
|
cellRender={d => (
|
||||||
|
<Cell data={d} rawData={data} longestASN={asLength} />
|
||||||
|
)}
|
||||||
|
bordersHorizontal
|
||||||
|
rowHighlightBg="green"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Structured;
|
@@ -37,14 +37,29 @@ const alphaColors = color => ({
|
|||||||
const generateColors = colorInput => {
|
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 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 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 lightnessGoal = validColor.get("hsl.l");
|
||||||
const closestLightness = lightnessMap.reduce((prev, curr) =>
|
const closestLightness = lightnessMap.reduce((prev, curr) =>
|
||||||
Math.abs(curr - lightnessGoal) < Math.abs(prev - lightnessGoal) ? curr : prev
|
Math.abs(curr - lightnessGoal) < Math.abs(prev - lightnessGoal)
|
||||||
|
? curr
|
||||||
|
: prev
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseColorIndex = lightnessMap.findIndex(l => l === closestLightness);
|
const baseColorIndex = lightnessMap.findIndex(l => l === closestLightness);
|
||||||
@@ -68,62 +83,7 @@ const generateColors = colorInput => {
|
|||||||
return colorMap;
|
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 = [
|
const defaultBodyFonts = [
|
||||||
// "Nunito",
|
|
||||||
"-apple-system",
|
"-apple-system",
|
||||||
"BlinkMacSystemFont",
|
"BlinkMacSystemFont",
|
||||||
'"Segoe UI"',
|
'"Segoe UI"',
|
||||||
@@ -136,7 +96,6 @@ const defaultBodyFonts = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const defaultMonoFonts = [
|
const defaultMonoFonts = [
|
||||||
// '"Fira Code"',
|
|
||||||
"SFMono-Regular",
|
"SFMono-Regular",
|
||||||
"Melno",
|
"Melno",
|
||||||
"Monaco",
|
"Monaco",
|
||||||
@@ -146,18 +105,6 @@ const defaultMonoFonts = [
|
|||||||
"monospace"
|
"monospace"
|
||||||
];
|
];
|
||||||
|
|
||||||
// const defaultFonts = {
|
|
||||||
// body: defaultBodyFonts.join(", "),
|
|
||||||
// heading: defaultBodyFonts.join(", "),
|
|
||||||
// mono: defaultMonoFonts.join(", ")
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const defaultTheme = {
|
|
||||||
// ...chakraTheme,
|
|
||||||
// colors: defaultColors,
|
|
||||||
// fonts: defaultFonts
|
|
||||||
// };
|
|
||||||
|
|
||||||
const generatePalette = palette => {
|
const generatePalette = palette => {
|
||||||
const generatedPalette = {};
|
const generatedPalette = {};
|
||||||
Object.keys(palette).map(color => {
|
Object.keys(palette).map(color => {
|
||||||
@@ -171,25 +118,10 @@ const generatePalette = palette => {
|
|||||||
return generatedPalette;
|
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 formatFont = font => {
|
||||||
const fontList = font.split(" ");
|
const fontList = font.split(" ");
|
||||||
const fontFmt = fontList.length >= 2 ? `'${fontList.join(" ")}'` : fontList.join(" ");
|
const fontFmt =
|
||||||
|
fontList.length >= 2 ? `'${fontList.join(" ")}'` : fontList.join(" ");
|
||||||
return fontFmt;
|
return fontFmt;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,22 +143,11 @@ const importFonts = userFonts => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const importColors = (userColors = {}) => {
|
const importColors = (userColors = {}) => {
|
||||||
// const baseColors = {
|
|
||||||
// ...defaultBasePalette,
|
|
||||||
// ...userColors
|
|
||||||
// };
|
|
||||||
|
|
||||||
const generatedColors = generatePalette(userColors);
|
const generatedColors = generatePalette(userColors);
|
||||||
// const swatchColors = generatePalette(baseColors);
|
|
||||||
// const funcColors = generateFuncPalette(baseColors);
|
|
||||||
// const bwAlphaColors = generateAlphaPalette(userColors);
|
|
||||||
return {
|
return {
|
||||||
transparent: "transparent",
|
transparent: "transparent",
|
||||||
current: "currentColor",
|
current: "currentColor",
|
||||||
// ...swatchColors,
|
|
||||||
// ...funcColors,
|
|
||||||
...generatedColors
|
...generatedColors
|
||||||
// ...bwAlphaColors
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -10,6 +10,14 @@ const isDark = color => {
|
|||||||
const isLight = color => isDark(color);
|
const isLight = color => isDark(color);
|
||||||
|
|
||||||
const opposingColor = (theme, color) => {
|
const opposingColor = (theme, color) => {
|
||||||
|
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;
|
const opposing = isDark(color) ? theme.colors.white : theme.colors.black;
|
||||||
return opposing;
|
return opposing;
|
||||||
};
|
};
|
||||||
|
29
hyperglass/ui/yarn.lock
vendored
29
hyperglass/ui/yarn.lock
vendored
@@ -1100,6 +1100,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.1.4.tgz#0dc4ecedf523004337214187db70a46183bd945b"
|
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.1.4.tgz#0dc4ecedf523004337214187db70a46183bd945b"
|
||||||
integrity sha512-QHbzXjflSlCvDd6vJwdwx16mSB+vUCCQMiU/wK/CgVNPibtpEiIbisyxkpZc55DyDFNUIqP91rSUsNae+ogGDQ==
|
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":
|
"@styled-system/background@^5.0.5", "@styled-system/background@^5.1.2":
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@styled-system/background/-/background-5.1.2.tgz#75c63d06b497ab372b70186c0bf608d62847a2ba"
|
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"
|
randombytes "^2.0.0"
|
||||||
randomfill "^1.0.3"
|
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:
|
css-blank-pseudo@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
|
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"
|
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
|
||||||
integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==
|
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:
|
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
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"
|
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||||
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
|
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:
|
nan@^2.12.1:
|
||||||
version "2.14.0"
|
version "2.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||||
@@ -6566,6 +6588,13 @@ react-string-replace@^0.4.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.17.4"
|
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:
|
react-textfit@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-textfit/-/react-textfit-1.1.0.tgz#088855580f2e7aad269efc81b734bf636877d0e1"
|
resolved "https://registry.yarnpkg.com/react-textfit/-/react-textfit-1.1.0.tgz#088855580f2e7aad269efc81b734bf636877d0e1"
|
||||||
|
47
poetry.lock
generated
47
poetry.lock
generated
@@ -846,7 +846,7 @@ description = "Data validation and settings management using python 3.6 type hin
|
|||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
version = "1.4"
|
version = "1.5.1"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
[package.dependencies.dataclasses]
|
[package.dependencies.dataclasses]
|
||||||
@@ -1159,6 +1159,14 @@ version = "1.0.1"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
|
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]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
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"]
|
testing = ["jaraco.itertools", "func-timeout"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "7a4bbf22965a2af2a61fcc9d67126093e54986f11e9514d828db6e156534b865"
|
content-hash = "7b58a797269e6f4220563b69300281662b8e97f1f8bde1d7551faab23f8327da"
|
||||||
python-versions = "^3.6"
|
python-versions = "^3.6"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
@@ -1566,20 +1574,23 @@ pycparser = [
|
|||||||
{file = "pycparser-2.19.tar.gz", hash = "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"},
|
{file = "pycparser-2.19.tar.gz", hash = "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"},
|
||||||
]
|
]
|
||||||
pydantic = [
|
pydantic = [
|
||||||
{file = "pydantic-1.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:07911aab70f3bc52bb845ce1748569c5e70478ac977e106a150dd9d0465ebf04"},
|
{file = "pydantic-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a6904e9f18dea58f76f16b95cba6a2f20b72d787abd84ecd67ebc526e61dce6"},
|
||||||
{file = "pydantic-1.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752"},
|
{file = "pydantic-1.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da8099fca5ee339d5572cfa8af12cf0856ae993406f0b1eb9bb38c8a660e7416"},
|
||||||
{file = "pydantic-1.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:61d22d36808087d3184ed6ac0d91dd71c533b66addb02e4a9930e1e30833202f"},
|
{file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:68dece67bff2b3a5cc188258e46b49f676a722304f1c6148ae08e9291e284d98"},
|
||||||
{file = "pydantic-1.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f863456d3d4bf817f2e5248553dee3974c5dc796f48e6ddb599383570f4215ac"},
|
{file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ab863853cb502480b118187d670f753be65ec144e1654924bec33d63bc8b3ce2"},
|
||||||
{file = "pydantic-1.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bbbed364376f4a0aebb9ea452ff7968b306499a9e74f4db69b28ff2cd4043a11"},
|
{file = "pydantic-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:2007eb062ed0e57875ce8ead12760a6e44bf5836e6a1a7ea81d71eeecf3ede0f"},
|
||||||
{file = "pydantic-1.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e27559cedbd7f59d2375bfd6eea29a330ea1a5b0589c34d6b4e0d7bec6027bbf"},
|
{file = "pydantic-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:20a15a303ce1e4d831b4e79c17a4a29cb6740b12524f5bba3ea363bff65732bc"},
|
||||||
{file = "pydantic-1.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:50e4e948892a6815649ad5a9a9379ad1e5f090f17842ac206535dfaed75c6f2f"},
|
{file = "pydantic-1.5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:473101121b1bd454c8effc9fe66d54812fdc128184d9015c5aaa0d4e58a6d338"},
|
||||||
{file = "pydantic-1.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8848b4eb458469739126e4c1a202d723dd092e087f8dbe3104371335f87ba5df"},
|
{file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:9be755919258d5d168aeffbe913ed6e8bd562e018df7724b68cabdee3371e331"},
|
||||||
{file = "pydantic-1.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:831a0265a9e3933b3d0f04d1a81bba543bafbe4119c183ff2771871db70524ab"},
|
{file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:b96ce81c4b5ca62ab81181212edfd057beaa41411cd9700fbcb48a6ba6564b4e"},
|
||||||
{file = "pydantic-1.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:47b8db7024ba3d46c3d4768535e1cf87b6c8cf92ccd81e76f4e1cb8ee47688b3"},
|
{file = "pydantic-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:93b9f265329d9827f39f0fca68f5d72cc8321881cdc519a1304fa73b9f8a75bd"},
|
||||||
{file = "pydantic-1.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:51f11c8bbf794a68086540da099aae4a9107447c7a9d63151edbb7d50110cf21"},
|
{file = "pydantic-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2c753d355126ddd1eefeb167fa61c7037ecd30b98e7ebecdc0d1da463b4ea09"},
|
||||||
{file = "pydantic-1.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6100d7862371115c40be55cc4b8d766a74b1d0dbaf99dbfe72bb4bac0faf89ed"},
|
{file = "pydantic-1.5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8433dbb87246c0f562af75d00fa80155b74e4f6924b0db6a2078a3cd2f11c6c4"},
|
||||||
{file = "pydantic-1.4-py36.py37.py38-none-any.whl", hash = "sha256:72184c1421103cca128300120f8f1185fb42a9ea73a1c9845b1c53db8c026a7d"},
|
{file = "pydantic-1.5.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0a1cdf24e567d42dc762d3fed399bd211a13db2e8462af9dfa93b34c41648efb"},
|
||||||
{file = "pydantic-1.4.tar.gz", hash = "sha256:f17ec336e64d4583311249fb179528e9a2c27c8a2eaf590ec6ec2c6dece7cb3f"},
|
{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 = [
|
pydocstyle = [
|
||||||
{file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"},
|
{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-py3-none-any.whl", hash = "sha256:568fd636c68350bcc54755213fe01966fe0a6c90b386c0776425944a0382abef"},
|
||||||
{file = "win32_setctime-1.0.1.tar.gz", hash = "sha256:b47e5023ec7f0b4962950902b15bc56464a380d869f59d27dbf9ab423b23e8f9"},
|
{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 = [
|
zipp = [
|
||||||
{file = "zipp-3.0.0-py3-none-any.whl", hash = "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2"},
|
{file = "zipp-3.0.0-py3-none-any.whl", hash = "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2"},
|
||||||
{file = "zipp-3.0.0.tar.gz", hash = "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a"},
|
{file = "zipp-3.0.0.tar.gz", hash = "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a"},
|
||||||
|
@@ -34,6 +34,7 @@ uvloop = "^0.14.0"
|
|||||||
inquirer = "^2.6.3"
|
inquirer = "^2.6.3"
|
||||||
paramiko = "^2.7.1"
|
paramiko = "^2.7.1"
|
||||||
gunicorn = "^20.0.4"
|
gunicorn = "^20.0.4"
|
||||||
|
xmltodict = "^0.12.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
bandit = "^1.6.2"
|
bandit = "^1.6.2"
|
||||||
|
Reference in New Issue
Block a user