From 2895d3ae4661fd5ff0b9be9569a1f26535b4368d Mon Sep 17 00:00:00 2001 From: checktheroads Date: Sat, 18 Apr 2020 07:57:55 -0700 Subject: [PATCH] allow static bgp community definitions --- hyperglass/configuration/models/queries.py | 51 ++-- hyperglass/ui/components/ChakraSelect.js | 282 ++++++++++---------- hyperglass/ui/components/CommunitySelect.js | 41 +++ hyperglass/ui/components/HyperglassForm.js | 50 ++-- hyperglass/ui/components/QueryLocation.js | 13 +- hyperglass/ui/components/QueryTarget.js | 15 +- hyperglass/ui/components/ResolvedTarget.js | 17 +- 7 files changed, 268 insertions(+), 201 deletions(-) create mode 100644 hyperglass/ui/components/CommunitySelect.js diff --git a/hyperglass/configuration/models/queries.py b/hyperglass/configuration/models/queries.py index ea11b8b..3aea4fd 100644 --- a/hyperglass/configuration/models/queries.py +++ b/hyperglass/configuration/models/queries.py @@ -1,5 +1,8 @@ """Validate query configuration parameters.""" +# Standard Library +from typing import List + # Third Party from pydantic import Field, StrictStr, StrictBool, constr @@ -8,25 +11,7 @@ from hyperglass.models import HyperglassModel from hyperglass.constants import SUPPORTED_QUERY_TYPES -class HyperglassLevel3(HyperglassModel): - """Automatic docs sorting subclass.""" - - class Config: - """Pydantic model configuration.""" - - schema_extra = {"level": 3} - - -class HyperglassLevel4(HyperglassModel): - """Automatic docs sorting subclass.""" - - class Config: - """Pydantic model configuration.""" - - schema_extra = {"level": 4} - - -class BgpCommunityPattern(HyperglassLevel4): +class BgpCommunityPattern(HyperglassModel): """Validation model for bgp_community regex patterns.""" decimal: StrictStr = Field( @@ -54,7 +39,7 @@ class BgpCommunityPattern(HyperglassLevel4): ) -class BgpAsPathPattern(HyperglassLevel4): +class BgpAsPathPattern(HyperglassModel): """Validation model for bgp_aspath regex patterns.""" mode: constr(regex=r"asplain|asdot") = Field( @@ -82,7 +67,15 @@ class BgpAsPathPattern(HyperglassLevel4): ) -class BgpCommunity(HyperglassLevel3): +class Community(HyperglassModel): + """Validation model for bgp_community communities.""" + + display_name: StrictStr + description: StrictStr + community: StrictStr + + +class BgpCommunity(HyperglassModel): """Validation model for bgp_community configuration.""" enable: StrictBool = Field( @@ -96,9 +89,11 @@ class BgpCommunity(HyperglassLevel3): description="Text displayed for the BGP Community query type in the hyperglas UI.", ) pattern: BgpCommunityPattern = BgpCommunityPattern() + mode: constr(regex=r"(input|select)") = "input" + communities: List[Community] = [] -class BgpRoute(HyperglassLevel3): +class BgpRoute(HyperglassModel): """Validation model for bgp_route configuration.""" enable: StrictBool = Field( @@ -111,7 +106,7 @@ class BgpRoute(HyperglassLevel3): ) -class BgpAsPath(HyperglassLevel3): +class BgpAsPath(HyperglassModel): """Validation model for bgp_aspath configuration.""" enable: StrictBool = Field( @@ -127,7 +122,7 @@ class BgpAsPath(HyperglassLevel3): pattern: BgpAsPathPattern = BgpAsPathPattern() -class Ping(HyperglassLevel3): +class Ping(HyperglassModel): """Validation model for ping configuration.""" enable: StrictBool = Field( @@ -140,7 +135,7 @@ class Ping(HyperglassLevel3): ) -class Traceroute(HyperglassLevel3): +class Traceroute(HyperglassModel): """Validation model for traceroute configuration.""" enable: StrictBool = Field( @@ -168,8 +163,9 @@ class Queries(HyperglassModel): query_obj = getattr(self, query) _map[query] = { "name": query, - "display_name": query_obj.display_name, - "enable": query_obj.enable, + **query_obj.export_dict( + include={"display_name", "enable", "mode", "communities"} + ), } return _map @@ -225,4 +221,3 @@ class Queries(HyperglassModel): "description": "Enable, disable, or configure the Traceroute query type.", }, } - schema_extra = {"level": 2} diff --git a/hyperglass/ui/components/ChakraSelect.js b/hyperglass/ui/components/ChakraSelect.js index 356aa67..d7408dc 100644 --- a/hyperglass/ui/components/ChakraSelect.js +++ b/hyperglass/ui/components/ChakraSelect.js @@ -3,139 +3,151 @@ import { Text, useColorMode, useTheme } from "@chakra-ui/core"; import Select from "react-select"; import { opposingColor } from "~/util"; -export default ({ placeholder = "Select...", isFullWidth, size, children, ...props }) => { - const theme = useTheme(); - const { colorMode } = useColorMode(); - const sizeMap = { - lg: { height: theme.space[12] }, - md: { height: theme.space[10] }, - sm: { height: theme.space[8] } - }; - const colorSetPrimaryBg = { dark: theme.colors.primary[300], light: theme.colors.primary[500] }; - const colorSetPrimaryColor = opposingColor(theme, colorSetPrimaryBg[colorMode]); - const bg = { dark: theme.colors.whiteAlpha[100], light: theme.colors.white }; - const color = { dark: theme.colors.whiteAlpha[800], light: theme.colors.black }; - const borderFocused = theme.colors.secondary[500]; - const borderDisabled = theme.colors.whiteAlpha[100]; - const border = { dark: theme.colors.whiteAlpha[50], light: theme.colors.gray[100] }; - const borderRadius = theme.space[1]; - const hoverColor = { dark: theme.colors.whiteAlpha[200], light: theme.colors.gray[300] }; - const { height } = sizeMap[size]; - const optionBgActive = { dark: theme.colors.primary[400], light: theme.colors.primary[600] }; - const optionBgColor = opposingColor(theme, optionBgActive[colorMode]); - const optionSelectedBg = { - dark: theme.colors.whiteAlpha[400], - light: theme.colors.blackAlpha[400] - }; - const optionSelectedColor = opposingColor(theme, optionSelectedBg[colorMode]); - const selectedDisabled = theme.colors.whiteAlpha[400]; - const placeholderColor = { - dark: theme.colors.whiteAlpha[400], - light: theme.colors.gray[400] - }; - const menuBg = { dark: theme.colors.black, light: theme.colors.white }; - const menuColor = { dark: theme.colors.white, light: theme.colors.blackAlpha[800] }; - return ( - ({ + ...base, + minHeight: height, + borderRadius: borderRadius, + width: "100%", + }), + control: (base, state) => ({ + ...base, + minHeight: height, + backgroundColor: bg[colorMode], + color: color[colorMode], + borderColor: state.isDisabled + ? borderDisabled + : state.isFocused + ? borderFocused + : border[colorMode], + borderRadius: borderRadius, + "&:hover": { + borderColor: hoverColor[colorMode], + }, + }), + menu: (base) => ({ + ...base, + backgroundColor: menuBg[colorMode], + borderRadius: borderRadius, + }), + option: (base, state) => ({ + ...base, + backgroundColor: state.isDisabled + ? selectedDisabled + : state.isSelected + ? optionSelectedBg[colorMode] + : state.isFocused + ? colorSetPrimaryBg[colorMode] + : "transparent", + color: state.isDisabled + ? selectedDisabled + : state.isFocused + ? colorSetPrimaryColor + : state.isSelected + ? optionSelectedColor + : menuColor[colorMode], + fontSize: theme.fontSizes[size], + "&:active": { + backgroundColor: optionBgActive[colorMode], + color: optionBgColor, + }, + }), + indicatorSeparator: (base) => ({ + ...base, + backgroundColor: placeholderColor[colorMode], + }), + dropdownIndicator: (base) => ({ + ...base, + color: placeholderColor[colorMode], + "&:hover": { + color: color[colorMode], + }, + }), + valueContainer: (base) => ({ + ...base, + paddingLeft: theme.space[4], + paddingRight: theme.space[4], + }), + multiValue: (base) => ({ + ...base, + backgroundColor: colorSetPrimaryBg[colorMode], + }), + multiValueLabel: (base) => ({ + ...base, color: colorSetPrimaryColor, - backgroundColor: "inherit" - } - }), - singleValue: base => ({ - ...base, - color: color[colorMode], - fontSize: theme.fontSizes[size] - }) - }} - placeholder={ - - {placeholder} - - } - {...props} - > - {children} - - ); -}; + }), + multiValueRemove: (base) => ({ + ...base, + color: colorSetPrimaryColor, + "&:hover": { + color: colorSetPrimaryColor, + backgroundColor: "inherit", + }, + }), + singleValue: (base) => ({ + ...base, + color: color[colorMode], + fontSize: theme.fontSizes[size], + }), + }} + placeholder={ + + {placeholder} + + } + {...props} + > + {children} + + ); + } +); + +ChakraSelect.displayName = "ChakraSelect"; +export default ChakraSelect; diff --git a/hyperglass/ui/components/CommunitySelect.js b/hyperglass/ui/components/CommunitySelect.js new file mode 100644 index 0000000..e085f14 --- /dev/null +++ b/hyperglass/ui/components/CommunitySelect.js @@ -0,0 +1,41 @@ +import * as React from "react"; +import { useEffect } from "react"; +import { Text } from "@chakra-ui/core"; +import { components } from "react-select"; +import ChakraSelect from "~/components/ChakraSelect"; + +const CommunitySelect = ({ name, communities, onChange, register, unregister }) => { + const communitySelections = communities.map((c) => { + return { value: c.community, label: c.display_name, description: c.description }; + }); + const Option = ({ label, data, ...props }) => { + return ( + + {label} + + {data.description} + + + ); + }; + useEffect(() => { + register({ name }); + return () => unregister(name); + }, [name, register, unregister]); + return ( + { + onChange({ field: name, value: e.value || "" }); + }} + options={communitySelections} + components={{ Option }} + /> + ); +}; + +CommunitySelect.displayName = "CommunitySelect"; + +export default CommunitySelect; diff --git a/hyperglass/ui/components/HyperglassForm.js b/hyperglass/ui/components/HyperglassForm.js index 5d0f09f..175509f 100644 --- a/hyperglass/ui/components/HyperglassForm.js +++ b/hyperglass/ui/components/HyperglassForm.js @@ -1,4 +1,5 @@ -import React, { useState, useEffect } from "react"; +import * as React from "react"; +import { useState, useEffect } from "react"; import { Box, Flex } from "@chakra-ui/core"; import { useForm } from "react-hook-form"; import lodash from "lodash"; @@ -9,6 +10,7 @@ import HelpModal from "~/components/HelpModal"; import QueryLocation from "~/components/QueryLocation"; import QueryType from "~/components/QueryType"; import QueryTarget from "~/components/QueryTarget"; +import CommunitySelect from "~/components/CommunitySelect"; import QueryVrf from "~/components/QueryVrf"; import ResolvedTarget from "~/components/ResolvedTarget"; import SubmitButton from "~/components/SubmitButton"; @@ -46,9 +48,9 @@ const FormRow = ({ children, ...props }) => ( const HyperglassForm = React.forwardRef( ({ isSubmitting, setSubmitting, setFormData, greetingAck, setGreetingAck, ...props }, ref) => { const config = useConfig(); - const { handleSubmit, register, setValue, errors } = useForm({ + const { handleSubmit, register, unregister, setValue, errors } = useForm({ validationSchema: formSchema(config), - defaultValues: { query_vrf: "default" }, + defaultValues: { query_vrf: "default", query_target: "" }, }); const [queryLocation, setQueryLocation] = useState([]); @@ -136,10 +138,10 @@ const HyperglassForm = React.forwardRef( useEffect(() => { register({ name: "query_location" }); - register({ name: "query_target" }); register({ name: "query_type" }); register({ name: "query_vrf" }); - }); + }, [register]); + Object.keys(errors).length >= 1 && console.error(errors); return ( - + {queryType === "bgp_community" && + config.queries.bgp_community.mode === "select" ? ( + + ) : ( + + )} diff --git a/hyperglass/ui/components/QueryLocation.js b/hyperglass/ui/components/QueryLocation.js index 6458c5d..abf71b8 100644 --- a/hyperglass/ui/components/QueryLocation.js +++ b/hyperglass/ui/components/QueryLocation.js @@ -1,15 +1,15 @@ import React from "react"; import ChakraSelect from "~/components/ChakraSelect"; -const buildLocations = networks => { +const buildLocations = (networks) => { const locations = []; - networks.map(net => { + networks.map((net) => { const netLocations = []; - net.locations.map(loc => { + net.locations.map((loc) => { netLocations.push({ label: loc.display_name, value: loc.name, - group: net.display_name + group: net.display_name, }); }); locations.push({ label: net.display_name, options: netLocations }); @@ -19,10 +19,10 @@ const buildLocations = networks => { export default ({ locations, onChange }) => { const options = buildLocations(locations); - const handleChange = e => { + const handleChange = (e) => { const selected = []; e && - e.map(sel => { + e.map((sel) => { selected.push(sel.value); }); onChange({ field: "query_location", value: selected }); @@ -34,6 +34,7 @@ export default ({ locations, onChange }) => { onChange={handleChange} options={options} isMulti + closeMenuOnSelect={false} /> ); }; diff --git a/hyperglass/ui/components/QueryTarget.js b/hyperglass/ui/components/QueryTarget.js index e04a403..c37dbdb 100644 --- a/hyperglass/ui/components/QueryTarget.js +++ b/hyperglass/ui/components/QueryTarget.js @@ -1,10 +1,10 @@ -import React, { useState } from "react"; +import React, { useEffect } from "react"; import styled from "@emotion/styled"; import { Input, useColorMode } from "@chakra-ui/core"; const StyledInput = styled(Input)` &::placeholder { - color: ${props => props.placeholderColor}; + color: ${(props) => props.placeholderColor}; } `; @@ -18,13 +18,14 @@ const placeholderColor = { dark: "whiteAlpha.400", light: "gray.400" }; const QueryTarget = ({ placeholder, register, + unregister, setFqdn, name, value, setTarget, resolveTarget, displayValue, - setDisplayValue + setDisplayValue, }) => { const { colorMode } = useColorMode(); @@ -35,15 +36,19 @@ const QueryTarget = ({ setFqdn(false); } }; - const handleChange = e => { + const handleChange = (e) => { setDisplayValue(e.target.value); setTarget({ field: name, value: e.target.value }); }; - const handleKeyDown = e => { + const handleKeyDown = (e) => { if ([9, 13].includes(e.keyCode)) { handleBlur(); } }; + useEffect(() => { + register({ name }); + return () => unregister(name); + }, [register, unregister, name]); return ( <> diff --git a/hyperglass/ui/components/ResolvedTarget.js b/hyperglass/ui/components/ResolvedTarget.js index 0585616..595ce17 100644 --- a/hyperglass/ui/components/ResolvedTarget.js +++ b/hyperglass/ui/components/ResolvedTarget.js @@ -13,7 +13,7 @@ const labelBgSuccess = { dark: "success", light: "success" }; async function containingPrefix(ipAddress) { try { const prefixData = await axios.get("https://stat.ripe.net/data/network-info/data.json", { - params: { resource: ipAddress } + params: { resource: ipAddress }, }); return prefixData.data?.data?.prefix; } catch (err) { @@ -36,32 +36,32 @@ const ResolvedTarget = React.forwardRef( params: { name: fqdnTarget, type: "A" }, headers: { accept: "application/dns-json" }, crossdomain: true, - timeout: 1000 + timeout: 1000, }, 6: { url: dnsUrl, params: { name: fqdnTarget, type: "AAAA" }, headers: { accept: "application/dns-json" }, crossdomain: true, - timeout: 1000 - } + timeout: 1000, + }, }; const [{ data: data4, loading: loading4, error: error4 }] = useAxios(params[4]); const [{ data: data6, loading: loading6, error: error6 }] = useAxios(params[6]); - const handleOverride = overridden => { + const handleOverride = (overridden) => { setTarget({ field: "query_target", value: overridden }); }; - const isSelected = value => { + const isSelected = (value) => { return labelBgStatus[value === queryTarget]; }; - const findAnswer = data => { + const findAnswer = (data) => { return data?.Answer?.filter( - answerData => answerData.type === data?.Question[0]?.type + (answerData) => answerData.type === data?.Question[0]?.type )[0]?.data; }; @@ -74,7 +74,6 @@ const ResolvedTarget = React.forwardRef( handleOverride(findAnswer(data4)); } }, [data4, data6]); - return (