diff --git a/.flake8 b/.flake8
index 0eb3eb8..3f63603 100644
--- a/.flake8
+++ b/.flake8
@@ -21,7 +21,7 @@ per-file-ignores=
hyperglass/**/test_*.py:S101,D103
hyperglass/api/*.py:B008
hyperglass/state/hooks.py:F811
-ignore=W503,C0330,R504,D202,S403,S301,S404,E731,D402
+ignore=W503,C0330,R504,D202,S403,S301,S404,E731,D402,IF100
select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W
disable-noqa=False
hang-closing=False
diff --git a/hyperglass/log.py b/hyperglass/log.py
index c35203a..36b650b 100644
--- a/hyperglass/log.py
+++ b/hyperglass/log.py
@@ -75,7 +75,6 @@ def setup_lib_logging(log_level: str) -> None:
"""
intercept_handler = LibIntercentHandler()
- logging.root.setLevel(log_level)
seen = set()
for name in [
diff --git a/hyperglass/models/config/devices.py b/hyperglass/models/config/devices.py
index 122ae8d..708add1 100644
--- a/hyperglass/models/config/devices.py
+++ b/hyperglass/models/config/devices.py
@@ -7,7 +7,7 @@ from pathlib import Path
from ipaddress import IPv4Address, IPv6Address
# Third Party
-from pydantic import StrictInt, StrictStr, StrictBool, validator
+from pydantic import FilePath, StrictInt, StrictStr, StrictBool, validator
# Project
from hyperglass.log import log
@@ -43,6 +43,8 @@ class Device(HyperglassModelWithId, extra="allow"):
id: StrictStr
name: StrictStr
+ description: Optional[StrictStr]
+ avatar: Optional[FilePath]
address: Union[IPv4Address, IPv6Address, StrictStr]
group: Optional[StrictStr]
credential: Credential
@@ -157,6 +159,28 @@ class Device(HyperglassModelWithId, extra="allow"):
)
return value
+ @validator("avatar")
+ def validate_avatar(
+ cls, value: Union[FilePath, None], values: Dict[str, Any]
+ ) -> Union[FilePath, None]:
+ """Migrate avatar to static directory."""
+ if value is not None:
+ # Standard Library
+ import shutil
+
+ # Third Party
+ from PIL import Image
+
+ target = Settings.static_path / "images" / value.name
+ copied = shutil.copy2(value, target)
+ log.debug("Copied {} avatar from {!r} to {!r}", values["name"], str(value), str(target))
+
+ with Image.open(copied) as src:
+ if src.width > 512:
+ src.thumbnail((512, 512 * src.height / src.width))
+ src.save(target)
+ return value
+
@validator("structured_output", pre=True, always=True)
def validate_structured_output(cls, value: bool, values: Dict) -> bool:
"""Validate structured output is supported on the device & set a default."""
@@ -300,9 +324,13 @@ class Devices(MultiModel, model=Device, unique_by="id"):
"group": group,
"locations": [
{
+ "group": group,
"id": device.id,
"name": device.name,
- "group": group,
+ "avatar": f"/images/{device.avatar.name}"
+ if device.avatar is not None
+ else None,
+ "description": device.description,
"directives": [d.frontend(params) for d in device.directives],
}
for device in self
diff --git a/hyperglass/models/ui.py b/hyperglass/models/ui.py
index d86e936..8fc6fed 100644
--- a/hyperglass/models/ui.py
+++ b/hyperglass/models/ui.py
@@ -43,6 +43,8 @@ class UILocation(HyperglassModel):
id: StrictStr
name: StrictStr
group: StrictStr
+ avatar: Optional[StrictStr]
+ description: Optional[StrictStr]
directives: List[UIDirective] = []
diff --git a/hyperglass/ui/components/form/queryLocation.tsx b/hyperglass/ui/components/form/queryLocation.tsx
index 4050df3..b43046d 100644
--- a/hyperglass/ui/components/form/queryLocation.tsx
+++ b/hyperglass/ui/components/form/queryLocation.tsx
@@ -14,11 +14,18 @@ function buildOptions(devices: DeviceGroup[]): OptionGroup[] {
.map(group => {
const label = group.group;
const options = group.locations
- .map(loc => ({
- label: loc.name,
- value: loc.id,
- group: loc.group,
- }))
+ .map(
+ loc =>
+ ({
+ label: loc.name,
+ value: loc.id,
+ group: loc.group,
+ data: {
+ avatar: loc.avatar,
+ description: loc.description,
+ },
+ } as SingleOption),
+ )
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
return { label, options };
})
@@ -67,6 +74,7 @@ const LocationCard = (props: LocationCardProps): JSX.Element => {
px={6}
bg={bg}
w="100%"
+ minW="xs"
maxW="sm"
mx="auto"
shadow="lg"
@@ -102,12 +110,15 @@ const LocationCard = (props: LocationCardProps): JSX.Element => {
bg="whiteAlpha.300"
borderStyle="solid"
borderColor={imageBorder}
+ src={(option.data?.avatar as string) ?? undefined}
/>
-
- To do: add details field (and location image field)
-
+ {option?.data?.description && (
+
+ {option.data.description as string}
+
+ )}
);
};
diff --git a/hyperglass/ui/types/common.ts b/hyperglass/ui/types/common.ts
index aea14ab..098e56f 100644
--- a/hyperglass/ui/types/common.ts
+++ b/hyperglass/ui/types/common.ts
@@ -6,6 +6,7 @@ export type SingleOption = AnyOption & {
value: string;
group?: string;
tags?: string[];
+ data?: Record;
};
export type OptionGroup = AnyOption & {
diff --git a/hyperglass/ui/types/config.ts b/hyperglass/ui/types/config.ts
index adae2f8..fe3f467 100644
--- a/hyperglass/ui/types/config.ts
+++ b/hyperglass/ui/types/config.ts
@@ -122,7 +122,9 @@ interface _Device {
id: string;
name: string;
group: string;
+ avatar: string | null;
directives: _Directive[];
+ description: string | null;
}
interface _QueryContent {