From 22ae6a97e81aea035cd29e3ae9299740307f3c73 Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Fri, 24 Sep 2021 01:04:28 -0700 Subject: [PATCH] Implement device description and avatar --- .flake8 | 2 +- hyperglass/log.py | 1 - hyperglass/models/config/devices.py | 32 +++++++++++++++++-- hyperglass/models/ui.py | 2 ++ .../ui/components/form/queryLocation.tsx | 27 +++++++++++----- hyperglass/ui/types/common.ts | 1 + hyperglass/ui/types/config.ts | 2 ++ 7 files changed, 55 insertions(+), 12 deletions(-) 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 {