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
|
||||
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
|
||||
|
@@ -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,
|
||||
|
@@ -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):
|
||||
|
@@ -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 \
|
||||
|
@@ -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"
|
||||
|
@@ -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):
|
||||
|
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,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 = (
|
||||
<AnimatePresence key="resetButton">
|
||||
<AnimatedFlex
|
||||
layoutTransition={headerTransition}
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0, width: "unset" }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
alignItems="center"
|
||||
mb={[null, "auto"]}
|
||||
ml={resetButtonMl[isSubmitting]}
|
||||
display={isSubmitting ? "flex" : "none"}
|
||||
>
|
||||
<AnimatedResetButton isSubmitting={isSubmitting} onClick={handleFormReset} />
|
||||
</AnimatedFlex>
|
||||
</AnimatePresence>
|
||||
);
|
||||
const title = (
|
||||
<AnimatedFlex
|
||||
key="title"
|
||||
px={1}
|
||||
alignItems={isSubmitting ? "center" : ["center", "center", "flex-end", "flex-end"]}
|
||||
positionTransition={headerTransition}
|
||||
initial={{ scale: 0.5 }}
|
||||
animate={
|
||||
isSubmitting && web.text.title_mode === "text_only"
|
||||
? "smallText"
|
||||
: isSubmitting && web.text.title_mode !== "text_only"
|
||||
? "smallLogo"
|
||||
: "fullSize"
|
||||
}
|
||||
variants={titleVariants[mediaSize]}
|
||||
justifyContent={titleJustify[isSubmitting]}
|
||||
mb={[null, isSubmitting ? "auto" : null]}
|
||||
mt={[null, isSubmitting ? null : "auto"]}
|
||||
maxW={widthMap[web.text.title_mode]}
|
||||
flex="1 0 0"
|
||||
minH={titleHeight[isSubmitting]}
|
||||
>
|
||||
<Title isSubmitting={isSubmitting} onClick={handleFormReset} />
|
||||
</AnimatedFlex>
|
||||
);
|
||||
const colorModeToggle = (
|
||||
<AnimatedFlex
|
||||
layoutTransition={headerTransition}
|
||||
key="colorModeToggle"
|
||||
alignItems="center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
mb={[null, "auto"]}
|
||||
mr={isSubmitting ? null : 2}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={colorSwitch[colorMode]}
|
||||
variant="ghost"
|
||||
color="current"
|
||||
px={4}
|
||||
p={null}
|
||||
fontSize="20px"
|
||||
onClick={toggleColorMode}
|
||||
icon={icon[colorMode]}
|
||||
/>
|
||||
</AnimatedFlex>
|
||||
);
|
||||
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 (
|
||||
<Flex
|
||||
px={2}
|
||||
zIndex="4"
|
||||
as="header"
|
||||
width="full"
|
||||
flex="0 1 auto"
|
||||
bg={bg[colorMode]}
|
||||
color="gray.500"
|
||||
{...props}
|
||||
>
|
||||
<Flex
|
||||
w="100%"
|
||||
mx="auto"
|
||||
pt={6}
|
||||
justify="space-between"
|
||||
flex="1 0 auto"
|
||||
alignItems={isSubmitting ? "center" : "flex-start"}
|
||||
>
|
||||
{layout[isSubmitting][mediaSize]}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
export default ({ layoutRef, ...props }) => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { web } = useConfig();
|
||||
const { mediaSize } = useMedia();
|
||||
const { isSubmitting, resetForm } = useHyperglassState();
|
||||
const handleFormReset = () => {
|
||||
resetForm(layoutRef);
|
||||
};
|
||||
const resetButton = (
|
||||
<AnimatePresence key="resetButton">
|
||||
<AnimatedFlex
|
||||
layoutTransition={headerTransition}
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0, width: "unset" }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
alignItems="center"
|
||||
mb={[null, "auto"]}
|
||||
ml={resetButtonMl[isSubmitting]}
|
||||
display={isSubmitting ? "flex" : "none"}
|
||||
>
|
||||
<AnimatedResetButton
|
||||
isSubmitting={isSubmitting}
|
||||
onClick={handleFormReset}
|
||||
/>
|
||||
</AnimatedFlex>
|
||||
</AnimatePresence>
|
||||
);
|
||||
const title = (
|
||||
<AnimatedFlex
|
||||
key="title"
|
||||
px={1}
|
||||
alignItems={
|
||||
isSubmitting ? "center" : ["center", "center", "flex-end", "flex-end"]
|
||||
}
|
||||
positionTransition={headerTransition}
|
||||
initial={{ scale: 0.5 }}
|
||||
animate={
|
||||
isSubmitting && web.text.title_mode === "text_only"
|
||||
? "smallText"
|
||||
: isSubmitting && web.text.title_mode !== "text_only"
|
||||
? "smallLogo"
|
||||
: "fullSize"
|
||||
}
|
||||
variants={titleVariants[mediaSize]}
|
||||
justifyContent={titleJustify[isSubmitting]}
|
||||
mb={[null, isSubmitting ? "auto" : null]}
|
||||
mt={[null, isSubmitting ? null : "auto"]}
|
||||
maxW={widthMap[web.text.title_mode]}
|
||||
flex="1 0 0"
|
||||
minH={titleHeight[isSubmitting]}
|
||||
>
|
||||
<Title isSubmitting={isSubmitting} onClick={handleFormReset} />
|
||||
</AnimatedFlex>
|
||||
);
|
||||
const colorModeToggle = (
|
||||
<AnimatedFlex
|
||||
layoutTransition={headerTransition}
|
||||
key="colorModeToggle"
|
||||
alignItems="center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
mb={[null, "auto"]}
|
||||
mr={isSubmitting ? null : 2}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={colorSwitch[colorMode]}
|
||||
variant="ghost"
|
||||
color="current"
|
||||
px={4}
|
||||
p={null}
|
||||
fontSize="20px"
|
||||
onClick={toggleColorMode}
|
||||
icon={icon[colorMode]}
|
||||
/>
|
||||
</AnimatedFlex>
|
||||
);
|
||||
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 (
|
||||
<Flex
|
||||
px={2}
|
||||
zIndex="4"
|
||||
as="header"
|
||||
width="full"
|
||||
flex="0 1 auto"
|
||||
bg={bg[colorMode]}
|
||||
color="gray.500"
|
||||
{...props}
|
||||
>
|
||||
<Flex
|
||||
w="100%"
|
||||
mx="auto"
|
||||
pt={6}
|
||||
justify="space-between"
|
||||
flex="1 0 auto"
|
||||
alignItems={isSubmitting ? "center" : "flex-start"}
|
||||
>
|
||||
{layout[isSubmitting][mediaSize]}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@@ -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 }) => {
|
||||
<ThemeProvider theme={theme}>
|
||||
<ColorModeProvider value={config.web.theme.default_color_mode ?? null}>
|
||||
<CSSReset />
|
||||
<MediaProvider theme={theme}>{children}</MediaProvider>
|
||||
<MediaProvider theme={theme}>
|
||||
<StateProvider>{children}</StateProvider>
|
||||
</MediaProvider>
|
||||
</ColorModeProvider>
|
||||
</ThemeProvider>
|
||||
</HyperglassContext.Provider>
|
||||
@@ -29,3 +36,7 @@ export const HyperglassProvider = ({ config, children }) => {
|
||||
};
|
||||
|
||||
export default () => useContext(HyperglassContext);
|
||||
|
||||
export const useHyperglassState = _useHyperglassState;
|
||||
|
||||
export const useMedia = _useMedia;
|
||||
|
@@ -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 (
|
||||
<>
|
||||
<Meta />
|
||||
@@ -45,10 +29,7 @@ const Layout = () => {
|
||||
color={color[colorMode]}
|
||||
>
|
||||
<Flex px={2} flex="0 1 auto" flexDirection="column">
|
||||
<Header
|
||||
isSubmitting={isSubmitting}
|
||||
handleFormReset={handleFormReset}
|
||||
/>
|
||||
<Header layoutRef={containerRef} />
|
||||
</Flex>
|
||||
<Flex
|
||||
px={2}
|
||||
@@ -61,30 +42,7 @@ const Layout = () => {
|
||||
justifyContent="start"
|
||||
flexDirection="column"
|
||||
>
|
||||
{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>
|
||||
{children}
|
||||
</Flex>
|
||||
<Footer />
|
||||
{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;
|
@@ -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 <MediaContext.Provider value={value}>{children}</MediaContext.Provider>;
|
||||
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 (
|
||||
<MediaContext.Provider value={value}>{children}</MediaContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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-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",
|
||||
|
@@ -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 = () => <Layout />;
|
||||
const Index = () => <LookingGlass />;
|
||||
|
||||
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;
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
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"
|
||||
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"
|
||||
|
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"
|
||||
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"},
|
||||
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user