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 (
{
diff --git a/hyperglass/ui/hooks/types.ts b/hyperglass/ui/hooks/types.ts
index 74a6bd1..c158e80 100644
--- a/hyperglass/ui/hooks/types.ts
+++ b/hyperglass/ui/hooks/types.ts
@@ -49,6 +49,7 @@ export interface TMethodsExtension {
formReady(): boolean;
resetForm(): void;
stateExporter(o: O): O | null;
+ getDirective(n: string): Nullable>;
}
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: O): O | null;
+ getDirective(n: string): Nullable>;
};
export type UseStrfArgs = { [k: string]: unknown } | string;
diff --git a/hyperglass/ui/hooks/useLGState.ts b/hyperglass/ui/hooks/useLGState.ts
index 51343fa..078e443 100644
--- a/hyperglass/ui/hooks/useLGState.ts
+++ b/hyperglass/ui/hooks/useLGState.ts
@@ -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, name: string): Nullable> {
+ 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): Plugin | TMethodsExtension {
resolvedClose: () => instance.resolvedClose(inst),
getResponse: device => instance.getResponse(inst, device),
stateExporter: obj => instance.stateExporter(obj),
+ getDirective: name => instance.getDirective(inst, name),
};
}
return {
diff --git a/hyperglass/ui/types/config.ts b/hyperglass/ui/types/config.ts
index 3a99357..d1d9978 100644
--- a/hyperglass/ui/types/config.ts
+++ b/hyperglass/ui/types/config.ts
@@ -134,6 +134,7 @@ export interface TDeviceVrf extends TDeviceVrfBase {
}
interface TDirectiveBase {
+ id: string;
name: string;
field_type: 'text' | 'select' | null;
description: string;
diff --git a/hyperglass/ui/types/globals.d.ts b/hyperglass/ui/types/globals.d.ts
index fd8d66c..e6dc9cd 100644
--- a/hyperglass/ui/types/globals.d.ts
+++ b/hyperglass/ui/types/globals.d.ts
@@ -4,6 +4,8 @@ declare global {
type Dict = Record;
type ValueOf = T[keyof T];
+ type Nullable = T | null;
+
type TRPKIStates = 0 | 1 | 2 | 3;
type TResponseLevel = 'success' | 'warning' | 'error' | 'danger';
diff --git a/hyperglass/ui/types/guards.ts b/hyperglass/ui/types/guards.ts
index f6439b5..c67f1bb 100644
--- a/hyperglass/ui/types/guards.ts
+++ b/hyperglass/ui/types/guards.ts
@@ -62,5 +62,5 @@ export function isState(a: any): a is State> {
* 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);
}