forked from mirrors/thatmattlove-hyperglass
Implement device description and avatar
This commit is contained in:
parent
89568dc8e5
commit
22ae6a97e8
7 changed files with 55 additions and 12 deletions
2
.flake8
2
.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
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ class UILocation(HyperglassModel):
|
|||
id: StrictStr
|
||||
name: StrictStr
|
||||
group: StrictStr
|
||||
avatar: Optional[StrictStr]
|
||||
description: Optional[StrictStr]
|
||||
directives: List[UIDirective] = []
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<chakra.p mt={2} color={fg} opacity={0.6} fontSize="sm">
|
||||
To do: add details field (and location image field)
|
||||
</chakra.p>
|
||||
{option?.data?.description && (
|
||||
<chakra.p mt={2} color={fg} opacity={0.6} fontSize="sm">
|
||||
{option.data.description as string}
|
||||
</chakra.p>
|
||||
)}
|
||||
</MotionBox>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export type SingleOption = AnyOption & {
|
|||
value: string;
|
||||
group?: string;
|
||||
tags?: string[];
|
||||
data?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type OptionGroup = AnyOption & {
|
||||
|
|
|
|||
|
|
@ -122,7 +122,9 @@ interface _Device {
|
|||
id: string;
|
||||
name: string;
|
||||
group: string;
|
||||
avatar: string | null;
|
||||
directives: _Directive[];
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
interface _QueryContent {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue