1
0
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:
checktheroads
2020-04-24 11:41:43 -07:00
parent 7ad435fdf2
commit a562c90094
37 changed files with 1791 additions and 453 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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):

View File

@@ -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 \

View File

@@ -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"

View File

@@ -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):

View 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")

View File

@@ -0,0 +1 @@
"""Data models for parsed responses."""

View 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

View 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)")

View 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;

View 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;

View 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;

View File

@@ -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={

View File

@@ -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;

View File

@@ -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 />}

View 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;

View File

@@ -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);

View 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);

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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();
}

View 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",
};

View File

@@ -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",

View File

@@ -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;

View 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;

View File

@@ -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
}; };
}; };

View File

@@ -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;
}; };

View File

@@ -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
View File

@@ -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"},

View File

@@ -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"