forked from mirrors/thatmattlove-hyperglass
Deprecate Device.network
This commit is contained in:
parent
509e8ac3ef
commit
89568dc8e5
16 changed files with 161 additions and 149 deletions
|
|
@ -204,7 +204,7 @@ def init_ui_params(*, params: "Params", devices: "Devices") -> "UIParameters":
|
||||||
return UIParameters(
|
return UIParameters(
|
||||||
**_ui_params,
|
**_ui_params,
|
||||||
version=__version__,
|
version=__version__,
|
||||||
networks=devices.networks(params),
|
devices=devices.frontend(),
|
||||||
parsed_data_fields=PARSED_RESPONSE_FIELDS,
|
parsed_data_fields=PARSED_RESPONSE_FIELDS,
|
||||||
content={"credit": content_credit, "greeting": content_greeting},
|
content={"credit": content_credit, "greeting": content_greeting},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ class Query(BaseModel):
|
||||||
def validate_query_type(cls, value):
|
def validate_query_type(cls, value):
|
||||||
"""Ensure a requested query type exists."""
|
"""Ensure a requested query type exists."""
|
||||||
devices = use_state("devices")
|
devices = use_state("devices")
|
||||||
if any((device.has_directives(value) for device in devices.objects)):
|
if any((device.has_directives(value) for device in devices)):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
raise QueryTypeNotFound(name=value)
|
raise QueryTypeNotFound(name=value)
|
||||||
|
|
@ -158,10 +158,8 @@ class Query(BaseModel):
|
||||||
"""Ensure query_location is defined."""
|
"""Ensure query_location is defined."""
|
||||||
|
|
||||||
devices = use_state("devices")
|
devices = use_state("devices")
|
||||||
valid_id = value in devices.ids
|
|
||||||
valid_hostname = value in devices.hostnames
|
|
||||||
|
|
||||||
if not any((valid_id, valid_hostname)):
|
if not devices.valid_id_or_name(value):
|
||||||
raise QueryLocationNotFound(location=value)
|
raise QueryLocationNotFound(location=value)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
|
||||||
|
|
@ -163,26 +163,12 @@ class Vrf(BaseModel):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Network(BaseModel):
|
|
||||||
"""Response model for /api/devices networks."""
|
|
||||||
|
|
||||||
name: StrictStr
|
|
||||||
display_name: StrictStr
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
|
|
||||||
title = "Network"
|
|
||||||
description = "Network/ASN attributes"
|
|
||||||
schema_extra = {"examples": [{"name": "primary", "display_name": "AS65000"}]}
|
|
||||||
|
|
||||||
|
|
||||||
class RoutersResponse(BaseModel):
|
class RoutersResponse(BaseModel):
|
||||||
"""Response model for /api/devices list items."""
|
"""Response model for /api/devices list items."""
|
||||||
|
|
||||||
id: StrictStr
|
id: StrictStr
|
||||||
name: StrictStr
|
name: StrictStr
|
||||||
network: StrictStr
|
group: StrictStr
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Pydantic model configuration."""
|
"""Pydantic model configuration."""
|
||||||
|
|
@ -191,7 +177,7 @@ class RoutersResponse(BaseModel):
|
||||||
description = "Device attributes"
|
description = "Device attributes"
|
||||||
schema_extra = {
|
schema_extra = {
|
||||||
"examples": [
|
"examples": [
|
||||||
{"id": "nyc_router_1", "name": "NYC Router 1", "network": "New York City, NY"}
|
{"id": "nyc_router_1", "name": "NYC Router 1", "group": "New York City, NY"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ from .ssl import Ssl
|
||||||
from ..main import MultiModel, HyperglassModel, HyperglassModelWithId
|
from ..main import MultiModel, HyperglassModel, HyperglassModelWithId
|
||||||
from ..util import check_legacy_fields
|
from ..util import check_legacy_fields
|
||||||
from .proxy import Proxy
|
from .proxy import Proxy
|
||||||
from .params import Params
|
|
||||||
from ..fields import SupportedDriver
|
from ..fields import SupportedDriver
|
||||||
from .network import Network
|
|
||||||
from ..directive import Directives
|
from ..directive import Directives
|
||||||
from .credential import Credential
|
from .credential import Credential
|
||||||
|
|
||||||
|
|
@ -46,7 +44,7 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
id: StrictStr
|
id: StrictStr
|
||||||
name: StrictStr
|
name: StrictStr
|
||||||
address: Union[IPv4Address, IPv6Address, StrictStr]
|
address: Union[IPv4Address, IPv6Address, StrictStr]
|
||||||
network: Network
|
group: Optional[StrictStr]
|
||||||
credential: Credential
|
credential: Credential
|
||||||
proxy: Optional[Proxy]
|
proxy: Optional[Proxy]
|
||||||
display_name: Optional[StrictStr]
|
display_name: Optional[StrictStr]
|
||||||
|
|
@ -60,7 +58,7 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
|
|
||||||
def __init__(self, **kw) -> None:
|
def __init__(self, **kw) -> None:
|
||||||
"""Check legacy fields and ensure an `id` is set."""
|
"""Check legacy fields and ensure an `id` is set."""
|
||||||
kw = check_legacy_fields("Device", **kw)
|
kw = check_legacy_fields(model="Device", data=kw)
|
||||||
if "id" not in kw:
|
if "id" not in kw:
|
||||||
kw = self._with_id(kw)
|
kw = self._with_id(kw)
|
||||||
super().__init__(**kw)
|
super().__init__(**kw)
|
||||||
|
|
@ -100,7 +98,7 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"network": self.network.display_name,
|
"group": self.group,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -269,6 +267,13 @@ class Devices(MultiModel, model=Device, unique_by="id"):
|
||||||
"""Export API-facing device fields."""
|
"""Export API-facing device fields."""
|
||||||
return [d.export_api() for d in self]
|
return [d.export_api() for d in self]
|
||||||
|
|
||||||
|
def valid_id_or_name(self, 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) -> Dict[Path, Tuple[StrictStr]]:
|
||||||
"""Get a mapping of plugin paths to associated directive IDs."""
|
"""Get a mapping of plugin paths to associated directive IDs."""
|
||||||
result: Dict[Path, Set[StrictStr]] = {}
|
result: Dict[Path, Set[StrictStr]] = {}
|
||||||
|
|
@ -286,22 +291,23 @@ class Devices(MultiModel, model=Device, unique_by="id"):
|
||||||
# Convert the directive set to a tuple.
|
# Convert the directive set to a tuple.
|
||||||
return {k: tuple(v) for k, v in result.items()}
|
return {k: tuple(v) for k, v in result.items()}
|
||||||
|
|
||||||
def networks(self, params: Params) -> List[Dict[str, Any]]:
|
def frontend(self) -> List[Dict[str, Any]]:
|
||||||
"""Group devices by network."""
|
"""Export grouped devices for UIParameters."""
|
||||||
names = {device.network.display_name for device in self}
|
params = use_state("params")
|
||||||
|
groups = {device.group for device in self}
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"display_name": name,
|
"group": group,
|
||||||
"locations": [
|
"locations": [
|
||||||
{
|
{
|
||||||
"id": device.id,
|
"id": device.id,
|
||||||
"name": device.name,
|
"name": device.name,
|
||||||
"network": device.network.display_name,
|
"group": group,
|
||||||
"directives": [d.frontend(params) for d in device.directives],
|
"directives": [d.frontend(params) for d in device.directives],
|
||||||
}
|
}
|
||||||
for device in self
|
for device in self
|
||||||
if device.network.display_name == name
|
if device.group == group
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
for name in names
|
for group in groups
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
"""Validate network configuration variables."""
|
|
||||||
|
|
||||||
# Third Party
|
|
||||||
from pydantic import Field, StrictStr
|
|
||||||
|
|
||||||
# Local
|
|
||||||
from ..main import HyperglassModel
|
|
||||||
|
|
||||||
|
|
||||||
class Network(HyperglassModel):
|
|
||||||
"""Validation Model for per-network/asn config in devices.yaml."""
|
|
||||||
|
|
||||||
name: StrictStr = Field(
|
|
||||||
..., title="Network Name", description="Internal name of the device's primary network.",
|
|
||||||
)
|
|
||||||
display_name: StrictStr = Field(
|
|
||||||
...,
|
|
||||||
title="Network Display Name",
|
|
||||||
description="Display name of the device's primary network.",
|
|
||||||
)
|
|
||||||
|
|
@ -28,7 +28,7 @@ class Proxy(HyperglassModel):
|
||||||
|
|
||||||
def __init__(self: "Proxy", **kwargs: Any) -> None:
|
def __init__(self: "Proxy", **kwargs: Any) -> None:
|
||||||
"""Check for legacy fields."""
|
"""Check for legacy fields."""
|
||||||
kwargs = check_legacy_fields("Proxy", **kwargs)
|
kwargs = check_legacy_fields(model="Proxy", data=kwargs)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,24 @@ import pytest
|
||||||
from ..util import check_legacy_fields
|
from ..util import check_legacy_fields
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.dependency()
|
||||||
def test_check_legacy_fields():
|
def test_check_legacy_fields():
|
||||||
test1 = {"name": "Device A", "nos": "juniper"}
|
test1 = {"name": "Device A", "nos": "juniper"}
|
||||||
test1_expected = {"name": "Device A", "platform": "juniper"}
|
test1_expected = {"name": "Device A", "platform": "juniper"}
|
||||||
test2 = {"name": "Device B", "platform": "juniper"}
|
test2 = {"name": "Device B", "platform": "juniper"}
|
||||||
test3 = {"name": "Device C"}
|
test3 = {"name": "Device C"}
|
||||||
assert set(check_legacy_fields("Device", **test1).keys()) == set(
|
test4 = {"name": "Device D", "network": "this is wrong"}
|
||||||
|
|
||||||
|
assert set(check_legacy_fields(model="Device", data=test1).keys()) == set(
|
||||||
test1_expected.keys()
|
test1_expected.keys()
|
||||||
), "legacy field not replaced"
|
), "legacy field not replaced"
|
||||||
assert set(check_legacy_fields("Device", **test2).keys()) == set(
|
|
||||||
|
assert set(check_legacy_fields(model="Device", data=test2).keys()) == set(
|
||||||
test2.keys()
|
test2.keys()
|
||||||
), "new field not left unmodified"
|
), "new field not left unmodified"
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
check_legacy_fields("Device", **test3)
|
check_legacy_fields(model="Device", data=test3)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
check_legacy_fields(model="Device", data=test4)
|
||||||
|
|
|
||||||
|
|
@ -42,14 +42,14 @@ class UILocation(HyperglassModel):
|
||||||
|
|
||||||
id: StrictStr
|
id: StrictStr
|
||||||
name: StrictStr
|
name: StrictStr
|
||||||
network: StrictStr
|
group: StrictStr
|
||||||
directives: List[UIDirective] = []
|
directives: List[UIDirective] = []
|
||||||
|
|
||||||
|
|
||||||
class UINetwork(HyperglassModel):
|
class UIDevices(HyperglassModel):
|
||||||
"""UI: Network."""
|
"""UI: Devices."""
|
||||||
|
|
||||||
display_name: StrictStr
|
group: StrictStr
|
||||||
locations: List[UILocation] = []
|
locations: List[UILocation] = []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -67,6 +67,6 @@ class UIParameters(ParamsPublic, HyperglassModel):
|
||||||
web: WebPublic
|
web: WebPublic
|
||||||
messages: Messages
|
messages: Messages
|
||||||
version: StrictStr
|
version: StrictStr
|
||||||
networks: List[UINetwork] = []
|
devices: List[UIDevices] = []
|
||||||
parsed_data_fields: Tuple[StructuredDataField, ...]
|
parsed_data_fields: Tuple[StructuredDataField, ...]
|
||||||
content: UIContent
|
content: UIContent
|
||||||
|
|
|
||||||
|
|
@ -3,28 +3,70 @@
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Any, Dict, Tuple
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
# Third Party
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
||||||
LEGACY_FIELDS: Dict[str, Tuple[Tuple[str, str], ...]] = {
|
|
||||||
"Device": (("nos", "platform"),),
|
class LegacyField(BaseModel):
|
||||||
"Proxy": (("nos", "platform"),),
|
"""Define legacy fields on a per-model basis.
|
||||||
|
|
||||||
|
When `overwrite` is `True`, the old key is replaced with the new
|
||||||
|
key. This will generally only occur when the value type is the same,
|
||||||
|
and the key name has only changed names for clarity or cosmetic
|
||||||
|
purposes.
|
||||||
|
|
||||||
|
When `overwrite` is `False` and the old key is found, an error is
|
||||||
|
raised. This generally occurs when the overall function of the old
|
||||||
|
and new keys has remained the same, but the value type has changed,
|
||||||
|
requiring the user to make changes to the config file.
|
||||||
|
|
||||||
|
When `required` is `True` and neither the old or new keys are found,
|
||||||
|
an error is raised. When `required` is false and neither keys are
|
||||||
|
found, nothing happens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
old: str
|
||||||
|
new: str
|
||||||
|
overwrite: bool = False
|
||||||
|
required: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
LEGACY_FIELDS: Dict[str, Tuple[LegacyField, ...]] = {
|
||||||
|
"Device": (
|
||||||
|
LegacyField(old="nos", new="platform", overwrite=True),
|
||||||
|
LegacyField(old="network", new="group", overwrite=False, required=False),
|
||||||
|
),
|
||||||
|
"Proxy": (LegacyField(old="nos", new="platform", overwrite=True),),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def check_legacy_fields(model: str, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
def check_legacy_fields(*, model: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Check for legacy fields prior to model initialization."""
|
"""Check for legacy fields prior to model initialization."""
|
||||||
if model in LEGACY_FIELDS:
|
if model in LEGACY_FIELDS:
|
||||||
for legacy_key, new_key in LEGACY_FIELDS[model]:
|
for field in LEGACY_FIELDS[model]:
|
||||||
legacy_value = kwargs.pop(legacy_key, None)
|
legacy_value = data.pop(field.old, None)
|
||||||
new_value = kwargs.get(new_key)
|
new_value = data.get(field.new)
|
||||||
if legacy_value is not None and new_value is None:
|
if legacy_value is not None and new_value is None:
|
||||||
log.warning(
|
if field.overwrite:
|
||||||
"The {} field has been deprecated and will be removed in a future release. Use the '{}' field moving forward.",
|
log.warning(
|
||||||
f"{model}.{legacy_key}",
|
(
|
||||||
new_key,
|
"The {!r} field has been deprecated and will be removed in a future release. "
|
||||||
)
|
"Use the {!r} field moving forward."
|
||||||
kwargs[new_key] = legacy_value
|
),
|
||||||
elif legacy_value is None and new_value is None:
|
f"{model}.{field.old}",
|
||||||
raise ValueError(f"'{new_key}' is missing")
|
field.new,
|
||||||
return kwargs
|
)
|
||||||
|
data[field.new] = legacy_value
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
(
|
||||||
|
"The {!r} field has been replaced with the {!r} field. "
|
||||||
|
"Please consult the documentation and/or changelog to determine the appropriate migration path."
|
||||||
|
).format(f"{model}.{field.old}", field.new)
|
||||||
|
)
|
||||||
|
elif legacy_value is None and new_value is None and field.required:
|
||||||
|
raise ValueError(f"'{field.new}' is missing")
|
||||||
|
return data
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@ from hyperglass.models.data.bgp_route import BGPRouteTable
|
||||||
from .._builtin.bgp_route_juniper import BGPRoutePluginJuniper
|
from .._builtin.bgp_route_juniper import BGPRoutePluginJuniper
|
||||||
|
|
||||||
DEPENDS_KWARGS = {
|
DEPENDS_KWARGS = {
|
||||||
"depends": ["hyperglass/external/tests/test_rpki.py::test_rpki"],
|
"depends": [
|
||||||
|
"hyperglass/models/tests/test_util.py::test_check_legacy_fields",
|
||||||
|
"hyperglass/external/tests/test_rpki.py::test_rpki",
|
||||||
|
],
|
||||||
"scope": "session",
|
"scope": "session",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,7 +35,7 @@ def _tester(sample: str):
|
||||||
device = Device(
|
device = Device(
|
||||||
name="Test Device",
|
name="Test Device",
|
||||||
address="127.0.0.1",
|
address="127.0.0.1",
|
||||||
network={"name": "Test Network", "display_name": "Test Network"},
|
group="Test Network",
|
||||||
credential={"username": "", "password": ""},
|
credential={"username": "", "password": ""},
|
||||||
platform="juniper",
|
platform="juniper",
|
||||||
structured_output=True,
|
structured_output=True,
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,18 @@ import { Select } from '~/components';
|
||||||
import { useConfig, useColorValue } from '~/context';
|
import { useConfig, useColorValue } from '~/context';
|
||||||
import { useOpposingColor, useFormState } from '~/hooks';
|
import { useOpposingColor, useFormState } from '~/hooks';
|
||||||
|
|
||||||
import type { Network, SingleOption, OptionGroup, FormData } from '~/types';
|
import type { DeviceGroup, SingleOption, OptionGroup, FormData } from '~/types';
|
||||||
import type { TQuerySelectField, LocationCardProps } from './types';
|
import type { TQuerySelectField, LocationCardProps } from './types';
|
||||||
|
|
||||||
function buildOptions(networks: Network[]): OptionGroup[] {
|
function buildOptions(devices: DeviceGroup[]): OptionGroup[] {
|
||||||
return networks
|
return devices
|
||||||
.map(net => {
|
.map(group => {
|
||||||
const label = net.displayName;
|
const label = group.group;
|
||||||
const options = net.locations
|
const options = group.locations
|
||||||
.map(loc => ({
|
.map(loc => ({
|
||||||
label: loc.name,
|
label: loc.name,
|
||||||
value: loc.id,
|
value: loc.id,
|
||||||
group: net.displayName,
|
group: loc.group,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
|
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
|
||||||
return { label, options };
|
return { label, options };
|
||||||
|
|
@ -115,14 +115,14 @@ const LocationCard = (props: LocationCardProps): JSX.Element => {
|
||||||
export const QueryLocation = (props: TQuerySelectField): JSX.Element => {
|
export const QueryLocation = (props: TQuerySelectField): JSX.Element => {
|
||||||
const { onChange, label } = props;
|
const { onChange, label } = props;
|
||||||
|
|
||||||
const { networks } = useConfig();
|
const { devices } = useConfig();
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useFormContext<FormData>();
|
} = useFormContext<FormData>();
|
||||||
const selections = useFormState(s => s.selections);
|
const selections = useFormState(s => s.selections);
|
||||||
const setSelection = useFormState(s => s.setSelection);
|
const setSelection = useFormState(s => s.setSelection);
|
||||||
const { form, filtered } = useFormState(({ form, filtered }) => ({ form, filtered }));
|
const { form, filtered } = useFormState(({ form, filtered }) => ({ form, filtered }));
|
||||||
const options = useMemo(() => buildOptions(networks), [networks]);
|
const options = useMemo(() => buildOptions(devices), [devices]);
|
||||||
const element = useMemo(() => {
|
const element = useMemo(() => {
|
||||||
const groups = options.length;
|
const groups = options.length;
|
||||||
const maxOptionsPerGroup = Math.max(...options.map(opt => opt.options.length));
|
const maxOptionsPerGroup = Math.max(...options.map(opt => opt.options.length));
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
chakra,
|
chakra,
|
||||||
HStack,
|
HStack,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
useToast,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionPanel,
|
AccordionPanel,
|
||||||
AccordionButton,
|
AccordionButton,
|
||||||
|
|
@ -44,6 +45,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { index, queryLocation } = props;
|
const { index, queryLocation } = props;
|
||||||
|
const toast = useToast();
|
||||||
const { web, cache, messages } = useConfig();
|
const { web, cache, messages } = useConfig();
|
||||||
const { index: indices, setIndex } = useAccordionContext();
|
const { index: indices, setIndex } = useAccordionContext();
|
||||||
const getDevice = useDevice();
|
const getDevice = useDevice();
|
||||||
|
|
@ -66,7 +68,9 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
addResponse(device.id, data);
|
if (device !== null) {
|
||||||
|
addResponse(device.id, data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
@ -150,6 +154,21 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [data, index, indices, isLoading, isError, setIndex]);
|
}, [data, index, indices, isLoading, isError, setIndex]);
|
||||||
|
|
||||||
|
if (device === null) {
|
||||||
|
const id = `toast-queryLocation-${index}-${queryLocation}`;
|
||||||
|
if (!toast.isActive(id)) {
|
||||||
|
toast({
|
||||||
|
id,
|
||||||
|
title: messages.general,
|
||||||
|
description: `Configuration for device with ID '${queryLocation}' not found.`,
|
||||||
|
status: 'error',
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedAccordionItem
|
<AnimatedAccordionItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,12 @@ export interface UseGreeting {
|
||||||
close(): void;
|
close(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TUseDevice = (
|
export type UseDevice = (
|
||||||
/**
|
/**
|
||||||
* Device's ID, e.g. the device.name field.
|
* Device's ID, e.g. the device.name field.
|
||||||
*/
|
*/
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
) => Device;
|
) => Device | null;
|
||||||
|
|
||||||
export type UseStrfArgs = { [k: string]: unknown } | string;
|
export type UseStrfArgs = { [k: string]: unknown } | string;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,19 @@ import { useCallback, useMemo } from 'react';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
|
|
||||||
import type { Device } from '~/types';
|
import type { Device } from '~/types';
|
||||||
import type { TUseDevice } from './types';
|
import type { UseDevice } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a device's configuration from the global configuration context based on its name.
|
* Get a device's configuration from the global configuration context based on its name.
|
||||||
*/
|
*/
|
||||||
export function useDevice(): TUseDevice {
|
export function useDevice(): UseDevice {
|
||||||
const { networks } = useConfig();
|
const { devices } = useConfig();
|
||||||
|
|
||||||
const devices = useMemo(() => networks.map(n => n.locations).flat(), [networks]);
|
const locations = useMemo(() => devices.map(group => group.locations).flat(), [devices]);
|
||||||
|
|
||||||
function getDevice(id: string): Device {
|
function getDevice(id: string): Device | null {
|
||||||
return devices.filter(dev => dev.id === id)[0];
|
return locations.find(device => device.id === id) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return useCallback(getDevice, [devices]);
|
return useCallback(getDevice, [locations]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { all, andJoin, dedupObjectArray, withDev } from '~/util';
|
||||||
import type { StateCreator } from 'zustand';
|
import type { StateCreator } from 'zustand';
|
||||||
import type { UseFormSetError, UseFormClearErrors } from 'react-hook-form';
|
import type { UseFormSetError, UseFormClearErrors } from 'react-hook-form';
|
||||||
import type { SingleOption, Directive, FormData, Text } from '~/types';
|
import type { SingleOption, Directive, FormData, Text } from '~/types';
|
||||||
import type { TUseDevice } from './types';
|
import type { UseDevice } from './types';
|
||||||
|
|
||||||
type FormStatus = 'form' | 'results';
|
type FormStatus = 'form' | 'results';
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ interface FormStateType {
|
||||||
extra: {
|
extra: {
|
||||||
setError: UseFormSetError<FormData>;
|
setError: UseFormSetError<FormData>;
|
||||||
clearErrors: UseFormClearErrors<FormData>;
|
clearErrors: UseFormClearErrors<FormData>;
|
||||||
getDevice: TUseDevice;
|
getDevice: UseDevice;
|
||||||
text: Text;
|
text: Text;
|
||||||
},
|
},
|
||||||
): void;
|
): void;
|
||||||
|
|
@ -129,7 +129,7 @@ const formState: StateCreator<FormStateType> = (set, get) => ({
|
||||||
extra: {
|
extra: {
|
||||||
setError: UseFormSetError<FormData>;
|
setError: UseFormSetError<FormData>;
|
||||||
clearErrors: UseFormClearErrors<FormData>;
|
clearErrors: UseFormClearErrors<FormData>;
|
||||||
getDevice: TUseDevice;
|
getDevice: UseDevice;
|
||||||
text: Text;
|
text: Text;
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
|
|
@ -144,15 +144,17 @@ const formState: StateCreator<FormStateType> = (set, get) => ({
|
||||||
|
|
||||||
for (const loc of locations) {
|
for (const loc of locations) {
|
||||||
const device = getDevice(loc);
|
const device = getDevice(loc);
|
||||||
locationNames.push(device.name);
|
if (device !== null) {
|
||||||
allDevices.push(device);
|
locationNames.push(device.name);
|
||||||
const groups = new Set<string>();
|
allDevices.push(device);
|
||||||
for (const directive of device.directives) {
|
const groups = new Set<string>();
|
||||||
for (const group of directive.groups) {
|
for (const directive of device.directives) {
|
||||||
groups.add(group);
|
for (const group of directive.groups) {
|
||||||
|
groups.add(group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
allGroups.push(Array.from(groups));
|
||||||
}
|
}
|
||||||
allGroups.push(Array.from(groups));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const intersecting = intersectionWith(...allGroups, isEqual);
|
const intersecting = intersectionWith(...allGroups, isEqual);
|
||||||
|
|
|
||||||
|
|
@ -97,36 +97,6 @@ interface _Web {
|
||||||
theme: _ThemeConfig;
|
theme: _ThemeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// export interface Query {
|
|
||||||
// name: string;
|
|
||||||
// enable: boolean;
|
|
||||||
// display_name: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface BGPCommunity {
|
|
||||||
// community: string;
|
|
||||||
// display_name: string;
|
|
||||||
// description: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface QueryBGPRoute extends Query {}
|
|
||||||
// export interface QueryBGPASPath extends Query {}
|
|
||||||
// export interface QueryPing extends Query {}
|
|
||||||
// export interface QueryTraceroute extends Query {}
|
|
||||||
// export interface QueryBGPCommunity extends Query {
|
|
||||||
// mode: 'input' | 'select';
|
|
||||||
// communities: BGPCommunity[];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface Queries {
|
|
||||||
// bgp_route: QueryBGPRoute;
|
|
||||||
// bgp_community: QueryBGPCommunity;
|
|
||||||
// bgp_aspath: QueryBGPASPath;
|
|
||||||
// ping: QueryPing;
|
|
||||||
// traceroute: QueryTraceroute;
|
|
||||||
// list: Query[];
|
|
||||||
// }
|
|
||||||
|
|
||||||
type _DirectiveBase = {
|
type _DirectiveBase = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -151,15 +121,10 @@ type _Directive = _DirectiveBase | _DirectiveSelect;
|
||||||
interface _Device {
|
interface _Device {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
network: string;
|
group: string;
|
||||||
directives: _Directive[];
|
directives: _Directive[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface _Network {
|
|
||||||
display_name: string;
|
|
||||||
locations: _Device[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface _QueryContent {
|
interface _QueryContent {
|
||||||
content: string;
|
content: string;
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
|
@ -184,13 +149,16 @@ interface _Cache {
|
||||||
|
|
||||||
type _Config = _ConfigDeep & _ConfigShallow;
|
type _Config = _ConfigDeep & _ConfigShallow;
|
||||||
|
|
||||||
|
interface _DeviceGroup {
|
||||||
|
group: string;
|
||||||
|
locations: _Device[];
|
||||||
|
}
|
||||||
|
|
||||||
interface _ConfigDeep {
|
interface _ConfigDeep {
|
||||||
cache: _Cache;
|
cache: _Cache;
|
||||||
web: _Web;
|
web: _Web;
|
||||||
messages: _Messages;
|
messages: _Messages;
|
||||||
// queries: Queries;
|
devices: _DeviceGroup[];
|
||||||
devices: _Device[];
|
|
||||||
networks: _Network[];
|
|
||||||
content: _Content;
|
content: _Content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,8 +193,8 @@ export type Config = CamelCasedPropertiesDeep<_ConfigDeep> & CamelCasedPropertie
|
||||||
export type ThemeConfig = CamelCasedProperties<_ThemeConfig>;
|
export type ThemeConfig = CamelCasedProperties<_ThemeConfig>;
|
||||||
export type Content = CamelCasedProperties<_Content>;
|
export type Content = CamelCasedProperties<_Content>;
|
||||||
export type QueryContent = CamelCasedPropertiesDeep<_QueryContent>;
|
export type QueryContent = CamelCasedPropertiesDeep<_QueryContent>;
|
||||||
export type Network = CamelCasedPropertiesDeep<_Network>;
|
|
||||||
export type Device = CamelCasedPropertiesDeep<_Device>;
|
export type Device = CamelCasedPropertiesDeep<_Device>;
|
||||||
|
export type DeviceGroup = CamelCasedPropertiesDeep<_DeviceGroup>;
|
||||||
export type Directive = CamelCasedPropertiesDeep<_Directive>;
|
export type Directive = CamelCasedPropertiesDeep<_Directive>;
|
||||||
export type DirectiveSelect = CamelCasedPropertiesDeep<_DirectiveSelect>;
|
export type DirectiveSelect = CamelCasedPropertiesDeep<_DirectiveSelect>;
|
||||||
export type DirectiveOption = CamelCasedPropertiesDeep<_DirectiveOption>;
|
export type DirectiveOption = CamelCasedPropertiesDeep<_DirectiveOption>;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue