Add info support back to directives

This commit is contained in:
thatmattlove 2021-12-18 19:38:13 -07:00
parent 292aa7612b
commit c479a2f2b4
9 changed files with 52 additions and 86 deletions

View file

@ -2,7 +2,7 @@
# Standard Library
import re
from typing import Any, Set, Dict, List, Tuple, Union, Optional
import typing as t
from pathlib import Path
from ipaddress import IPv4Address, IPv6Address
@ -33,7 +33,7 @@ ALL_DEVICE_TYPES = {*DRIVER_MAP.keys(), *CLASS_MAPPER.keys()}
class DirectiveOptions(HyperglassModel, extra="ignore"):
"""Per-device directive options."""
builtins: Union[StrictBool, List[StrictStr]] = True
builtins: t.Union[StrictBool, t.List[StrictStr]] = True
class Device(HyperglassModelWithId, extra="allow"):
@ -41,21 +41,21 @@ class Device(HyperglassModelWithId, extra="allow"):
id: StrictStr
name: StrictStr
description: Optional[StrictStr]
avatar: Optional[FilePath]
address: Union[IPv4Address, IPv6Address, StrictStr]
group: Optional[StrictStr]
description: t.Optional[StrictStr]
avatar: t.Optional[FilePath]
address: t.Union[IPv4Address, IPv6Address, StrictStr]
group: t.Optional[StrictStr]
credential: Credential
proxy: Optional[Proxy]
display_name: Optional[StrictStr]
proxy: t.Optional[Proxy]
display_name: t.Optional[StrictStr]
port: StrictInt = 22
http: HttpConfiguration = HttpConfiguration()
platform: StrictStr
structured_output: Optional[StrictBool]
structured_output: t.Optional[StrictBool]
directives: Directives = Directives()
driver: Optional[SupportedDriver]
driver_config: Dict[str, Any] = {}
attrs: Dict[str, str] = {}
driver: t.Optional[SupportedDriver]
driver_config: t.Dict[str, t.Any] = {}
attrs: t.Dict[str, str] = {}
def __init__(self, **kw) -> None:
"""Check legacy fields and ensure an `id` is set."""
@ -70,7 +70,7 @@ class Device(HyperglassModelWithId, extra="allow"):
return str(self.address)
@staticmethod
def _with_id(values: Dict) -> str:
def _with_id(values: t.Dict) -> str:
"""Generate device id & handle legacy display_name field."""
def generate_id(name: str) -> str:
@ -94,7 +94,7 @@ class Device(HyperglassModelWithId, extra="allow"):
return {"id": device_id, "name": display_name, "display_name": None, **values}
def export_api(self) -> Dict[str, Any]:
def export_api(self) -> t.Dict[str, t.Any]:
"""Export API-facing device fields."""
return {
"id": self.id,
@ -103,7 +103,7 @@ class Device(HyperglassModelWithId, extra="allow"):
}
@property
def directive_commands(self) -> List[str]:
def directive_commands(self) -> t.List[str]:
"""Get all commands associated with the device."""
return [
command
@ -113,7 +113,7 @@ class Device(HyperglassModelWithId, extra="allow"):
]
@property
def directive_ids(self) -> List[str]:
def directive_ids(self) -> t.List[str]:
"""Get all directive IDs associated with the device."""
return [directive.id for directive in self.directives]
@ -146,7 +146,9 @@ class Device(HyperglassModelWithId, extra="allow"):
)
@validator("address")
def validate_address(cls, value, values):
def validate_address(
cls, value: t.Union[IPv4Address, IPv6Address, str], values: t.Dict[str, t.Any]
) -> t.Union[IPv4Address, IPv6Address, str]:
"""Ensure a hostname is resolvable."""
if not isinstance(value, (IPv4Address, IPv6Address)):
@ -160,8 +162,8 @@ class Device(HyperglassModelWithId, extra="allow"):
@validator("avatar")
def validate_avatar(
cls, value: Union[FilePath, None], values: Dict[str, Any]
) -> Union[FilePath, None]:
cls, value: t.Union[FilePath, None], values: t.Dict[str, t.Any]
) -> t.Union[FilePath, None]:
"""Migrate avatar to static directory."""
if value is not None:
# Standard Library
@ -181,7 +183,7 @@ class Device(HyperglassModelWithId, extra="allow"):
return value
@validator("platform", pre=True, always=True)
def validate_platform(cls: "Device", value: Any, values: Dict[str, Any]) -> str:
def validate_platform(cls: "Device", value: t.Any, values: t.Dict[str, t.Any]) -> str:
"""Validate & rewrite device platform, set default `directives`."""
if value is None:
@ -202,7 +204,7 @@ class Device(HyperglassModelWithId, extra="allow"):
return value
@validator("structured_output", pre=True, always=True)
def validate_structured_output(cls, value: bool, values: Dict[str, Any]) -> bool:
def validate_structured_output(cls, value: bool, values: t.Dict[str, t.Any]) -> bool:
"""Validate structured output is supported on the device & set a default."""
if value is True:
@ -221,7 +223,9 @@ class Device(HyperglassModelWithId, extra="allow"):
return value
@validator("directives", pre=True, always=True)
def validate_directives(cls: "Device", value, values) -> "Directives":
def validate_directives(
cls: "Device", value: t.Optional[t.List[str]], values: t.Dict[str, t.Any]
) -> "Directives":
"""Associate directive IDs to loaded directive objects."""
directives = use_state("directives")
@ -234,7 +238,7 @@ class Device(HyperglassModelWithId, extra="allow"):
**{
k: v
for statement in directive_ids
if isinstance(statement, Dict)
if isinstance(statement, t.Dict)
for k, v in statement.items()
}
)
@ -253,14 +257,14 @@ class Device(HyperglassModelWithId, extra="allow"):
if directive_options.builtins is True:
# Add all builtins.
device_directives += builtins
elif isinstance(directive_options.builtins, List):
elif isinstance(directive_options.builtins, t.List):
# If the user provides a list of builtin directives to include, add only those.
device_directives += builtins.matching(*directive_options.builtins)
return device_directives
@validator("driver")
def validate_driver(cls, value: Optional[str], values: Dict) -> Dict:
def validate_driver(cls: "Device", value: t.Optional[str], values: t.Dict[str, t.Any]) -> str:
"""Set the correct driver and override if supported."""
return get_driver(values["platform"], value)
@ -268,25 +272,25 @@ class Device(HyperglassModelWithId, extra="allow"):
class Devices(MultiModel, model=Device, unique_by="id"):
"""Container for all devices."""
def __init__(self, *items: Dict[str, Any]) -> None:
def __init__(self: "Devices", *items: t.Dict[str, t.Any]) -> None:
"""Generate IDs prior to validation."""
with_id = (Device._with_id(item) for item in items)
super().__init__(*with_id)
def export_api(self) -> List[Dict[str, Any]]:
def export_api(self: "Devices") -> t.List[t.Dict[str, t.Any]]:
"""Export API-facing device fields."""
return [d.export_api() for d in self]
def valid_id_or_name(self, value: str) -> bool:
def valid_id_or_name(self: "Devices", value: str) -> bool:
"""Determine if a value is a valid device name or ID."""
for device in self:
if value == device.id or value == device.name:
return True
return False
def directive_plugins(self) -> Dict[Path, Tuple[StrictStr]]:
def directive_plugins(self: "Devices") -> t.Dict[Path, t.Tuple[StrictStr]]:
"""Get a mapping of plugin paths to associated directive IDs."""
result: Dict[Path, Set[StrictStr]] = {}
result: t.Dict[Path, t.Set[StrictStr]] = {}
# Unique set of all directives.
directives = {directive for device in self for directive in device.directives}
# Unique set of all plugin file names.
@ -301,9 +305,8 @@ class Devices(MultiModel, model=Device, unique_by="id"):
# Convert the directive set to a tuple.
return {k: tuple(v) for k, v in result.items()}
def frontend(self) -> List[Dict[str, Any]]:
def frontend(self: "Devices") -> t.List[t.Dict[str, t.Any]]:
"""Export grouped devices for UIParameters."""
params = use_state("params")
groups = {device.group for device in self}
return [
{
@ -317,7 +320,7 @@ class Devices(MultiModel, model=Device, unique_by="id"):
if device.avatar is not None
else None,
"description": device.description,
"directives": [d.frontend(params) for d in device.directives],
"directives": [d.frontend() for d in device.directives],
}
for device in self
if device.group == group

View file

@ -143,10 +143,6 @@ class Params(ParamsPublic, HyperglassModel):
"""Get all validated external common plugins as Path objects."""
return tuple(Path(p) for p in self.plugins)
def content_params(self) -> Dict[str, Any]:
"""Export content-specific parameters."""
return self.dict(include={"primary_asn", "org_name", "site_title", "site_description"})
def frontend(self) -> Dict[str, Any]:
"""Export UI-specific parameters."""

View file

@ -26,10 +26,6 @@ from hyperglass.exceptions.private import InputValidationError
from .main import MultiModel, HyperglassModel, HyperglassUniqueModel
from .fields import Action
if t.TYPE_CHECKING:
# Local
from .config.params import Params
IPv4PrefixLength = conint(ge=0, le=32)
IPv6PrefixLength = conint(ge=0, le=128)
IPNetwork = t.Union[IPv4Network, IPv6Network]
@ -277,7 +273,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
return [str(f) for f in matching_plugins]
return []
def frontend(self: "Directive", params: "Params") -> t.Dict[str, t.Any]:
def frontend(self: "Directive") -> t.Dict[str, t.Any]:
"""Prepare a representation of the directive for the UI."""
value = {
@ -291,11 +287,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")):
if self.info is not None:
with self.info.open() as md:
value["info"] = {
"enable": True,
"params": params.content_params(),
"content": md.read(),
}
value["info"] = md.read()
if self.field.is_select:
value["options"] = [o.export_dict() for o in self.field.options if o is not None]

View file

@ -17,14 +17,6 @@ Alignment = Union[Literal["left"], Literal["center"], Literal["right"], None]
StructuredDataField = Tuple[str, str, Alignment]
class UIDirectiveInfo(HyperglassModel):
"""UI: Directive Info."""
enable: StrictBool
params: Dict[str, str]
content: StrictStr
class UIDirective(HyperglassModel):
"""UI: Directive."""
@ -33,7 +25,7 @@ class UIDirective(HyperglassModel):
field_type: StrictStr
groups: List[StrictStr]
description: StrictStr
info: Optional[UIDirectiveInfo] = None
info: Optional[str] = None
options: Optional[List[Dict[str, Any]]]

View file

@ -11,16 +11,15 @@ import {
} from '@chakra-ui/react';
import { DynamicIcon, Markdown } from '~/components';
import { useColorValue } from '~/context';
import { isQueryContent } from '~/types';
import type { THelpModal } from './types';
export const HelpModal = (props: THelpModal): JSX.Element => {
const { visible, item, name, ...rest } = props;
const { visible, item, name, title, ...rest } = props;
const { isOpen, onOpen, onClose } = useDisclosure();
const bg = useColorValue('whiteSolid.50', 'blackSolid.800');
const color = useColorValue('black', 'white');
if (!isQueryContent(item)) {
if (item === null) {
return <></>;
}
return (
@ -33,19 +32,19 @@ export const HelpModal = (props: THelpModal): JSX.Element => {
minW={3}
size="md"
variant="link"
icon={<DynamicIcon icon={{ fi: 'FiInfo' }} />}
onClick={onOpen}
colorScheme="blue"
aria-label={`${name}_help`}
icon={<DynamicIcon icon={{ fi: 'FiInfo' }} />}
/>
</ScaleFade>
<Modal isOpen={isOpen} onClose={onClose} size="xl" motionPreset="slideInRight">
<ModalOverlay />
<ModalContent bg={bg} color={color} py={4} borderRadius="md" {...rest}>
<ModalHeader>{item.params.title}</ModalHeader>
<ModalHeader>{title}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Markdown content={item.content} />
<Markdown content={item} />
</ModalBody>
</ModalContent>
</Modal>

View file

@ -1,8 +1,8 @@
import type { ModalContentProps } from '@chakra-ui/react';
import type { QueryContent } from '~/types';
export interface THelpModal extends ModalContentProps {
item: QueryContent | null;
export interface THelpModal extends Omit<ModalContentProps, 'title'> {
title: string | null;
item: string | null;
name: string;
visible: boolean;
}

View file

@ -189,9 +189,10 @@ export const LookingGlass = (): JSX.Element => {
label={web.text.queryType}
labelAddOn={
<HelpModal
visible={directive?.info !== null}
item={directive?.info ?? null}
name="queryType"
title={directive?.name ?? null}
item={directive?.info ?? null}
visible={directive?.info !== null}
/>
}
>

View file

@ -112,7 +112,7 @@ type _DirectiveBase = {
field_type: 'text' | 'select' | null;
description: string;
groups: string[];
info: _QueryContent | null;
info: string | null;
};
type _DirectiveOption = {
@ -136,18 +136,6 @@ interface _Device {
description: string | null;
}
interface _QueryContent {
content: string;
enable: boolean;
params: {
primary_asn: _Config['primary_asn'];
org_name: _Config['org_name'];
site_title: _Config['site_title'];
title: string;
[k: string]: string;
};
}
interface _Content {
credit: string;
greeting: string;
@ -196,7 +184,6 @@ export interface Favicon {
export type Config = CamelCasedPropertiesDeep<_ConfigDeep> & CamelCasedProperties<_ConfigShallow>;
export type ThemeConfig = CamelCasedProperties<_ThemeConfig>;
export type Content = CamelCasedProperties<_Content>;
export type QueryContent = CamelCasedPropertiesDeep<_QueryContent>;
export type Device = CamelCasedPropertiesDeep<_Device>;
export type DeviceGroup = CamelCasedPropertiesDeep<_DeviceGroup>;
export type Directive = CamelCasedPropertiesDeep<_Directive>;

View file

@ -1,5 +1,5 @@
import type { FormData, TStringTableData, TQueryResponseString } from './data';
import type { QueryContent, DirectiveSelect, Directive } from './config';
import type { DirectiveSelect, Directive } from './config';
export function isString(a: unknown): a is string {
return typeof a === 'string';
@ -25,10 +25,6 @@ export function isStringOutput(data: unknown): data is TQueryResponseString {
);
}
export function isQueryContent(content: unknown): content is QueryContent {
return isObject(content) && 'content' in content;
}
/**
* Determine if a form field name is a valid form key name.
*/