tooling overhaul

This commit is contained in:
thatmattlove 2024-02-27 17:44:19 -05:00
parent ae2753b695
commit cd6bf7a162
60 changed files with 3310 additions and 3539 deletions

1
.gitignore vendored
View file

@ -16,6 +16,7 @@ __pycache__/
# Pyenv # Pyenv
.python-version .python-version
.venv
# MyPy # MyPy
.mypy_cache .mypy_cache

View file

@ -3,5 +3,5 @@
"eslint.workingDirectories": ["./hyperglass/ui"], "eslint.workingDirectories": ["./hyperglass/ui"],
"python.linting.mypyEnabled": false, "python.linting.mypyEnabled": false,
"python.linting.enabled": false, "python.linting.enabled": false,
"prettier.configPath": "./hyperglass/ui/.prettierrc" "biome.lspBin": "./hyperglass/ui/node_modules/.bin/biome"
} }

View file

@ -16,7 +16,7 @@ def build_ui(timeout: int) -> None:
# Project # Project
from hyperglass.state import use_state from hyperglass.state import use_state
from hyperglass.configuration import init_user_config from hyperglass.configuration import init_user_config
from hyperglass.util.frontend import build_frontend from hyperglass.frontend import build_frontend
# Populate configuration to Redis prior to accessing it. # Populate configuration to Redis prior to accessing it.
init_user_config() init_user_config()

View file

@ -11,9 +11,9 @@ from pathlib import Path
# Project # Project
from hyperglass.log import log from hyperglass.log import log
from hyperglass.util import copyfiles, check_path, dotenv_to_dict
from hyperglass.state import use_state
# Local
from .files import copyfiles, check_path, dotenv_to_dict
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
# Project # Project
@ -56,7 +56,6 @@ async def read_package_json() -> t.Dict[str, t.Any]:
package_json_file = Path(__file__).parent.parent / "ui" / "package.json" package_json_file = Path(__file__).parent.parent / "ui" / "package.json"
try: try:
with package_json_file.open("r") as file: with package_json_file.open("r") as file:
package_json = json.load(file) package_json = json.load(file)
@ -82,7 +81,7 @@ async def node_initial(timeout: int = 180, dev_mode: bool = False) -> str:
try: try:
proc = await asyncio.create_subprocess_shell( proc = await asyncio.create_subprocess_shell(
cmd="yarn --silent --emoji false", cmd="pnpm install",
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
cwd=ui_path, cwd=ui_path,
@ -178,10 +177,8 @@ def generate_opengraph(
log.debug("Copied {} to {}", str(image_path), str(target_path)) log.debug("Copied {} to {}", str(image_path), str(target_path))
with Image.open(copied) as src: with Image.open(copied) as src:
# Only resize the image if it needs to be resized # Only resize the image if it needs to be resized
if src.size[0] != max_width or src.size[1] != max_height: if src.size[0] != max_width or src.size[1] != max_height:
# Resize image while maintaining aspect ratio # Resize image while maintaining aspect ratio
log.debug("Opengraph image is not 1200x630, resizing...") log.debug("Opengraph image is not 1200x630, resizing...")
src.thumbnail((max_width, max_height)) src.thumbnail((max_width, max_height))
@ -290,6 +287,10 @@ async def build_frontend( # noqa: C901
dot_env_file = Path(__file__).parent.parent / "ui" / ".env" dot_env_file = Path(__file__).parent.parent / "ui" / ".env"
env_config = {} env_config = {}
ui_config_file = Path(__file__).parent.parent / "ui" / "hyperglass.json"
ui_config_file.write_text(params.export_json(by_alias=True))
package_json = await read_package_json() package_json = await read_package_json()
# Set NextJS production/development mode and base URL based on # Set NextJS production/development mode and base URL based on

View file

@ -52,6 +52,12 @@ HyperglassConsole = Console(
"warning": "bold yellow", "warning": "bold yellow",
"error": "bold red", "error": "bold red",
"success": "bold green", "success": "bold green",
"critical": "bold bright_red",
"logging.level.info": "bold cyan",
"logging.level.warning": "bold yellow",
"logging.level.error": "bold red",
"logging.level.critical": "bold bright_red",
"logging.level.success": "bold green",
"subtle": "rgb(128,128,128)", "subtle": "rgb(128,128,128)",
} }
) )
@ -146,12 +152,13 @@ def init_logger(level: str = "INFO"):
if sys.stdout.isatty(): if sys.stdout.isatty():
# Use Rich for logging if hyperglass started from a TTY. # Use Rich for logging if hyperglass started from a TTY.
_loguru_logger.add( _loguru_logger.add(
sink=RichHandler( sink=RichHandler(
console=HyperglassConsole, console=HyperglassConsole,
rich_tracebacks=True, rich_tracebacks=True,
level=level, level=level,
tracebacks_show_locals=True, tracebacks_show_locals=level == "DEBUG",
log_time_format="[%Y%m%d %H:%M:%S]", log_time_format="[%Y%m%d %H:%M:%S]",
), ),
format=_FMT_BASIC, format=_FMT_BASIC,

View file

@ -12,9 +12,8 @@ from gunicorn.arbiter import Arbiter # type: ignore
from gunicorn.app.base import BaseApplication # type: ignore from gunicorn.app.base import BaseApplication # type: ignore
# Local # Local
from .log import CustomGunicornLogger, log, init_logger, setup_lib_logging from .log import log, init_logger, setup_lib_logging
from .util import get_node_version from .util import get_node_version
from .plugins import InputPluginManager, OutputPluginManager, register_plugin, init_builtin_plugins
from .constants import MIN_NODE_VERSION, MIN_PYTHON_VERSION, __version__ from .constants import MIN_NODE_VERSION, MIN_PYTHON_VERSION, __version__
# Ensure the Python version meets the minimum requirements. # Ensure the Python version meets the minimum requirements.
@ -34,12 +33,18 @@ if node_major < MIN_NODE_VERSION:
from .util import cpu_count from .util import cpu_count
from .state import use_state from .state import use_state
from .settings import Settings from .settings import Settings
from .configuration import init_user_config
from .util.frontend import build_frontend
log_level = "INFO" if Settings.debug is False else "DEBUG"
setup_lib_logging(log_level)
init_logger(log_level)
async def build_ui() -> bool: async def build_ui() -> bool:
"""Perform a UI build prior to starting the application.""" """Perform a UI build prior to starting the application."""
from .frontend import build_frontend
state = use_state() state = use_state()
await build_frontend( await build_frontend(
dev_mode=Settings.dev_mode, dev_mode=Settings.dev_mode,
@ -54,6 +59,8 @@ async def build_ui() -> bool:
def register_all_plugins() -> None: def register_all_plugins() -> None:
"""Validate and register configured plugins.""" """Validate and register configured plugins."""
from .plugins import register_plugin, init_builtin_plugins
state = use_state() state = use_state()
# Register built-in plugins. # Register built-in plugins.
@ -78,6 +85,8 @@ def register_all_plugins() -> None:
def unregister_all_plugins() -> None: def unregister_all_plugins() -> None:
"""Unregister all plugins.""" """Unregister all plugins."""
from .plugins import InputPluginManager, OutputPluginManager
for manager in (InputPluginManager, OutputPluginManager): for manager in (InputPluginManager, OutputPluginManager):
manager().reset() manager().reset()
@ -91,7 +100,12 @@ def on_starting(server: "Arbiter") -> None:
register_all_plugins() register_all_plugins()
asyncio.run(build_ui()) if not Settings.disable_ui:
asyncio.run(build_ui())
def when_ready(server: "Arbiter") -> None:
"""Gunicorn post-start hook."""
log.success( log.success(
"Started hyperglass {} on http://{} with {!s} workers", "Started hyperglass {} on http://{} with {!s} workers",
@ -141,6 +155,8 @@ class HyperglassWSGI(BaseApplication):
def start(*, log_level: str, workers: int, **kwargs) -> None: def start(*, log_level: str, workers: int, **kwargs) -> None:
"""Start hyperglass via gunicorn.""" """Start hyperglass via gunicorn."""
from .log import CustomGunicornLogger
HyperglassWSGI( HyperglassWSGI(
app="hyperglass.api:app", app="hyperglass.api:app",
options={ options={
@ -152,6 +168,7 @@ def start(*, log_level: str, workers: int, **kwargs) -> None:
"loglevel": log_level, "loglevel": log_level,
"bind": Settings.bind(), "bind": Settings.bind(),
"on_starting": on_starting, "on_starting": on_starting,
"when_ready": when_ready,
"command": shutil.which("gunicorn"), "command": shutil.which("gunicorn"),
"logger_class": CustomGunicornLogger, "logger_class": CustomGunicornLogger,
"worker_class": "uvicorn.workers.UvicornWorker", "worker_class": "uvicorn.workers.UvicornWorker",
@ -163,21 +180,15 @@ def start(*, log_level: str, workers: int, **kwargs) -> None:
def run(_workers: int = None): def run(_workers: int = None):
"""Run hyperglass.""" """Run hyperglass."""
try: from .configuration import init_user_config
init_user_config()
try:
log.debug("System settings: {!r}", Settings) log.debug("System settings: {!r}", Settings)
workers, log_level = 1, "DEBUG" init_user_config()
if Settings.debug is False: workers = 1 if Settings.debug else cpu_count(2)
workers, log_level = cpu_count(2), "WARNING"
if _workers is not None:
workers = _workers
init_logger(log_level)
setup_lib_logging(log_level)
start(log_level=log_level, workers=workers) start(log_level=log_level, workers=workers)
except Exception as error: except Exception as error:
# Handle app exceptions. # Handle app exceptions.

View file

@ -121,7 +121,6 @@ class Params(ParamsPublic, HyperglassModel):
return self.export_dict( return self.export_dict(
include={ include={
"cache": {"show_text", "timeout"}, "cache": {"show_text", "timeout"},
"debug": ...,
"developer_mode": ..., "developer_mode": ...,
"primary_asn": ..., "primary_asn": ...,
"request_timeout": ..., "request_timeout": ...,

View file

@ -142,7 +142,6 @@ class ConfigPathItem(Path):
value = Settings.default_app_path.joinpath( value = Settings.default_app_path.joinpath(
*(p for p in value.parts if p not in Settings.app_path.parts) *(p for p in value.parts if p not in Settings.app_path.parts)
) )
print(f"{value=}")
return value return value
def __repr__(self): def __repr__(self):

View file

@ -43,6 +43,7 @@ class HyperglassSettings(BaseSettings):
debug: bool = False debug: bool = False
dev_mode: bool = False dev_mode: bool = False
disable_ui: bool = False
app_path: DirectoryPath = _default_app_path app_path: DirectoryPath = _default_app_path
redis_host: str = "localhost" redis_host: str = "localhost"
redis_password: t.Optional[SecretStr] redis_password: t.Optional[SecretStr]
@ -58,7 +59,6 @@ class HyperglassSettings(BaseSettings):
super().__init__(**kwargs) super().__init__(**kwargs)
if self.container: if self.container:
self.app_path = self.default_app_path self.app_path = self.default_app_path
print(self)
def __rich_console__(self, console: "Console", options: "ConsoleOptions") -> "RenderResult": def __rich_console__(self, console: "Console", options: "ConsoleOptions") -> "RenderResult":
"""Render a Rich table representation of hyperglass settings.""" """Render a Rich table representation of hyperglass settings."""

View file

@ -1,5 +1,6 @@
.DS_Store .DS_Store
.env* .env*
hyperglass.json
custom.*[js, html] custom.*[js, html]
*.tsbuildinfo *.tsbuildinfo
# dev/test files # dev/test files

40
hyperglass/ui/biome.json Normal file
View file

@ -0,0 +1,40 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"organizeImports": {
"enabled": true
},
"files": {
"ignore": ["node_modules", "dist", ".next/", "favicon-formats.ts", "custom.*[js, html]"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noUselessTypeConstraint": "off",
"noBannedTypes": "off"
},
"style": {
"noInferrableTypes": "off",
"noNonNullAssertion": "off"
},
"correctness": {
"useExhaustiveDependencies": "off"
}
}
},
"formatter": {
"indentStyle": "space",
"lineWidth": 100,
"indentWidth": 2
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"bracketSpacing": true,
"semicolons": "always",
"arrowParentheses": "asNeeded",
"trailingComma": "all"
}
}
}

View file

@ -17,7 +17,7 @@ import {
useColorMode, useColorMode,
useColorValue, useColorValue,
useBreakpointValue, useBreakpointValue,
useHyperglassConfig, // useHyperglassConfig,
} from '~/hooks'; } from '~/hooks';
import type { UseDisclosureReturn } from '@chakra-ui/react'; import type { UseDisclosureReturn } from '@chakra-ui/react';
@ -56,7 +56,7 @@ export const Debugger = (): JSX.Element => {
useBreakpointValue({ base: 'SMALL', md: 'MEDIUM', lg: 'LARGE', xl: 'X-LARGE' }) ?? 'UNKNOWN'; useBreakpointValue({ base: 'SMALL', md: 'MEDIUM', lg: 'LARGE', xl: 'X-LARGE' }) ?? 'UNKNOWN';
const tagSize = useBreakpointValue({ base: 'sm', lg: 'lg' }) ?? 'lg'; const tagSize = useBreakpointValue({ base: 'sm', lg: 'lg' }) ?? 'lg';
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' }) ?? 'sm'; const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' }) ?? 'sm';
const { refetch } = useHyperglassConfig(); // const { refetch } = useHyperglassConfig();
return ( return (
<> <>
<HStack <HStack
@ -92,14 +92,14 @@ export const Debugger = (): JSX.Element => {
> >
View Theme View Theme
</Button> </Button>
<Button {/* <Button
size={btnSize} size={btnSize}
colorScheme="purple" colorScheme="purple"
leftIcon={<DynamicIcon icon={{ hi: 'HiOutlineDownload' }} />} leftIcon={<DynamicIcon icon={{ hi: 'HiOutlineDownload' }} />}
onClick={() => refetch()} onClick={() => refetch()}
> >
Reload Config Reload Config
</Button> </Button> */}
<Tag size={tagSize} colorScheme="teal"> <Tag size={tagSize} colorScheme="teal">
{mediaSize} {mediaSize}
</Tag> </Tag>

View file

@ -61,7 +61,8 @@ export const Footer = (): JSX.Element => {
icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />; icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />;
} }
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />; return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
} else if (isMenu(item)) { }
if (isMenu(item)) {
return ( return (
<FooterButton key={item.title} side="left" content={item.content} title={item.title} /> <FooterButton key={item.title} side="left" content={item.content} title={item.title} />
); );
@ -77,7 +78,8 @@ export const Footer = (): JSX.Element => {
icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />; icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />;
} }
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />; return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
} else if (isMenu(item)) { }
if (isMenu(item)) {
return ( return (
<FooterButton key={item.title} side="right" content={item.content} title={item.title} /> <FooterButton key={item.title} side="right" content={item.content} title={item.title} />
); );

View file

@ -55,10 +55,10 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
? // Highlight red when there are no overlapping query types for the locations selected. ? // Highlight red when there are no overlapping query types for the locations selected.
errorBorder errorBorder
: isChecked && !hasError : isChecked && !hasError
? // Highlight blue when any location is selected and there is no error. ? // Highlight blue when any location is selected and there is no error.
checkedBorder checkedBorder
: // Otherwise, no border. : // Otherwise, no border.
'transparent', 'transparent',
[hasError, isChecked, checkedBorder, errorBorder], [hasError, isChecked, checkedBorder, errorBorder],
); );

View file

@ -103,23 +103,24 @@ export const LookingGlassForm = (): JSX.Element => {
const isFqdn = isFqdnQuery(form.queryTarget, directive?.fieldType ?? null); const isFqdn = isFqdnQuery(form.queryTarget, directive?.fieldType ?? null);
if (greetingReady && !isFqdn) { if (greetingReady && !isFqdn) {
return setStatus('results'); setStatus('results');
return;
} }
if (greetingReady && isFqdn) { if (greetingReady && isFqdn) {
setLoading(true); setLoading(true);
return resolvedOpen(); resolvedOpen();
} else { return;
console.group('%cSomething went wrong', 'color:red;');
console.table({
'Greeting Required': web.greeting.required,
'Greeting Ready': greetingReady,
'Query Target': form.queryTarget,
'Query Type': form.queryType,
'Is FQDN': isFqdn,
});
console.groupEnd();
} }
console.group('%cSomething went wrong', 'color:red;');
console.table({
'Greeting Required': web.greeting.required,
'Greeting Ready': greetingReady,
'Query Target': form.queryTarget,
'Query Type': form.queryType,
'Is FQDN': isFqdn,
});
console.groupEnd();
} }
const handleLocChange = (locations: string[]) => const handleLocChange = (locations: string[]) =>

View file

@ -25,5 +25,5 @@ export const Cell = (props: CellProps): JSX.Element => {
rpki_state: <RPKIState state={data.value} active={data.row.values.active} />, rpki_state: <RPKIState state={data.value} active={data.row.values.active} />,
weight: <Weight weight={data.value} winningWeight={rawData.winning_weight} />, weight: <Weight weight={data.value} winningWeight={rawData.winning_weight} />,
}; };
return component[cellId] ?? <> </>; return component[cellId] ?? '';
}; };

View file

@ -119,6 +119,7 @@ export const ASPath = (props: ASPathProps): JSX.Element => {
paths.push( paths.push(
<DynamicIcon <DynamicIcon
icon={{ fa: 'FaChevronRight' }} icon={{ fa: 'FaChevronRight' }}
// biome-ignore lint/suspicious/noArrayIndexKey: index makes sense in this case.
key={`separator-${i}`} key={`separator-${i}`}
color={color[+active]} color={color[+active]}
boxSize={5} boxSize={5}
@ -127,6 +128,7 @@ export const ASPath = (props: ASPathProps): JSX.Element => {
/>, />,
); );
paths.push( paths.push(
// biome-ignore lint/suspicious/noArrayIndexKey: index makes sense in this case.
<Text fontSize="sm" as="span" whiteSpace="pre" fontFamily="mono" key={`as-${asnStr}-${i}`}> <Text fontSize="sm" as="span" whiteSpace="pre" fontFamily="mono" key={`as-${asnStr}-${i}`}>
{asnStr} {asnStr}
</Text>, </Text>,

View file

@ -32,12 +32,14 @@ function buildOptions(devices: DeviceGroup[]): OptionGroup<LocationOption>[] {
avatar: loc.avatar, avatar: loc.avatar,
description: loc.description, description: loc.description,
}, },
} as SingleOption), }) as SingleOption,
) )
.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: label ?? '', options };
}) })
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); .sort((a, b) =>
(a.label ?? 0) < (b.label ?? 0) ? -1 : (a.label ?? 0) > (b.label ?? 0) ? 1 : 0,
);
} }
export const QueryLocation = (props: QueryLocationProps): JSX.Element => { export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
@ -58,7 +60,8 @@ export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
const element = useMemo(() => { const element = useMemo(() => {
if (locationDisplayMode === 'dropdown') { if (locationDisplayMode === 'dropdown') {
return 'select'; return 'select';
} else if (locationDisplayMode === 'gallery') { }
if (locationDisplayMode === 'gallery') {
return 'cards'; return 'cards';
} }
const groups = options.length; const groups = options.length;
@ -159,7 +162,8 @@ export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
)} )}
</> </>
); );
} else if (element === 'select') { }
if (element === 'select') {
return ( return (
<Select<LocationOption, true> <Select<LocationOption, true>
isMulti isMulti

View file

@ -60,10 +60,7 @@ function useOptions() {
const filtered = useFormState(s => s.filtered); const filtered = useFormState(s => s.filtered);
return useMemo((): OptionsOrGroup<QueryTypeOption> => { return useMemo((): OptionsOrGroup<QueryTypeOption> => {
const groupNames = new Set( const groupNames = new Set(
filtered.types filtered.types.filter(t => t.groups.length > 0).flatMap(t => t.groups),
.filter(t => t.groups.length > 0)
.map(t => t.groups)
.flat(),
); );
const optGroups: OptionGroup<QueryTypeOption>[] = Array.from(groupNames).map(group => ({ const optGroups: OptionGroup<QueryTypeOption>[] = Array.from(groupNames).map(group => ({
label: group, label: group,

View file

@ -1,14 +1,14 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */ // biome-ignore lint/suspicious/noExplicitAny: type guard
/* eslint @typescript-eslint/explicit-module-boundary-types: 0 */
export function isStackError(error: any): error is Error { export function isStackError(error: any): error is Error {
return typeof error !== 'undefined' && error !== null && 'message' in error; return typeof error !== 'undefined' && error !== null && 'message' in error;
} }
// biome-ignore lint/suspicious/noExplicitAny: type guard
export function isFetchError(error: any): error is Response { export function isFetchError(error: any): error is Response {
return typeof error !== 'undefined' && error !== null && 'statusText' in error; return typeof error !== 'undefined' && error !== null && 'statusText' in error;
} }
// biome-ignore lint/suspicious/noExplicitAny: type guard
export function isLGError(error: any): error is QueryResponse { export function isLGError(error: any): error is QueryResponse {
return typeof error !== 'undefined' && error !== null && 'output' in error; return typeof error !== 'undefined' && error !== null && 'output' in error;
} }
@ -16,6 +16,7 @@ export function isLGError(error: any): error is QueryResponse {
/** /**
* Returns true if the response is an LG error, false if not. * Returns true if the response is an LG error, false if not.
*/ */
// biome-ignore lint/suspicious/noExplicitAny: type guard
export function isLGOutputOrError(data: any): data is QueryResponse { export function isLGOutputOrError(data: any): data is QueryResponse {
return typeof data !== 'undefined' && data !== null && data?.level !== 'success'; return typeof data !== 'undefined' && data !== null && data?.level !== 'success';
} }

View file

@ -107,17 +107,20 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
const errorMsg = useMemo(() => { const errorMsg = useMemo(() => {
if (isLGError(error)) { if (isLGError(error)) {
return error.output as string; return error.output as string;
} else if (isLGOutputOrError(data)) {
return data.output as string;
} else if (isFetchError(error)) {
return startCase(error.statusText);
} else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
return messages.requestTimeout;
} else if (isStackError(error)) {
return startCase(error.message);
} else {
return messages.general;
} }
if (isLGOutputOrError(data)) {
return data.output as string;
}
if (isFetchError(error)) {
return startCase(error.statusText);
}
if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
return messages.requestTimeout;
}
if (isStackError(error)) {
return startCase(error.message);
}
return messages.general;
}, [error, data, messages.general, messages.requestTimeout]); }, [error, data, messages.general, messages.requestTimeout]);
const errorLevel = useMemo<ErrorLevels>(() => { const errorLevel = useMemo<ErrorLevels>(() => {

View file

@ -45,8 +45,8 @@ export const useControlStyle = <Opt extends SingleOption, IsMulti extends boolea
boxShadow: isError boxShadow: isError
? `0 0 0 1px ${invalidBorder}` ? `0 0 0 1px ${invalidBorder}`
: isFocused : isFocused
? `0 0 0 1px ${focusBorder}` ? `0 0 0 1px ${focusBorder}`
: undefined, : undefined,
'&:hover': { borderColor: isFocused ? focusBorder : borderHover }, '&:hover': { borderColor: isFocused ? focusBorder : borderHover },
'&:hover > div > span': { backgroundColor: borderHover }, '&:hover > div > span': { backgroundColor: borderHover },
'&:focus': { borderColor: isError ? invalidBorder : focusBorder }, '&:focus': { borderColor: isError ? invalidBorder : focusBorder },

View file

@ -30,7 +30,7 @@ export const TableRow = (props: TableRowProps): JSX.Element => {
{ borderTop: '1px', borderTopColor: 'blackAlpha.100' }, { borderTop: '1px', borderTopColor: 'blackAlpha.100' },
{ borderTop: '1px', borderTopColor: 'whiteAlpha.100' }, { borderTop: '1px', borderTopColor: 'whiteAlpha.100' },
); );
let bg; let bg = undefined;
if (highlight) { if (highlight) {
bg = `${String(highlightBg)}.${alpha}`; bg = `${String(highlightBg)}.${alpha}`;

View file

@ -6,6 +6,7 @@
export const CustomJavascript = (props: React.PropsWithChildren<Dict>): JSX.Element => { export const CustomJavascript = (props: React.PropsWithChildren<Dict>): JSX.Element => {
const { children } = props; const { children } = props;
if (typeof children === 'string' && children !== '') { if (typeof children === 'string' && children !== '') {
// biome-ignore lint/security/noDangerouslySetInnerHtml: required for injecting custom JS
return <script id="custom-javascript" dangerouslySetInnerHTML={{ __html: children }} />; return <script id="custom-javascript" dangerouslySetInnerHTML={{ __html: children }} />;
} }
return <></>; return <></>;
@ -19,6 +20,7 @@ export const CustomJavascript = (props: React.PropsWithChildren<Dict>): JSX.Elem
export const CustomHtml = (props: React.PropsWithChildren<Dict>): JSX.Element => { export const CustomHtml = (props: React.PropsWithChildren<Dict>): JSX.Element => {
const { children } = props; const { children } = props;
if (typeof children === 'string' && children !== '') { if (typeof children === 'string' && children !== '') {
// biome-ignore lint/security/noDangerouslySetInnerHtml: required for injecting custom HTML
return <div id="custom-html" dangerouslySetInnerHTML={{ __html: children }} />; return <div id="custom-html" dangerouslySetInnerHTML={{ __html: children }} />;
} }
return <></>; return <></>;

View file

@ -47,7 +47,7 @@ class IconError extends Error {
this.original = original; this.original = original;
this.library = library; this.library = library;
this.iconName = iconName; this.iconName = iconName;
this.stack = this.stack + `\nOriginal object: '${JSON.stringify(this.original)}'`; this.stack += `\nOriginal object: '${JSON.stringify(this.original)}'`;
} }
get message(): string { get message(): string {

View file

@ -68,7 +68,7 @@ type MDProps = {
node: Dict; node: Dict;
}; };
/* eslint @typescript-eslint/no-explicit-any: off */ // biome-ignore lint/suspicious/noExplicitAny: reasons!
function hasNode<C>(p: any): p is C & MDProps { function hasNode<C>(p: any): p is C & MDProps {
return 'node' in p; return 'node' in p;
} }

View file

@ -1,3 +1,4 @@
import { expect, describe, it } from 'vitest';
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { useBooleanValue } from './use-boolean-value'; import { useBooleanValue } from './use-boolean-value';

View file

@ -11,8 +11,7 @@ export function useBooleanValue<T extends unknown, F extends unknown>(
return useMemo(() => { return useMemo(() => {
if (status) { if (status) {
return ifTrue; return ifTrue;
} else {
return ifFalse;
} }
return ifFalse;
}, [status, ifTrue, ifFalse]); }, [status, ifTrue, ifFalse]);
} }

View file

@ -1,3 +1,4 @@
import { expect, describe, it } from 'vitest';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { useDevice } from './use-device'; import { useDevice } from './use-device';

View file

@ -14,10 +14,7 @@ export type UseDeviceReturn = (
export function useDevice(): UseDeviceReturn { export function useDevice(): UseDeviceReturn {
const { devices } = useConfig(); const { devices } = useConfig();
const locations = useMemo<Device[]>( const locations = useMemo<Device[]>(() => devices.flatMap(group => group.locations), [devices]);
() => devices.map(group => group.locations).flat(),
[devices],
);
function getDevice(id: string): Nullable<Device> { function getDevice(id: string): Nullable<Device> {
return locations.find(device => device.id === id) ?? null; return locations.find(device => device.id === id) ?? null;

View file

@ -1,4 +1,5 @@
import 'isomorphic-fetch'; import 'isomorphic-fetch';
import { expect, describe, it } from 'vitest';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
@ -16,7 +17,7 @@ const CloudflareWrapper = (props: React.PropsWithChildren<Dict<JSX.Element>>) =>
cache: { timeout: 120 }, cache: { timeout: 120 },
web: { dnsProvider: { url: 'https://cloudflare-dns.com/dns-query' } }, web: { dnsProvider: { url: 'https://cloudflare-dns.com/dns-query' } },
} as jest.Mocked<Config>; } as Config;
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<HyperglassContext.Provider value={config} {...props} /> <HyperglassContext.Provider value={config} {...props} />
@ -28,7 +29,7 @@ const GoogleWrapper = (props: React.PropsWithChildren<Dict<JSX.Element>>) => {
const config = { const config = {
cache: { timeout: 120 }, cache: { timeout: 120 },
web: { dnsProvider: { url: 'https://dns.google/resolve' } }, web: { dnsProvider: { url: 'https://dns.google/resolve' } },
} as jest.Mocked<Config>; } as Config;
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<HyperglassContext.Provider value={config} {...props} /> <HyperglassContext.Provider value={config} {...props} />

View file

@ -21,7 +21,7 @@ const query: QueryFunction<DnsOverHttps.Response, DNSQueryKey> = async (
const controller = new AbortController(); const controller = new AbortController();
let json; let json = undefined;
const type = family === 4 ? 'A' : family === 6 ? 'AAAA' : ''; const type = family === 4 ? 'A' : family === 6 ? 'AAAA' : '';
if (url !== null) { if (url !== null) {

View file

@ -61,10 +61,7 @@ interface FormStateType<Opt extends SingleOption = SingleOption> {
setSelection< setSelection<
Opt extends SingleOption, Opt extends SingleOption,
K extends keyof FormSelections<Opt> = keyof FormSelections<Opt>, K extends keyof FormSelections<Opt> = keyof FormSelections<Opt>,
>( >(field: K, value: FormSelections[K]): void;
field: K,
value: FormSelections[K],
): void;
setTarget(update: Partial<Target>): void; setTarget(update: Partial<Target>): void;
getDirective(): Directive | null; getDirective(): Directive | null;
reset(): void; reset(): void;
@ -155,7 +152,7 @@ const formState: StateCreator<FormStateType> = (set, get) => ({
// Determine all unique group names. // Determine all unique group names.
const allGroups = allDevices.map(dev => const allGroups = allDevices.map(dev =>
Array.from(new Set(dev.directives.map(dir => dir.groups).flat())), Array.from(new Set(dev.directives.flatMap(dir => dir.groups))),
); );
// Get group names that are common between all selected locations. // Get group names that are common between all selected locations.

View file

@ -1,6 +1,7 @@
import { expect, describe, it } from 'vitest';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import { userEvent } from '@testing-library/user-event';
import { useGreeting } from './use-greeting'; import { useGreeting } from './use-greeting';
const TRUE = JSON.stringify(true); const TRUE = JSON.stringify(true);
@ -26,16 +27,16 @@ const TestComponent = (): JSX.Element => {
Close Close
</button> </button>
<button id="ack-false-required" type="button" onClick={() => ack(false, true)}> <button id="ack-false-required" type="button" onClick={() => ack(false, true)}>
{`Don't acknowledge, is required`} Don't acknowledge, is required
</button> </button>
<button id="ack-true-required" type="button" onClick={() => ack(true, true)}> <button id="ack-true-required" type="button" onClick={() => ack(true, true)}>
{`Acknowledge, is required`} Acknowledge, is required
</button> </button>
<button id="ack-false-not-required" type="button" onClick={() => ack(false, false)}> <button id="ack-false-not-required" type="button" onClick={() => ack(false, false)}>
{`Don't Acknowledge, not required`} Don't Acknowledge, not required
</button> </button>
<button id="ack-true-not-required" type="button" onClick={() => ack(true, false)}> <button id="ack-true-not-required" type="button" onClick={() => ack(true, false)}>
{`Acknowledge, not required`} Acknowledge, not required
</button> </button>
</div> </div>
); );

View file

@ -1,6 +1,7 @@
import { expect, describe, it } from 'vitest';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event'; import { userEvent } from '@testing-library/user-event';
import { ChakraProvider, useColorMode, useColorModeValue, extendTheme } from '@chakra-ui/react'; import { ChakraProvider, useColorMode, useColorModeValue, extendTheme } from '@chakra-ui/react';
import { useOpposingColor } from './use-opposing-color'; import { useOpposingColor } from './use-opposing-color';
@ -32,17 +33,17 @@ describe('useOpposingColor Hook', () => {
const test1 = container.querySelector('#test1'); const test1 = container.querySelector('#test1');
const test2 = container.querySelector('#test2'); const test2 = container.querySelector('#test2');
expect(test1).toHaveStyle('color: black;'); expect(test1).toHaveStyle('color: rgb(0, 0, 0);');
expect(test2).toHaveStyle('color: black;'); expect(test2).toHaveStyle('color: rgb(0, 0, 0);');
await userEvent.click(getByRole('button')); await userEvent.click(getByRole('button'));
expect(test1).toHaveStyle('color: white;'); expect(test1).toHaveStyle('color: rgb(255, 255, 255);');
expect(test2).toHaveStyle('color: white;'); expect(test2).toHaveStyle('color: rgb(255, 255, 255);');
await userEvent.click(getByRole('button')); await userEvent.click(getByRole('button'));
expect(test1).toHaveStyle('color: black;'); expect(test1).toHaveStyle('color: rgb(0, 0, 0);');
expect(test2).toHaveStyle('color: black;'); expect(test2).toHaveStyle('color: rgb(0, 0, 0);');
}); });
}); });

View file

@ -22,12 +22,13 @@ export function useIsDarkCallback(): UseIsDarkCallbackReturn {
const theme = useTheme(); const theme = useTheme();
return useCallback( return useCallback(
(color: string): boolean => { (color: string): boolean => {
let opposing = color;
if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) { if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) {
color = getColor(theme, color, color); opposing = getColor(theme, color, color);
} }
let opposingShouldBeDark = true; let opposingShouldBeDark = true;
try { try {
opposingShouldBeDark = isLight(color)(theme); opposingShouldBeDark = isLight(opposing)(theme);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -46,9 +47,8 @@ export function useOpposingColor(color: string, options?: OpposingColorOptions):
return useMemo(() => { return useMemo(() => {
if (isBlack) { if (isBlack) {
return options?.dark ?? 'black'; return options?.dark ?? 'black';
} else {
return options?.light ?? 'white';
} }
return options?.light ?? 'white';
}, [isBlack, options?.dark, options?.light]); }, [isBlack, options?.dark, options?.light]);
} }

View file

@ -1,3 +1,4 @@
import { expect, describe, it } from 'vitest';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { useStrf } from './use-strf'; import { useStrf } from './use-strf';

View file

@ -29,7 +29,7 @@ function formatAsPath(path: number[]): string {
function formatCommunities(comms: string[]): string { function formatCommunities(comms: string[]): string {
const commsStr = comms.map(c => ` - ${c}`); const commsStr = comms.map(c => ` - ${c}`);
return '\n' + commsStr.join('\n'); return `\n ${commsStr.join('\n')}`;
} }
function formatBool(val: boolean): string { function formatBool(val: boolean): string {
@ -85,9 +85,8 @@ export function useTableToString(
function getFmtFunc(accessor: keyof Route): TableToStringFormatter { function getFmtFunc(accessor: keyof Route): TableToStringFormatter {
if (isFormatted(accessor)) { if (isFormatted(accessor)) {
return tableFormatMap[accessor]; return tableFormatMap[accessor];
} else {
return String;
} }
return String;
} }
function doFormat(target: string[], data: QueryResponse | undefined): string { function doFormat(target: string[], data: QueryResponse | undefined): string {

View file

@ -1,31 +0,0 @@
/**
* Jest Testing Configuration
*
* @see https://nextjs.org/docs/testing
* @type {import('@jest/types').Config.InitialOptions}
*/
const jestConfig = {
collectCoverageFrom: ['**/*.{ts,tsx}', '!**/*.d.ts', '!**/node_modules/**'],
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],
testEnvironment: 'jsdom',
transform: { '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }] },
transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^~/components': ['components/index'],
'^~/components/(.*)$': '<rootDir>/components/$1',
'^~/context': '<rootDir>/context/index',
'^~/context/(.*)$': '<rootDir>/context/$1',
'^~/hooks': '<rootDir>/hooks/index',
'^~/hooks/(.*)$': '<rootDir>/hooks/$1',
'^~/state': '<rootDir>/state/index',
'^~/state/(.*)$': '<rootDir>/state/$1',
'^~/types': '<rootDir>/types/index',
'^~/types/(.*)$': '<rootDir>/types/$1',
'^~/util': '<rootDir>/util/index',
'^~/util/(.*)$': '<rootDir>/util/$1',
},
};
module.exports = jestConfig;

View file

@ -1 +0,0 @@
import '@testing-library/jest-dom/extend-expect';

View file

@ -20,18 +20,19 @@ app
const devProxy = { const devProxy = {
'/api/query/': { '/api/query/': {
target: process.env.HYPERGLASS_URL + 'api/query/', target: `${process.env.HYPERGLASS_URL}api/query/`,
pathRewrite: { '^/api/query/': '' }, pathRewrite: { '^/api/query/': '' },
}, },
'/ui/props/': { '/ui/props/': {
target: process.env.HYPERGLASS_URL + 'ui/props/', target: `${process.env.HYPERGLASS_URL}ui/props/`,
pathRewrite: { '^/ui/props/': '' }, pathRewrite: { '^/ui/props/': '' },
}, },
'/images': { target: process.env.HYPERGLASS_URL + 'images', pathRewrite: { '^/images': '' } }, '/images': { target: `${process.env.HYPERGLASS_URL}images`, pathRewrite: { '^/images': '' } },
}; };
// Set up the proxy. // Set up the proxy.
if (dev) { if (dev) {
// biome-ignore lint/complexity/noForEach: not messing with Next's example code.
Object.keys(devProxy).forEach(context => { Object.keys(devProxy).forEach(context => {
server.use(proxyMiddleware(context, devProxy[context])); server.use(proxyMiddleware(context, devProxy[context]));
}); });

View file

@ -1,91 +1,94 @@
{ {
"version": "2.0.0-dev", "version": "2.0.0-dev",
"name": "ui", "name": "ui",
"description": "UI for hyperglass, the modern network looking glass", "description": "UI for hyperglass, the modern network looking glass",
"author": "Matt Love", "author": "Matt Love",
"license": "BSD-3-Clause-Clear", "license": "BSD-3-Clause-Clear",
"private": true, "private": true,
"scripts": { "scripts": {
"lint": "eslint . --ext .ts --ext .tsx", "lint": "biome lint .",
"dev": "node nextdev", "dev": "node nextdev",
"start": "next start", "start": "next start",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"format": "prettier --config ./.prettierrc -c -w .", "format": "biome format --write .",
"format:check": "prettier --config ./.prettierrc -c .", "format:check": "biome format .",
"build": "export NODE_OPTIONS=--openssl-legacy-provider; next build && next export -o ../hyperglass/static/ui", "build": "export NODE_OPTIONS=--openssl-legacy-provider; next build && next export -o ../hyperglass/static/ui",
"test": "jest" "test": "vitest --run"
}, },
"browserslist": "> 0.25%, not dead", "browserslist": "> 0.25%, not dead",
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.5.5", "@chakra-ui/react": "^2.5.5",
"@chakra-ui/theme": "3.0.1", "@chakra-ui/theme": "3.0.1",
"@chakra-ui/theme-tools": "^2.0.17", "@chakra-ui/theme-tools": "^2.0.17",
"@chakra-ui/utils": "^2.0.15", "@chakra-ui/utils": "^2.0.15",
"@emotion/react": "^11.10.6", "@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@hookform/devtools": "^4.3.0", "@hookform/devtools": "^4.3.0",
"@hookform/resolvers": "^2.9.10", "@hookform/resolvers": "^2.9.10",
"@tanstack/react-query": "^4.22.0", "@tanstack/react-query": "^4.22.0",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"framer-motion": "^10.11.6", "framer-motion": "^10.11.6",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"merge-anything": "^4.0.1", "merge-anything": "^4.0.1",
"next": "12.3.4", "next": "13.5.6",
"palette-by-numbers": "^0.1.6", "palette-by-numbers": "^0.1.6",
"plur": "^4.0.0", "plur": "^4.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-countdown": "^2.3.0", "react-countdown": "^2.3.0",
"react-device-detect": "^1.15.0", "react-device-detect": "^1.15.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-fast-compare": "^3.2.1", "react-fast-compare": "^3.2.1",
"react-flow-renderer": "^10.3.17", "react-flow-renderer": "^10.3.17",
"react-hook-form": "^7.42.1", "react-hook-form": "^7.42.1",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-if": "^4.1.4", "react-if": "^4.1.4",
"react-markdown": "^5.0.3", "react-markdown": "^5.0.3",
"react-select": "^5.7.0", "react-select": "^5.7.0",
"react-string-replace": "^0.5.0", "react-string-replace": "^0.5.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",
"remark-gfm": "^1.0.0", "remark-gfm": "^1.0.0",
"string-format": "^2.0.0", "string-format": "^2.0.0",
"vest": "^3.2.8", "vest": "^3.2.8",
"zustand": "^3.7.2" "zustand": "^3.7.2"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.16.5", "@biomejs/biome": "1.5.3",
"@testing-library/react": "^14.0.0", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/react-hooks": "^7.0.2", "@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.4.3", "@testing-library/react-hooks": "^8.0.1",
"@types/dagre": "^0.7.44", "@testing-library/user-event": "^14.5.2",
"@types/express": "^4.17.13", "@types/dagre": "^0.7.44",
"@types/lodash": "^4.14.177", "@types/express": "^4.17.21",
"@types/node": "^18.15.11", "@types/lodash": "^4.14.177",
"@types/react": "^18.0.35", "@types/node": "^20.11.20",
"@types/react-table": "^7.7.1", "@types/react": "^18.2.60",
"@types/string-format": "^2.0.0", "@types/react-table": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^4.31.0", "@types/string-format": "^2.0.0",
"@typescript-eslint/parser": "^4.31.0", "@typescript-eslint/eslint-plugin": "^7.1.0",
"babel-eslint": "^10.1.0", "@typescript-eslint/parser": "^7.1.0",
"babel-jest": "^27.2.1", "@vitejs/plugin-react": "^4.2.1",
"eslint": "^7.32.0", "@vitest/ui": "^1.3.1",
"eslint-config-prettier": "^8.3.0", "babel-eslint": "^10.1.0",
"eslint-import-resolver-typescript": "^2.4.0", "eslint": "^8.57.0",
"eslint-plugin-import": "^2.24.2", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-json": "^3.1.0", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-json": "^3.1.0",
"eslint-plugin-react": "^7.25.1", "eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-prettier": "^5.1.3",
"express": "^4.17.1", "eslint-plugin-react": "^7.33.2",
"http-proxy-middleware": "0.20.0", "eslint-plugin-react-hooks": "^4.6.0",
"identity-obj-proxy": "^3.0.0", "express": "^4.18.2",
"isomorphic-fetch": "^3.0.0", "http-proxy-middleware": "2.0.6",
"jest": "^27.2.1", "identity-obj-proxy": "^3.0.0",
"prettier": "^2.3.2", "isomorphic-fetch": "^3.0.0",
"prettier-eslint": "^13.0.0", "jsdom": "^24.0.0",
"react-test-renderer": "^18.2.0", "prettier": "^3.2.5",
"type-fest": "^3.8.0", "prettier-eslint": "^16.3.0",
"typescript": "^5.0.4" "react-test-renderer": "^18.2.0",
} "type-fest": "^4.10.3",
"typescript": "^5.3.3",
"vitest": "^1.3.1"
}
} }

View file

@ -1,45 +1,23 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Switch, Case, Default } from 'react-if';
import { Meta, Layout } from '~/components'; import { Meta, Layout } from '~/components';
import { HyperglassProvider } from '~/context'; import { HyperglassProvider } from '~/context';
import { LoadError, Loading } from '~/elements'; import * as config from '../hyperglass.json';
import { useHyperglassConfig } from '~/hooks';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import type { Config } from '~/types';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const AppComponent = (props: AppProps) => {
const { Component, pageProps } = props;
const { data, error, isLoading, ready, refetch, showError, isLoadingInitial } =
useHyperglassConfig();
return (
<Switch>
<Case condition={isLoadingInitial}>
<Loading />
</Case>
<Case condition={showError}>
<LoadError error={error!} retry={refetch} inProgress={isLoading} />
</Case>
<Case condition={ready}>
<HyperglassProvider config={data!}>
<Meta />
<Layout>
<Component {...pageProps} />
</Layout>
</HyperglassProvider>
</Case>
<Default>
<LoadError error={error!} retry={refetch} inProgress={isLoading} />
</Default>
</Switch>
);
};
const App = (props: AppProps): JSX.Element => { const App = (props: AppProps): JSX.Element => {
const { Component, pageProps } = props;
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AppComponent {...props} /> <HyperglassProvider config={config as unknown as Config}>
<Meta />
<Layout>
<Component {...pageProps} />
</Layout>
</HyperglassProvider>
</QueryClientProvider> </QueryClientProvider>
); );
}; };

View file

@ -2,8 +2,9 @@ import fs from 'fs';
import Document, { Html, Head, Main, NextScript } from 'next/document'; import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ColorModeScript } from '@chakra-ui/react'; import { ColorModeScript } from '@chakra-ui/react';
import { CustomJavascript, CustomHtml, Favicon } from '~/elements'; import { CustomJavascript, CustomHtml, Favicon } from '~/elements';
import { getHyperglassConfig, googleFontUrl } from '~/util'; import { googleFontUrl } from '~/util';
import favicons from '../favicon-formats'; import favicons from '../favicon-formats';
import config from '../hyperglass.json';
import type { DocumentContext, DocumentInitialProps } from 'next/document'; import type { DocumentContext, DocumentInitialProps } from 'next/document';
import type { ThemeConfig } from '~/types'; import type { ThemeConfig } from '~/types';
@ -18,8 +19,8 @@ interface DocumentExtra
class MyDocument extends Document<DocumentExtra> { class MyDocument extends Document<DocumentExtra> {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentExtra> { static async getInitialProps(ctx: DocumentContext): Promise<DocumentExtra> {
const initialProps = await Document.getInitialProps(ctx); const initialProps = await Document.getInitialProps(ctx);
let customJs = '', let customJs = '';
customHtml = ''; let customHtml = '';
if (fs.existsSync('custom.js')) { if (fs.existsSync('custom.js')) {
customJs = fs.readFileSync('custom.js').toString(); customJs = fs.readFileSync('custom.js').toString();
@ -31,18 +32,18 @@ class MyDocument extends Document<DocumentExtra> {
let fonts = { body: '', mono: '' }; let fonts = { body: '', mono: '' };
let defaultColorMode: 'light' | 'dark' | null = null; let defaultColorMode: 'light' | 'dark' | null = null;
const hyperglassUrl = process.env.HYPERGLASS_URL ?? ''; // const hyperglassUrl = process.env.HYPERGLASS_URL ?? '';
const { // const {
web: { // web: {
theme: { fonts: themeFonts, defaultColorMode: themeDefaultColorMode }, // theme: { fonts: themeFonts, defaultColorMode: themeDefaultColorMode },
}, // },
} = await getHyperglassConfig(hyperglassUrl); // } = await getHyperglassConfig(hyperglassUrl);
fonts = { fonts = {
body: googleFontUrl(themeFonts.body), body: googleFontUrl(config.web.theme.fonts.body),
mono: googleFontUrl(themeFonts.mono), mono: googleFontUrl(config.web.theme.fonts.mono),
}; };
defaultColorMode = themeDefaultColorMode; defaultColorMode = config.web.theme.defaultColorMode;
return { customJs, customHtml, fonts, defaultColorMode, ...initialProps }; return { customJs, customHtml, fonts, defaultColorMode, ...initialProps };
} }
@ -63,8 +64,8 @@ class MyDocument extends Document<DocumentExtra> {
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link href={this.props.fonts.mono} rel="stylesheet" /> <link href={this.props.fonts.mono} rel="stylesheet" />
<link href={this.props.fonts.body} rel="stylesheet" /> <link href={this.props.fonts.body} rel="stylesheet" />
{favicons.map((favicon, idx) => ( {favicons.map(favicon => (
<Favicon key={idx} {...favicon} /> <Favicon key={JSON.stringify(favicon)} {...favicon} />
))} ))}
<CustomJavascript>{this.props.customJs}</CustomJavascript> <CustomJavascript>{this.props.customJs}</CustomJavascript>
</Head> </Head>

File diff suppressed because it is too large Load diff

View file

@ -40,6 +40,7 @@
"**/*.tsx", "**/*.tsx",
"types/*.d.ts", "types/*.d.ts",
"next.config.js", "next.config.js",
"nextdev.js" "nextdev.js",
"hyperglass.json"
] ]
} }

View file

@ -1,7 +1,7 @@
import type { Theme } from './theme'; import type { Theme } from './theme';
import type { CamelCasedPropertiesDeep, CamelCasedProperties } from 'type-fest'; import type { CamelCasedPropertiesDeep, CamelCasedProperties } from 'type-fest';
type Side = 'left' | 'right'; type Side = 'left' | 'right' | string;
export type ParsedDataField = [string, keyof Route, 'left' | 'right' | 'center' | null]; export type ParsedDataField = [string, keyof Route, 'left' | 'right' | 'center' | null];
@ -92,19 +92,17 @@ interface _Web {
links: _Link[]; links: _Link[];
menus: _Menu[]; menus: _Menu[];
greeting: _Greeting; greeting: _Greeting;
help_menu: { enable: boolean; title: string };
logo: _Logo; logo: _Logo;
terms: { enable: boolean; title: string };
text: _Text; text: _Text;
theme: _ThemeConfig; theme: _ThemeConfig;
location_display_mode: 'auto' | 'gallery' | 'dropdown'; location_display_mode: 'auto' | 'gallery' | 'dropdown' | string;
highlight: _Highlight[]; highlight: _Highlight[];
} }
type _DirectiveBase = { type _DirectiveBase = {
id: string; id: string;
name: string; name: string;
field_type: 'text' | 'select' | null; field_type: 'text' | 'select' | null | string;
description: string; description: string;
groups: string[]; groups: string[];
info: string | null; info: string | null;
@ -125,7 +123,7 @@ type _Directive = _DirectiveBase | _DirectiveSelect;
interface _Device { interface _Device {
id: string; id: string;
name: string; name: string;
group: string; group: string | null;
avatar: string | null; avatar: string | null;
directives: _Directive[]; directives: _Directive[];
description: string | null; description: string | null;
@ -144,7 +142,7 @@ interface _Cache {
type _Config = _ConfigDeep & _ConfigShallow; type _Config = _ConfigDeep & _ConfigShallow;
interface _DeviceGroup { interface _DeviceGroup {
group: string; group: string | null;
locations: _Device[]; locations: _Device[];
} }
@ -157,7 +155,6 @@ interface _ConfigDeep {
} }
interface _ConfigShallow { interface _ConfigShallow {
debug: boolean;
developer_mode: boolean; developer_mode: boolean;
primary_asn: string; primary_asn: string;
request_timeout: number; request_timeout: number;

View file

@ -55,10 +55,16 @@ export declare global {
export interface ProcessEnv { export interface ProcessEnv {
hyperglass: { favicons: import('./config').Favicon[]; version: string }; hyperglass: { favicons: import('./config').Favicon[]; version: string };
buildId: string; buildId: string;
UI_PARAMS: import('./config').Config;
} }
} }
} }
declare module 'hyperglass.json' {
type Config = import('./config').Config;
export default Config;
}
declare module 'react' { declare module 'react' {
// Enable generic typing with forwardRef. // Enable generic typing with forwardRef.
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types

View file

@ -1,10 +1,13 @@
import { expect, describe, it, test } from 'vitest';
import { all, chunkArray, entries, dedupObjectArray, andJoin, isFQDN } from './common'; import { all, chunkArray, entries, dedupObjectArray, andJoin, isFQDN } from './common';
test('all - all items are truthy', () => { test('all - all items are truthy', () => {
// biome-ignore lint/suspicious/noSelfCompare: because this is a test, duh
expect(all(1 === 1, true, 'one' === 'one')).toBe(true); expect(all(1 === 1, true, 'one' === 'one')).toBe(true);
}); });
test('all - one item is not truthy', () => { test('all - one item is not truthy', () => {
// biome-ignore lint/suspicious/noSelfCompare: because this is a test, duh
expect(all(1 === 1, false, 'one' === 'one')).toBe(false); expect(all(1 === 1, false, 'one' === 'one')).toBe(false);
}); });

View file

@ -35,6 +35,7 @@ export function entries<O, K extends keyof O = keyof O>(obj: O): [K, O[K]][] {
*/ */
export async function fetchWithTimeout( export async function fetchWithTimeout(
uri: string, uri: string,
// biome-ignore lint/style/useDefaultParameterLast: goal is to match the fetch API as closely as possible.
options: RequestInit = {}, options: RequestInit = {},
timeout: number, timeout: number,
controller: AbortController, controller: AbortController,
@ -69,9 +70,8 @@ export function dedupObjectArray<E extends Record<string, unknown>, P extends ke
if (!x) { if (!x) {
return acc.concat([current]); return acc.concat([current]);
} else {
return acc;
} }
return acc;
}, []); }, []);
} }

View file

@ -11,7 +11,7 @@ export class ConfigLoadError extends Error {
constructor(detail?: string) { constructor(detail?: string) {
super(); super();
this.detail = detail; this.detail = detail;
this.baseMessage = `Unable to connect to hyperglass at`; this.baseMessage = 'Unable to connect to hyperglass at';
this.message = `${this.baseMessage} '${this.url}'`; this.message = `${this.baseMessage} '${this.url}'`;
console.error(this); console.error(this);
} }
@ -30,7 +30,7 @@ export async function getHyperglassConfig(url?: QueryFunctionContext | string):
let fetchUrl = '/ui/props/'; let fetchUrl = '/ui/props/';
if (typeof url === 'string') { if (typeof url === 'string') {
fetchUrl = url.replace(/(^\/)|(\/$)/g, '') + '/ui/props/'; fetchUrl = `${url.replace(/(^\/)|(\/$)/g, '')}/ui/props`;
} }
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {

View file

@ -8,7 +8,6 @@ import type { StateCreator, SetState, GetState, StoreApi } from 'zustand';
* @param store zustand store function. * @param store zustand store function.
* @param name Store name. * @param name Store name.
*/ */
// eslint-disable-next-line @typescript-eslint/ban-types
export function withDev<T extends object = {}>( export function withDev<T extends object = {}>(
store: StateCreator<T>, store: StateCreator<T>,
name: string, name: string,

View file

@ -1,4 +1,5 @@
import { googleFontUrl } from './theme'; import { googleFontUrl } from './theme';
import { describe, expect, test } from 'vitest';
describe('google font URL generation', () => { describe('google font URL generation', () => {
test('no space font', () => { test('no space font', () => {

View file

@ -0,0 +1,18 @@
/// <reference types="vitest" />
import path from 'node:path';
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
},
resolve: {
alias: {
'~': path.resolve(__dirname, './'),
},
},
});

View file

@ -16,14 +16,13 @@ from .tools import (
run_coroutine_in_new_thread, run_coroutine_in_new_thread,
) )
from .typing import is_type, is_series from .typing import is_type, is_series
from .frontend import build_ui, build_frontend
from .validation import get_driver, resolve_hostname, validate_platform from .validation import get_driver, resolve_hostname, validate_platform
from .system_info import cpu_count, check_python, get_system_info, get_node_version from .system_info import cpu_count, check_python, get_system_info, get_node_version
__all__ = ( __all__ = (
"at_least", "at_least",
"build_frontend", # "build_frontend",
"build_ui", # "build_ui",
"check_path", "check_path",
"check_python", "check_python",
"compare_dicts", "compare_dicts",

View file

@ -1,70 +1,64 @@
[project]
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core"]
[tool.poetry]
authors = ["Matt Love <matt@hyperglass.dev>"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Information Technology",
"Operating System :: POSIX :: Linux",
"Programming Language :: TypeScript",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Internet",
"Topic :: System :: Networking",
]
description = "hyperglass is the modern network looking glass that tries to make the internet better."
documentation = "https://hyperglass.dev"
homepage = "https://hyperglass.dev"
keywords = ["looking glass", "network automation", "isp", "bgp", "routing"]
license = "BSD-3-Clause-Clear"
name = "hyperglass" name = "hyperglass"
readme = "README.md"
repository = "https://github.com/thatmattlove/hyperglass"
version = "2.0.0-dev" version = "2.0.0-dev"
description = "hyperglass is the modern network looking glass that tries to make the internet better."
authors = [
{ name = "thatmattlove", email = "matt@hyperglass.dev" }
]
dependencies = [
"Pillow==10.2.0",
"PyJWT==2.6.0",
"PyYAML>=6.0",
"aiofiles>=23.2.1",
"distro==1.8.0",
"fastapi==0.95.1",
"favicons==0.2.2",
"gunicorn==20.1.0",
"httpx==0.24.0",
"loguru==0.7.0",
"netmiko==4.1.2",
"paramiko==3.4.0",
"psutil==5.9.4",
"py-cpuinfo==9.0.0",
"pydantic==1.10.14",
"redis==4.5.4",
"rich>=13.7.0",
"typer>=0.9.0",
"uvicorn==0.21.1",
"uvloop>=0.17.0",
"xmltodict==0.13.0",
]
readme = "README.md"
requires-python = ">= 3.11"
[tool.poetry.scripts] [project.scripts]
hyperglass = "hyperglass.console:run" hyperglass = "hyperglass.console:run"
[tool.poetry.dependencies] [build-system]
Pillow = "^9.5.0" requires = ["hatchling"]
PyJWT = "^2.6.0" build-backend = "hatchling.build"
PyYAML = "^6.0"
aiofiles = "^23.1.0"
distro = "^1.8.0"
fastapi = "^0.95.1"
favicons = "^0.2.0"
gunicorn = "^20.1.0"
httpx = "^0.24.0"
loguru = "^0.7.0"
netmiko = "^4.1.2"
paramiko = "^3.1.0"
psutil = "^5.9.4"
py-cpuinfo = "^9.0.0"
pydantic = "^1.10.7"
python = ">=3.8.1,<4.0"
redis = "^4.5.4"
rich = "^13.3.4"
typer = "^0.7.0"
uvicorn = "^0.21.1"
uvloop = "^0.17.0"
xmltodict = "^0.13.0"
[tool.poetry.group.dev.dependencies] [tool.rye]
bandit = "^1.7.4" managed = true
black = "^22.12.0" dev-dependencies = [
isort = "^5.10.1" "bandit>=1.7.7",
pep8-naming = "^0.13.2" "black>=24.2.0",
pre-commit = "^2.20.0" "isort>=5.13.2",
pytest = "^7.2.0" "pep8-naming>=0.13.3",
pytest-asyncio = "^0.20.3" "pre-commit>=3.6.1",
pytest-dependency = "^0.5.1" "pytest>=8.0.1",
ruff = "^0.0.261" "pytest-asyncio>=0.23.5",
stackprinter = "^0.2.10" "pytest-dependency>=0.6.0",
taskipy = "^1.10.3" "ruff>=0.2.1",
"stackprinter>=0.2.11",
"taskipy>=1.12.2",
]
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["hyperglass"]
[tool.black] [tool.black]
line-length = 100 line-length = 100
@ -85,13 +79,6 @@ multi_line_output = 3
profile = "black" profile = "black"
skip_glob = "hyperglass/api/examples/*.py" skip_glob = "hyperglass/api/examples/*.py"
[tool.pyright]
exclude = ["**/node_modules", "**/ui", "**/__pycache__"]
include = ["hyperglass"]
pythonVersion = "3.9"
reportMissingImports = true
reportMissingTypeStubs = true
[tool.taskipy.tasks] [tool.taskipy.tasks]
check = {cmd = "task lint && task ui-lint", help = "Run all lint checks"} check = {cmd = "task lint && task ui-lint", help = "Run all lint checks"}
docs-platforms = {cmd = "python3 -c 'from hyperglass.util.docs import create_platform_list;print(create_platform_list())'"} docs-platforms = {cmd = "python3 -c 'from hyperglass.util.docs import create_platform_list;print(create_platform_list())'"}
@ -102,12 +89,12 @@ start = {cmd = "python3 -m hyperglass.main", help = "Start hyperglass"}
start-asgi = {cmd = "uvicorn hyperglass.api:app", help = "Start hyperglass via Uvicorn"} start-asgi = {cmd = "uvicorn hyperglass.api:app", help = "Start hyperglass via Uvicorn"}
test = {cmd = "pytest hyperglass --ignore hyperglass/plugins/external", help = "Run hyperglass tests"} test = {cmd = "pytest hyperglass --ignore hyperglass/plugins/external", help = "Run hyperglass tests"}
ui-build = {cmd = "python3 -m hyperglass.console build-ui", help = "Run a UI Build"} ui-build = {cmd = "python3 -m hyperglass.console build-ui", help = "Run a UI Build"}
ui-dev = {cmd = "yarn --cwd ./hyperglass/ui/ dev", help = "Start the Next.JS dev server"} ui-dev = {cmd = "pnpm run --dir ./hyperglass/ui/ dev", help = "Start the Next.JS dev server"}
ui-format = {cmd = "yarn --cwd ./hyperglass/ui/ format", help = "Run Prettier"} ui-format = {cmd = "pnpm run --dir ./hyperglass/ui/ format", help = "Run Prettier"}
ui-lint = {cmd = "yarn --cwd ./hyperglass/ui/ lint", help = "Run ESLint"} ui-lint = {cmd = "pnpm run --dir ./hyperglass/ui/ lint", help = "Run ESLint"}
ui-typecheck = {cmd = "yarn --cwd ./hyperglass/ui/ typecheck", help = "Run TypeScript Check"} ui-typecheck = {cmd = "pnpm run --dir ./hyperglass/ui/ typecheck", help = "Run TypeScript Check"}
upgrade = {cmd = "python3 version.py", help = "Upgrade hyperglass version"} upgrade = {cmd = "python3 version.py", help = "Upgrade hyperglass version"}
yarn = {cmd = "yarn --cwd ./hyperglass/ui/", help = "Run a yarn command from the UI directory"} pnpm = {cmd = "pnpm run --dir ./hyperglass/ui/", help = "Run a yarn command from the UI directory"}
[tool.ruff] [tool.ruff]
exclude = [ exclude = [
@ -148,7 +135,7 @@ convention = "pep257"
[tool.ruff.mccabe] [tool.ruff.mccabe]
max-complexity = 10 max-complexity = 10
[tool.ruff.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"hyperglass/main.py" = ["E402"] "hyperglass/main.py" = ["E402"]
# Disable classmethod warning for validator decorat # Disable classmethod warning for validator decorat
"hyperglass/configuration/models/*.py" = ["N805"] "hyperglass/configuration/models/*.py" = ["N805"]

206
requirements-dev.lock Normal file
View file

@ -0,0 +1,206 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
-e file:.
aiofiles==23.2.1
# via hyperglass
anyio==4.3.0
# via httpcore
# via starlette
bandit==1.7.7
bcrypt==4.1.2
# via paramiko
black==24.2.0
certifi==2024.2.2
# via httpcore
# via httpx
cffi==1.16.0
# via cryptography
# via pynacl
cfgv==3.4.0
# via pre-commit
chardet==5.2.0
# via reportlab
click==8.1.7
# via black
# via typer
# via uvicorn
colorama==0.4.6
# via taskipy
cryptography==42.0.3
# via paramiko
cssselect2==0.7.0
# via svglib
distlib==0.3.8
# via virtualenv
distro==1.8.0
# via hyperglass
fastapi==0.95.1
# via hyperglass
favicons==0.2.2
# via hyperglass
filelock==3.13.1
# via virtualenv
flake8==7.0.0
# via pep8-naming
freetype-py==2.4.0
# via rlpycairo
future==0.18.3
# via textfsm
gunicorn==20.1.0
# via hyperglass
h11==0.14.0
# via httpcore
# via uvicorn
httpcore==0.17.3
# via httpx
httpx==0.24.0
# via hyperglass
identify==2.5.35
# via pre-commit
idna==3.6
# via anyio
# via httpx
iniconfig==2.0.0
# via pytest
isort==5.13.2
loguru==0.7.0
# via hyperglass
lxml==5.1.0
# via svglib
markdown-it-py==3.0.0
# via rich
mccabe==0.7.0
# via flake8
mdurl==0.1.2
# via markdown-it-py
mypy-extensions==1.0.0
# via black
netmiko==4.1.2
# via hyperglass
nodeenv==1.8.0
# via pre-commit
ntc-templates==4.3.0
# via netmiko
packaging==23.2
# via black
# via pytest
paramiko==3.4.0
# via hyperglass
# via netmiko
# via scp
pathspec==0.12.1
# via black
pbr==6.0.0
# via stevedore
pep8-naming==0.13.3
pillow==10.2.0
# via favicons
# via hyperglass
# via reportlab
platformdirs==4.2.0
# via black
# via virtualenv
pluggy==1.4.0
# via pytest
pre-commit==3.6.2
psutil==5.9.4
# via hyperglass
# via taskipy
py-cpuinfo==9.0.0
# via hyperglass
pycairo==1.26.0
# via rlpycairo
pycodestyle==2.11.1
# via flake8
pycparser==2.21
# via cffi
pydantic==1.10.14
# via fastapi
# via hyperglass
pyflakes==3.2.0
# via flake8
pygments==2.17.2
# via rich
pyjwt==2.6.0
# via hyperglass
pynacl==1.5.0
# via paramiko
pyserial==3.5
# via netmiko
pytest==8.0.1
# via pytest-asyncio
# via pytest-dependency
pytest-asyncio==0.23.5
pytest-dependency==0.6.0
pyyaml==6.0.1
# via bandit
# via hyperglass
# via netmiko
# via pre-commit
redis==4.5.4
# via hyperglass
reportlab==4.1.0
# via favicons
# via svglib
rich==13.7.0
# via bandit
# via favicons
# via hyperglass
rlpycairo==0.3.0
# via favicons
ruff==0.2.2
scp==0.14.5
# via netmiko
setuptools==69.1.0
# via gunicorn
# via netmiko
# via nodeenv
# via pytest-dependency
six==1.16.0
# via textfsm
sniffio==1.3.0
# via anyio
# via httpcore
# via httpx
stackprinter==0.2.11
starlette==0.26.1
# via fastapi
stevedore==5.1.0
# via bandit
svglib==1.5.1
# via favicons
taskipy==1.12.2
tenacity==8.2.3
# via netmiko
textfsm==1.1.2
# via netmiko
# via ntc-templates
tinycss2==1.2.1
# via cssselect2
# via svglib
tomli==2.0.1
# via taskipy
typer==0.9.0
# via favicons
# via hyperglass
typing-extensions==4.9.0
# via pydantic
# via typer
uvicorn==0.21.1
# via hyperglass
uvloop==0.17.0
# via hyperglass
virtualenv==20.25.0
# via pre-commit
webencodings==0.5.1
# via cssselect2
# via tinycss2
xmltodict==0.13.0
# via hyperglass

144
requirements.lock Normal file
View file

@ -0,0 +1,144 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
-e file:.
aiofiles==23.2.1
# via hyperglass
anyio==4.3.0
# via httpcore
# via starlette
bcrypt==4.1.2
# via paramiko
certifi==2024.2.2
# via httpcore
# via httpx
cffi==1.16.0
# via cryptography
# via pynacl
chardet==5.2.0
# via reportlab
click==8.1.7
# via typer
# via uvicorn
cryptography==42.0.3
# via paramiko
cssselect2==0.7.0
# via svglib
distro==1.8.0
# via hyperglass
fastapi==0.95.1
# via hyperglass
favicons==0.2.2
# via hyperglass
freetype-py==2.4.0
# via rlpycairo
future==0.18.3
# via textfsm
gunicorn==20.1.0
# via hyperglass
h11==0.14.0
# via httpcore
# via uvicorn
httpcore==0.17.3
# via httpx
httpx==0.24.0
# via hyperglass
idna==3.6
# via anyio
# via httpx
loguru==0.7.0
# via hyperglass
lxml==5.1.0
# via svglib
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
netmiko==4.1.2
# via hyperglass
ntc-templates==4.3.0
# via netmiko
paramiko==3.4.0
# via hyperglass
# via netmiko
# via scp
pillow==10.2.0
# via favicons
# via hyperglass
# via reportlab
psutil==5.9.4
# via hyperglass
py-cpuinfo==9.0.0
# via hyperglass
pycairo==1.26.0
# via rlpycairo
pycparser==2.21
# via cffi
pydantic==1.10.14
# via fastapi
# via hyperglass
pygments==2.17.2
# via rich
pyjwt==2.6.0
# via hyperglass
pynacl==1.5.0
# via paramiko
pyserial==3.5
# via netmiko
pyyaml==6.0.1
# via hyperglass
# via netmiko
redis==4.5.4
# via hyperglass
reportlab==4.1.0
# via favicons
# via svglib
rich==13.7.0
# via favicons
# via hyperglass
rlpycairo==0.3.0
# via favicons
scp==0.14.5
# via netmiko
setuptools==69.1.0
# via gunicorn
# via netmiko
six==1.16.0
# via textfsm
sniffio==1.3.0
# via anyio
# via httpcore
# via httpx
starlette==0.26.1
# via fastapi
svglib==1.5.1
# via favicons
tenacity==8.2.3
# via netmiko
textfsm==1.1.2
# via netmiko
# via ntc-templates
tinycss2==1.2.1
# via cssselect2
# via svglib
typer==0.9.0
# via favicons
# via hyperglass
typing-extensions==4.9.0
# via pydantic
# via typer
uvicorn==0.21.1
# via hyperglass
uvloop==0.17.0
# via hyperglass
webencodings==0.5.1
# via cssselect2
# via tinycss2
xmltodict==0.13.0
# via hyperglass