From f40004b38f01171c3d9ca062233fa13ea4f01dc9 Mon Sep 17 00:00:00 2001 From: checktheroads Date: Wed, 23 Jun 2021 19:11:59 -0700 Subject: [PATCH] continue generic command work --- hyperglass/configuration/main.py | 11 +-- hyperglass/models/api/query.py | 73 ++++++++++++------ hyperglass/models/commands/generic.py | 1 + hyperglass/ui/components/footer/footer.tsx | 7 +- hyperglass/ui/components/form/field.tsx | 21 +++-- hyperglass/ui/components/form/queryType.tsx | 23 +++--- hyperglass/ui/components/layout/layout.tsx | 4 +- hyperglass/ui/components/lookingGlass.tsx | 85 +++++++++++---------- hyperglass/ui/components/results/tags.tsx | 7 +- hyperglass/ui/hooks/types.ts | 4 +- hyperglass/ui/hooks/useLGState.ts | 12 ++- hyperglass/ui/types/config.ts | 1 + hyperglass/ui/types/globals.d.ts | 2 + hyperglass/ui/types/guards.ts | 2 +- 14 files changed, 152 insertions(+), 101 deletions(-) diff --git a/hyperglass/configuration/main.py b/hyperglass/configuration/main.py index 37cb644..2ab26dd 100644 --- a/hyperglass/configuration/main.py +++ b/hyperglass/configuration/main.py @@ -26,20 +26,13 @@ from hyperglass.constants import ( from hyperglass.exceptions import ConfigError, ConfigMissing from hyperglass.util.files import check_path -# from hyperglass.models.commands import Commands from hyperglass.models.commands.generic import Directive from hyperglass.models.config.params import Params -from hyperglass.models.config.devices import Devices, Device -from hyperglass.configuration.defaults import ( - CREDIT, - DEFAULT_HELP, - DEFAULT_TERMS, - DEFAULT_DETAILS, -) +from hyperglass.models.config.devices import Devices # Local from .markdown import get_markdown -from .validation import validate_config, validate_nos_commands +from .validation import validate_config set_app_path(required=True) diff --git a/hyperglass/models/api/query.py b/hyperglass/models/api/query.py index 793efc5..8246600 100644 --- a/hyperglass/models/api/query.py +++ b/hyperglass/models/api/query.py @@ -5,6 +5,7 @@ import json import hashlib import secrets from datetime import datetime +from typing import Optional # Third Party from pydantic import BaseModel, StrictStr, constr, validator @@ -22,6 +23,7 @@ from .validators import ( validate_community_select, ) from ..config.vrf import Vrf +from ..commands.generic import Directive def get_vrf_object(vrf_name: str) -> Vrf: @@ -41,12 +43,23 @@ def get_vrf_object(vrf_name: str) -> Vrf: raise InputInvalid(params.messages.vrf_not_found, vrf_name=vrf_name) +def get_directive(group: str) -> Optional[Directive]: + for device in devices.objects: + for command in device.commands: + if group in command.groups: + return command + # TODO: Move this to a param + # raise InputInvalid("Group {group} not found", group=group) + return None + + class Query(BaseModel): """Validation model for input query parameters.""" query_location: StrictStr query_type: SupportedQuery - query_vrf: StrictStr + # query_vrf: StrictStr + query_group: StrictStr query_target: constr(strip_whitespace=True, min_length=1) class Config: @@ -64,7 +77,12 @@ class Query(BaseModel): "description": "Type of Query to Execute", "example": "bgp_route", }, - "query_vrf": { + # "query_vrf": { + # "title": params.web.text.query_vrf, + # "description": "Routing Table/VRF", + # "example": "default", + # }, + "query_group": { "title": params.web.text.query_vrf, "description": "Routing Table/VRF", "example": "default", @@ -88,7 +106,7 @@ class Query(BaseModel): """Represent only the query fields.""" return ( f"Query(query_location={str(self.query_location)}, " - f"query_type={str(self.query_type)}, query_vrf={str(self.query_vrf)}, " + f"query_type={str(self.query_type)}, query_group={str(self.query_group)}, " f"query_target={str(self.query_target)})" ) @@ -108,7 +126,7 @@ class Query(BaseModel): items = ( f"query_location={self.query_location}", f"query_type={self.query_type}", - f"query_vrf={self.query_vrf.name}", + f"query_group={self.query_group}", f"query_target={str(self.query_target)}", ) return f'Query({", ".join(items)})' @@ -130,14 +148,14 @@ class Query(BaseModel): items = { "query_location": self.device.name, "query_type": self.query.display_name, - "query_vrf": self.query_vrf.display_name, + "query_group": self.query_group, "query_target": str(self.query_target), } else: items = { "query_location": self.query_location, "query_type": self.query_type, - "query_vrf": self.query_vrf._id, + "query_group": self.query_group, "query_target": str(self.query_target), } return items @@ -177,26 +195,35 @@ class Query(BaseModel): ) return value - @validator("query_vrf") - def validate_query_vrf(cls, value, values): - """Ensure query_vrf is defined.""" + # @validator("query_vrf") + # def validate_query_vrf(cls, value, values): + # """Ensure query_vrf is defined.""" - vrf_object = get_vrf_object(value) - device = devices[values["query_location"]] - device_vrf = None + # vrf_object = get_vrf_object(value) + # device = devices[values["query_location"]] + # device_vrf = None - for vrf in device.vrfs: - if vrf == vrf_object: - device_vrf = vrf - break + # for vrf in device.vrfs: + # if vrf == vrf_object: + # device_vrf = vrf + # break - if device_vrf is None: - raise InputInvalid( - params.messages.vrf_not_associated, - vrf_name=vrf_object.display_name, - device_name=device.name, - ) - return device_vrf + # if device_vrf is None: + # raise InputInvalid( + # params.messages.vrf_not_associated, + # vrf_name=vrf_object.display_name, + # device_name=device.name, + # ) + # return device_vrf + + # @validator("query_group") + # def validate_query_group(cls, value, values): + # """Ensure query_vrf is defined.""" + + # obj = get_directive(value) + # if obj is not None: + # ... + # return device_vrf @validator("query_target") def validate_query_target(cls, value, values): diff --git a/hyperglass/models/commands/generic.py b/hyperglass/models/commands/generic.py index 6a19d18..1b609bb 100644 --- a/hyperglass/models/commands/generic.py +++ b/hyperglass/models/commands/generic.py @@ -102,6 +102,7 @@ class Directive(HyperglassModel): def frontend(self, params: Params) -> Dict: value = { + "id": self.id, "name": self.name, "field_type": self.field_type, "groups": self.groups, diff --git a/hyperglass/ui/components/footer/footer.tsx b/hyperglass/ui/components/footer/footer.tsx index 80a28a9..b26d628 100644 --- a/hyperglass/ui/components/footer/footer.tsx +++ b/hyperglass/ui/components/footer/footer.tsx @@ -77,13 +77,16 @@ export const Footer: React.FC = () => { if (item.show_icon) { icon.rightIcon = ; } - return ; + return ; } else if (isMenu(item)) { - return ; + return ( + + ); } })} } diff --git a/hyperglass/ui/components/form/field.tsx b/hyperglass/ui/components/form/field.tsx index fab397b..bbaf54e 100644 --- a/hyperglass/ui/components/form/field.tsx +++ b/hyperglass/ui/components/form/field.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import { Flex, FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react'; import { useFormContext } from 'react-hook-form'; import { If } from '~/components'; @@ -13,15 +14,19 @@ export const FormField: React.FC = (props: TField) => { const errorColor = useColorValue('red.500', 'red.300'); const opacity = useBooleanValue(hiddenLabels, 0, undefined); + const [error, setError] = useState>(null); + const { formState: { errors }, } = useFormContext(); - const error = name in errors && (errors[name] as FieldError); - - if (error !== false) { - console.warn(`Error on field '${label}': ${error.message}`); - } + useEffect(() => { + if (name in errors) { + console.dir(errors); + setError(errors[name]); + console.warn(`Error on field '${label}': ${error?.message}`); + } + }, [error, errors, setError]); return ( = (props: TField) => { w="100%" maxW="100%" flexDir="column" + isInvalid={error !== null} my={{ base: 2, lg: 4 }} - isInvalid={error !== false} {...rest} > = (props: TField) => { opacity={opacity} alignItems="center" justifyContent="space-between" - color={error !== false ? errorColor : labelColor} + color={error !== null ? errorColor : labelColor} > {label} {labelAddOn} @@ -52,7 +57,7 @@ export const FormField: React.FC = (props: TField) => { {fieldAddOn} - {error && error.message} + {error?.message} ); }; diff --git a/hyperglass/ui/components/form/queryType.tsx b/hyperglass/ui/components/form/queryType.tsx index e7836f5..77c4e07 100644 --- a/hyperglass/ui/components/form/queryType.tsx +++ b/hyperglass/ui/components/form/queryType.tsx @@ -14,16 +14,16 @@ import type { TQuerySelectField } from './types'; // .map(q => ({ value: q.name, label: q.display_name })); // } -function* buildOptions(networks: TNetwork[]): Generator { - for (const net of networks) { - for (const loc of net.locations) { - for (const directive of loc.directives) { - const { name } = directive; - yield { value: name, label: name }; - } - } - } -} +// function* buildOptions(networks: TNetwork[]): Generator { +// for (const net of networks) { +// for (const loc of net.locations) { +// for (const directive of loc.directives) { +// const { name } = directive; +// yield { value: name, label: name }; +// } +// } +// } +// } export const QueryType: React.FC = (props: TQuerySelectField) => { const { onChange, label } = props; @@ -67,7 +67,8 @@ export const QueryType: React.FC = (props: TQuerySelectField) aria-label={label} onChange={handleChange} value={exportState(selections.queryType.value)} - isError={typeof errors.query_type !== 'undefined'} + // isError={typeof errors.query_type !== 'undefined'} + isError={'query_type' in errors} /> ); }; diff --git a/hyperglass/ui/components/layout/layout.tsx b/hyperglass/ui/components/layout/layout.tsx index 6810f3f..e253520 100644 --- a/hyperglass/ui/components/layout/layout.tsx +++ b/hyperglass/ui/components/layout/layout.tsx @@ -5,9 +5,11 @@ import { Frame } from './frame'; export const Layout: React.FC = () => { const { formReady } = useLGMethods(); + const ready = formReady(); + console.log('ready', ready); return ( - {formReady() ? ( + {ready ? ( ) : ( diff --git a/hyperglass/ui/components/lookingGlass.tsx b/hyperglass/ui/components/lookingGlass.tsx index a12db52..10b215b 100644 --- a/hyperglass/ui/components/lookingGlass.tsx +++ b/hyperglass/ui/components/lookingGlass.tsx @@ -19,7 +19,7 @@ import { } from '~/components'; import { useConfig } from '~/context'; import { useStrf, useGreeting, useDevice, useLGState, useLGMethods } from '~/hooks'; -import { isQueryType, isQueryContent, isString, isQueryField } from '~/types'; +import { isQueryType, isQueryContent, isString, isQueryField, TDirective } from '~/types'; import type { TFormData, TDeviceVrf, OnChangeArgs } from '~/types'; @@ -52,34 +52,6 @@ export const LookingGlass: React.FC = () => { const noQueryLoc = useStrf(messages.no_input, { field: web.text.query_location }); const noQueryTarget = useStrf(messages.no_input, { field: web.text.query_target }); - const formSchema = vest.create((data: TFormData = {} as TFormData) => { - test('query_location', noQueryLoc, () => { - enforce(data.query_location).isArrayOf(enforce.isString()).isNotEmpty(); - }); - test('query_target', noQueryTarget, () => { - enforce(data.query_target).longerThan(1); - }); - test('query_type', noQueryType, () => { - enforce(data.query_type).anyOf( - enforce.equals('bgp_route'), - enforce.equals('bgp_community'), - enforce.equals('bgp_aspath'), - enforce.equals('ping'), - enforce.equals('traceroute'), - ); - }); - test('query_vrf', 'Query VRF is empty', () => { - enforce(data.query_vrf).isString(); - }); - }); - - const formInstance = useForm({ - resolver: vestResolver(formSchema), - defaultValues: { query_vrf: 'default', query_target: '', query_location: [], query_type: '' }, - }); - - const { handleSubmit, register, setValue, setError, clearErrors } = formInstance; - const { availableGroups, queryVrf, @@ -95,7 +67,37 @@ export const LookingGlass: React.FC = () => { selections, } = useLGState(); - const { resolvedOpen, resetForm } = useLGMethods(); + const queryTypes = useMemo(() => availableTypes.map(t => t.id.value), [availableTypes.length]); + + const formSchema = vest.create((data: TFormData = {} as TFormData) => { + test('query_location', noQueryLoc, () => { + enforce(data.query_location).isArrayOf(enforce.isString()).isNotEmpty(); + }); + test('query_target', noQueryTarget, () => { + enforce(data.query_target).longerThan(1); + }); + test('query_type', noQueryType, () => { + enforce(data.query_type).inside(queryTypes); + }); + test('query_group', 'Query Group is empty', () => { + enforce(data.query_group).isString(); + }); + }); + + const formInstance = useForm({ + resolver: vestResolver(formSchema), + defaultValues: { + // query_vrf: 'default', + query_target: '', + query_location: [], + query_type: '', + query_group: '', + }, + }); + + const { handleSubmit, register, setValue, setError, clearErrors } = formInstance; + + const { resolvedOpen, resetForm, getDirective } = useLGMethods(); const isFqdnQuery = useIsFqdn(queryTarget.value, queryType.value); @@ -115,6 +117,13 @@ export const LookingGlass: React.FC = () => { }, [queryType.value]); function submitHandler() { + console.table({ + 'Query Location': queryLocation.value, + 'Query Type': queryType.value, + 'Query Group': queryGroup.value, + 'Query Target': queryTarget.value, + 'Selected Directive': selectedDirective?.name ?? null, + }); /** * Before submitting a query, make sure the greeting is acknowledged if required. This should * be handled before loading the app, but people be sneaky. @@ -153,7 +162,7 @@ export const LookingGlass: React.FC = () => { const allVrfs = [] as TDeviceVrf[][]; const locationNames = [] as string[]; const allGroups = [] as string[][]; - const allTypes = [] as string[][]; + const allTypes = [] as TDirective[][]; const allDevices = []; queryLocation.set(locations); @@ -187,7 +196,8 @@ export const LookingGlass: React.FC = () => { for (const directive of device.directives) { if (directive.groups.includes(group)) { // allTypes.add(directive.name); - allTypes.push(device.directives.map(d => d.name)); + allTypes.push(device.directives); + // allTypes.push(device.directives.map(d => d.name)); } } } @@ -215,7 +225,7 @@ export const LookingGlass: React.FC = () => { message: `${locationNames.join(', ')} have no query types in common.`, }); } else if (intersectingTypes.length === 1) { - queryType.set(intersectingTypes[0]); + queryType.set(intersectingTypes[0].id); } } @@ -263,20 +273,13 @@ export const LookingGlass: React.FC = () => { // queryGroup.set(e.value); handleGroupChange(e.value); } - console.table({ - 'Query Location': queryLocation.value, - 'Query Type': queryType.value, - 'Query Group': queryGroup.value, - 'Query Target': queryTarget.value, - 'Selected Directive': selectedDirective?.name ?? null, - }); } useEffect(() => { register('query_location', { required: true }); register('query_target', { required: true }); register('query_type', { required: true }); - register('query_vrf'); + register('query_group'); }, [register]); return ( diff --git a/hyperglass/ui/components/results/tags.tsx b/hyperglass/ui/components/results/tags.tsx index 26de570..f8f4ca8 100644 --- a/hyperglass/ui/components/results/tags.tsx +++ b/hyperglass/ui/components/results/tags.tsx @@ -64,8 +64,8 @@ export const Tags: React.FC = () => { queryTypeLabel = queries[queryType.value].display_name; } - const getVrf = useVrf(); - const vrf = getVrf(queryVrf.value); + // const getVrf = useVrf(); + // const vrf = getVrf(queryVrf.value); return ( {