continue generic command work

This commit is contained in:
checktheroads 2021-06-23 19:11:59 -07:00
parent 029649e44f
commit f40004b38f
14 changed files with 152 additions and 101 deletions

View file

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

View file

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

View file

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

View file

@ -77,13 +77,16 @@ export const Footer: React.FC = () => {
if (item.show_icon) {
icon.rightIcon = <ExtIcon />;
}
return <FooterLink href={url} title={item.title} {...icon} />;
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
} else if (isMenu(item)) {
return <FooterButton side="right" content={item.content} title={item.title} />;
return (
<FooterButton key={item.title} side="right" content={item.content} title={item.title} />
);
}
})}
<If c={web.credit.enable}>
<FooterButton
key="credit"
side="right"
content={content.credit}
title={<Icon as={CodeIcon} boxSize={size} />}

View file

@ -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<TField> = (props: TField) => {
const errorColor = useColorValue('red.500', 'red.300');
const opacity = useBooleanValue(hiddenLabels, 0, undefined);
const [error, setError] = useState<Nullable<FieldError>>(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 (
<FormControl
@ -29,8 +34,8 @@ export const FormField: React.FC<TField> = (props: TField) => {
w="100%"
maxW="100%"
flexDir="column"
isInvalid={error !== null}
my={{ base: 2, lg: 4 }}
isInvalid={error !== false}
{...rest}
>
<FormLabel
@ -41,7 +46,7 @@ export const FormField: React.FC<TField> = (props: TField) => {
opacity={opacity}
alignItems="center"
justifyContent="space-between"
color={error !== false ? errorColor : labelColor}
color={error !== null ? errorColor : labelColor}
>
{label}
<If c={typeof labelAddOn !== 'undefined'}>{labelAddOn}</If>
@ -52,7 +57,7 @@ export const FormField: React.FC<TField> = (props: TField) => {
{fieldAddOn}
</Flex>
</If>
<FormErrorMessage opacity={opacity}>{error && error.message}</FormErrorMessage>
<FormErrorMessage opacity={opacity}>{error?.message}</FormErrorMessage>
</FormControl>
);
};

View file

@ -14,16 +14,16 @@ import type { TQuerySelectField } from './types';
// .map(q => ({ value: q.name, label: q.display_name }));
// }
function* buildOptions(networks: TNetwork[]): Generator<TSelectOption> {
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<TSelectOption> {
// 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<TQuerySelectField> = (props: TQuerySelectField) => {
const { onChange, label } = props;
@ -67,7 +67,8 @@ export const QueryType: React.FC<TQuerySelectField> = (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}
/>
);
};

View file

@ -5,9 +5,11 @@ import { Frame } from './frame';
export const Layout: React.FC = () => {
const { formReady } = useLGMethods();
const ready = formReady();
console.log('ready', ready);
return (
<Frame>
{formReady() ? (
{ready ? (
<Results />
) : (
<AnimatePresence>

View file

@ -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<TFormData>({
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<TFormData>({
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 (

View file

@ -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 (
<Box
@ -115,7 +115,8 @@ export const Tags: React.FC = () => {
<Label
bg={vrfBg}
label={web.text.query_vrf}
value={vrf.display_name}
// value={vrf.display_name}
value="fix me"
fontSize={{ base: 'xs', md: 'sm' }}
/>
</motion.div>

View file

@ -49,6 +49,7 @@ export interface TMethodsExtension {
formReady(): boolean;
resetForm(): void;
stateExporter<O extends unknown>(o: O): O | null;
getDirective(n: string): Nullable<State<TDirective>>;
}
export type TLGState = {
@ -65,7 +66,7 @@ export type TLGState = {
queryLocation: string[];
availVrfs: TDeviceVrf[];
availableGroups: string[];
availableTypes: string[];
availableTypes: TDirective[];
resolvedIsOpen: boolean;
selections: TSelections;
responses: { [d: string]: TQueryResponse };
@ -79,6 +80,7 @@ export type TLGStateHandlers = {
formReady(): boolean;
resetForm(): void;
stateExporter<O extends unknown>(o: O): O | null;
getDirective(n: string): Nullable<State<TDirective>>;
};
export type UseStrfArgs = { [k: string]: unknown } | string;

View file

@ -5,6 +5,7 @@ import { all } from '~/util';
import type { State, PluginStateControl, Plugin } from '@hookstate/core';
import type { TLGState, TLGStateHandlers, TMethodsExtension } from './types';
import { TDirective } from '~/types';
const MethodsId = Symbol('Methods');
@ -37,6 +38,14 @@ class MethodsInstance {
}
}
public getDirective(state: State<TLGState>, name: string): Nullable<State<TDirective>> {
const [directive] = state.availableTypes.filter(t => t.name.value === name);
if (typeof directive !== 'undefined') {
return directive;
}
return null;
}
/**
* Determine if the form is ready for submission, e.g. all fields have values and isSubmitting
* has been set to true. This ultimately controls the UI layout.
@ -46,7 +55,7 @@ class MethodsInstance {
state.isSubmitting.value &&
all(
...[
state.queryVrf.value !== '',
// state.queryVrf.value !== '',
state.queryType.value !== '',
state.queryGroup.value !== '',
state.queryTarget.value !== '',
@ -122,6 +131,7 @@ function Methods(inst?: State<TLGState>): Plugin | TMethodsExtension {
resolvedClose: () => instance.resolvedClose(inst),
getResponse: device => instance.getResponse(inst, device),
stateExporter: obj => instance.stateExporter(obj),
getDirective: name => instance.getDirective(inst, name),
};
}
return {

View file

@ -134,6 +134,7 @@ export interface TDeviceVrf extends TDeviceVrfBase {
}
interface TDirectiveBase {
id: string;
name: string;
field_type: 'text' | 'select' | null;
description: string;

View file

@ -4,6 +4,8 @@ declare global {
type Dict<T = string> = Record<string, T>;
type ValueOf<T> = T[keyof T];
type Nullable<T> = T | null;
type TRPKIStates = 0 | 1 | 2 | 3;
type TResponseLevel = 'success' | 'warning' | 'error' | 'danger';

View file

@ -62,5 +62,5 @@ export function isState<S>(a: any): a is State<NonNullable<S>> {
* Determine if a form field name is a valid form key name.
*/
export function isQueryField(field: string): field is keyof TFormData {
return ['query_location', 'query_type', 'query_vrf', 'query_target'].includes(field);
return ['query_location', 'query_type', 'query_group', 'query_target'].includes(field);
}