mirror of
https://github.com/thatmattlove/hyperglass.git
synced 2026-01-17 00:38:06 +00:00
countless updates I mostly don't remember.
This commit is contained in:
parent
b94eb3006a
commit
446853e4a9
45 changed files with 3302 additions and 3549 deletions
|
|
@ -5,11 +5,10 @@ repos:
|
|||
- id: isort
|
||||
args: ['--profile', 'black', '--filter-files', '--check']
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.192
|
||||
rev: v0.0.261
|
||||
hooks:
|
||||
- id: ruff
|
||||
# Respect `exclude` and `extend-exclude` settings.
|
||||
args: ['--force-exclude']
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: typescript
|
||||
|
|
|
|||
1
hyperglass/external/_base.py
vendored
1
hyperglass/external/_base.py
vendored
|
|
@ -155,7 +155,6 @@ class BaseExternal:
|
|||
try:
|
||||
parsed = _json.loads(response)
|
||||
except (JSONDecodeError, TypeError):
|
||||
log.error("Error parsing JSON for response {}", repr(response))
|
||||
parsed = {"data": response.text}
|
||||
else:
|
||||
parsed = response
|
||||
|
|
|
|||
|
|
@ -57,8 +57,10 @@ HyperglassConsole = Console(
|
|||
)
|
||||
)
|
||||
|
||||
log = _loguru_logger
|
||||
|
||||
class LibIntercentHandler(logging.Handler):
|
||||
|
||||
class LibInterceptHandler(logging.Handler):
|
||||
"""Custom log handler for integrating third party library logging with hyperglass's logger."""
|
||||
|
||||
def emit(self, record):
|
||||
|
|
@ -104,10 +106,10 @@ def setup_lib_logging(log_level: str) -> None:
|
|||
See: https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/
|
||||
"""
|
||||
|
||||
intercept_handler = LibIntercentHandler()
|
||||
|
||||
seen = set()
|
||||
for name in [
|
||||
intercept_handler = LibInterceptHandler()
|
||||
names = {
|
||||
name.split(".")[0]
|
||||
for name in (
|
||||
*logging.root.manager.loggerDict.keys(),
|
||||
"gunicorn",
|
||||
"gunicorn.access",
|
||||
|
|
@ -119,9 +121,9 @@ def setup_lib_logging(log_level: str) -> None:
|
|||
"netmiko",
|
||||
"paramiko",
|
||||
"httpx",
|
||||
]:
|
||||
if name not in seen:
|
||||
seen.add(name.split(".")[0])
|
||||
)
|
||||
}
|
||||
for name in names:
|
||||
logging.getLogger(name).handlers = [intercept_handler]
|
||||
|
||||
|
||||
|
|
@ -165,20 +167,12 @@ def init_logger(level: str = "INFO"):
|
|||
return _loguru_logger
|
||||
|
||||
|
||||
log = init_logger()
|
||||
|
||||
logging.addLevelName(25, "SUCCESS")
|
||||
|
||||
|
||||
def _log_success(self: "LoguruLogger", message: str, *a: t.Any, **kw: t.Any) -> None:
|
||||
"""Add custom builtin logging handler for the success level."""
|
||||
if self.isEnabledFor(25):
|
||||
self._log(25, message, a, **kw)
|
||||
|
||||
|
||||
logging.Logger.success = _log_success
|
||||
|
||||
|
||||
def enable_file_logging(
|
||||
log_directory: "Path", log_format: "LogFormat", log_max_size: "ByteSize", debug: bool
|
||||
) -> None:
|
||||
|
|
@ -238,3 +232,8 @@ def enable_syslog_logging(syslog_host: str, syslog_port: int) -> None:
|
|||
str(syslog_host),
|
||||
str(syslog_port),
|
||||
)
|
||||
|
||||
|
||||
# Side Effects
|
||||
logging.addLevelName(25, "SUCCESS")
|
||||
logging.Logger.success = _log_success
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from gunicorn.arbiter import Arbiter # type: ignore
|
|||
from gunicorn.app.base import BaseApplication # type: ignore
|
||||
|
||||
# Local
|
||||
from .log import CustomGunicornLogger, log, setup_lib_logging
|
||||
from .log import CustomGunicornLogger, log, init_logger, setup_lib_logging
|
||||
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__
|
||||
|
|
@ -23,10 +23,11 @@ if sys.version_info < MIN_PYTHON_VERSION:
|
|||
raise RuntimeError(f"Python {pretty_version}+ is required.")
|
||||
|
||||
# Ensure the NodeJS version meets the minimum requirements.
|
||||
node_major, _, __ = get_node_version()
|
||||
node_major, node_minor, node_patch = get_node_version()
|
||||
|
||||
if node_major < MIN_NODE_VERSION:
|
||||
raise RuntimeError(f"NodeJS {MIN_NODE_VERSION!s}+ is required.")
|
||||
installed = ".".join(str(v) for v in (node_major, node_minor, node_patch))
|
||||
raise RuntimeError(f"NodeJS {MIN_NODE_VERSION!s}+ is required (version {installed} installed)")
|
||||
|
||||
|
||||
# Local
|
||||
|
|
@ -175,6 +176,7 @@ def run(_workers: int = None):
|
|||
if _workers is not None:
|
||||
workers = _workers
|
||||
|
||||
init_logger(log_level)
|
||||
setup_lib_logging(log_level)
|
||||
start(log_level=log_level, workers=workers)
|
||||
except Exception as error:
|
||||
|
|
@ -188,6 +190,8 @@ def run(_workers: int = None):
|
|||
except SystemExit:
|
||||
# Handle Gunicorn exit.
|
||||
sys.exit(4)
|
||||
except BaseException:
|
||||
sys.exit(4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class Query(BaseModel):
|
|||
|
||||
def __repr__(self) -> str:
|
||||
"""Represent only the query fields."""
|
||||
return repr_from_attrs(self, self.__config__.fields.keys())
|
||||
return repr_from_attrs(self, ("query_location", "query_type", "query_target"))
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Alias __str__ to __repr__."""
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ class Params(ParamsPublic, HyperglassModel):
|
|||
|
||||
schema_extra = {"level": 1}
|
||||
|
||||
def __init__(self, **kw: Any) -> None:
|
||||
return super().__init__(**self.convert_paths(kw))
|
||||
|
||||
@validator("site_description")
|
||||
def validate_site_description(cls: "Params", value: str, values: Dict[str, Any]) -> str:
|
||||
"""Format the site descripion with the org_name field."""
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
# Standard Library
|
||||
import re
|
||||
import typing as t
|
||||
from pathlib import Path
|
||||
|
||||
# Third Party
|
||||
from pydantic import StrictInt, StrictFloat
|
||||
|
|
@ -111,3 +112,39 @@ class HttpMethod(str):
|
|||
def __repr__(self):
|
||||
"""Stringify custom field representation."""
|
||||
return f"HttpMethod({super().__repr__()})"
|
||||
|
||||
|
||||
class ConfigPathItem(Path):
|
||||
"""Custom field type for files or directories contained within app_path."""
|
||||
|
||||
@classmethod
|
||||
def __get_validators__(cls):
|
||||
"""Pydantic custom field method."""
|
||||
yield cls.validate
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value: Path) -> Path:
|
||||
"""Ensure path is within app path."""
|
||||
|
||||
if isinstance(value, str):
|
||||
value = Path(value)
|
||||
|
||||
if not isinstance(value, Path):
|
||||
raise TypeError("Unable to convert type {} to ConfigPathItem".format(type(value)))
|
||||
|
||||
# Project
|
||||
from hyperglass.settings import Settings
|
||||
|
||||
if not value.is_relative_to(Settings.app_path):
|
||||
raise ValueError("{!s} must be relative to {!s}".format(value, Settings.app_path))
|
||||
|
||||
if Settings.container:
|
||||
value = Settings.default_app_path.joinpath(
|
||||
*(p for p in value.parts if p not in Settings.app_path.parts)
|
||||
)
|
||||
print(f"{value=}")
|
||||
return value
|
||||
|
||||
def __repr__(self):
|
||||
"""Stringify custom field representation."""
|
||||
return f"ConfigPathItem({super().__repr__()})"
|
||||
|
|
|
|||
|
|
@ -53,6 +53,39 @@ class HyperglassModel(BaseModel):
|
|||
)
|
||||
return snake_to_camel(snake_field)
|
||||
|
||||
def convert_paths(self, value: t.Any):
|
||||
"""Change path to relative to app_path."""
|
||||
# Project
|
||||
from hyperglass.settings import Settings
|
||||
|
||||
if isinstance(value, Path):
|
||||
if Settings.container:
|
||||
return str(
|
||||
Settings.default_app_path.joinpath(
|
||||
*(p for p in value.parts if p not in Settings.app_path.parts)
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(value, str):
|
||||
path = Path(value)
|
||||
if path.exists() and Settings.container:
|
||||
# if path.exists():
|
||||
return str(
|
||||
Settings.default_app_path.joinpath(
|
||||
*(p for p in path.parts if p not in Settings.app_path.parts)
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(value, t.Tuple):
|
||||
return tuple(self.convert_paths(v) for v in value)
|
||||
if isinstance(value, t.List):
|
||||
return [self.convert_paths(v) for v in value]
|
||||
if isinstance(value, t.Generator):
|
||||
return (self.convert_paths(v) for v in value)
|
||||
if isinstance(value, t.Dict):
|
||||
return {k: self.convert_paths(v) for k, v in value.items()}
|
||||
return value
|
||||
|
||||
def _repr_from_attrs(self, attrs: Series[str]) -> str:
|
||||
"""Alias to `hyperglass.util:repr_from_attrs` in the context of this model."""
|
||||
return repr_from_attrs(self, attrs)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ if t.TYPE_CHECKING:
|
|||
|
||||
ListenHost = t.Union[None, IPvAnyAddress, t.Literal["localhost"]]
|
||||
|
||||
_default_app_path = Path("/etc/hyperglass")
|
||||
|
||||
|
||||
class HyperglassSettings(BaseSettings):
|
||||
"""hyperglass system settings, required to start hyperglass."""
|
||||
|
|
@ -33,12 +35,15 @@ class HyperglassSettings(BaseSettings):
|
|||
"""hyperglass system settings configuration."""
|
||||
|
||||
env_prefix = "hyperglass_"
|
||||
underscore_attrs_are_private = True
|
||||
|
||||
config_file_names: t.ClassVar[t.Tuple[str, ...]] = ("config", "devices", "directives")
|
||||
default_app_path: t.ClassVar[Path] = _default_app_path
|
||||
_original_app_path: DirectoryPath = _default_app_path
|
||||
|
||||
debug: bool = False
|
||||
dev_mode: bool = False
|
||||
app_path: DirectoryPath = "/etc/hyperglass"
|
||||
app_path: DirectoryPath = _default_app_path
|
||||
redis_host: str = "localhost"
|
||||
redis_password: t.Optional[SecretStr]
|
||||
redis_db: int = 1
|
||||
|
|
@ -46,6 +51,14 @@ class HyperglassSettings(BaseSettings):
|
|||
host: IPvAnyAddress = None
|
||||
port: int = 8001
|
||||
ca_cert: t.Optional[FilePath]
|
||||
container: bool = False
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
"""Create hyperglass Settings instance."""
|
||||
super().__init__(**kwargs)
|
||||
if self.container:
|
||||
self.app_path = self.default_app_path
|
||||
print(self)
|
||||
|
||||
def __rich_console__(self, console: "Console", options: "ConsoleOptions") -> "RenderResult":
|
||||
"""Render a Rich table representation of hyperglass settings."""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Data models used throughout hyperglass."""
|
||||
|
||||
# Standard Library
|
||||
from typing import Optional
|
||||
import typing as t
|
||||
from datetime import datetime
|
||||
|
||||
# Third Party
|
||||
|
|
@ -20,12 +20,12 @@ _ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon
|
|||
class WebhookHeaders(HyperglassModel):
|
||||
"""Webhook data model."""
|
||||
|
||||
user_agent: Optional[StrictStr]
|
||||
referer: Optional[StrictStr]
|
||||
accept_encoding: Optional[StrictStr]
|
||||
accept_language: Optional[StrictStr]
|
||||
x_real_ip: Optional[StrictStr]
|
||||
x_forwarded_for: Optional[StrictStr]
|
||||
user_agent: t.Optional[StrictStr]
|
||||
referer: t.Optional[StrictStr]
|
||||
accept_encoding: t.Optional[StrictStr]
|
||||
accept_language: t.Optional[StrictStr]
|
||||
x_real_ip: t.Optional[StrictStr]
|
||||
x_forwarded_for: t.Optional[StrictStr]
|
||||
|
||||
class Config:
|
||||
"""Pydantic model config."""
|
||||
|
|
@ -51,9 +51,9 @@ class WebhookNetwork(HyperglassModel, extra="allow"):
|
|||
class Webhook(HyperglassModel):
|
||||
"""Webhook data model."""
|
||||
|
||||
query_location: StrictStr
|
||||
query_type: StrictStr
|
||||
query_target: StrictStr
|
||||
query_location: str
|
||||
query_type: str
|
||||
query_target: t.Union[t.List[str], str]
|
||||
headers: WebhookHeaders
|
||||
source: StrictStr = "Unknown"
|
||||
network: WebhookNetwork
|
||||
|
|
@ -69,7 +69,7 @@ class Webhook(HyperglassModel):
|
|||
def msteams(self):
|
||||
"""Format the webhook data as a Microsoft Teams card."""
|
||||
|
||||
def code(value):
|
||||
def code(value: t.Any):
|
||||
"""Wrap argument in backticks for markdown inline code formatting."""
|
||||
return f"`{str(value)}`"
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class RedisManager:
|
|||
name = self.key(key)
|
||||
value: t.Optional[bytes] = self.instance.get(name)
|
||||
if isinstance(value, bytes):
|
||||
return pickle.loads(value)
|
||||
return pickle.loads(value) # noqa
|
||||
if raise_if_none is True:
|
||||
raise StateError("'{key}' ('{name}') does not exist in Redis store", key=key, name=name)
|
||||
if value_if_none is not None:
|
||||
|
|
@ -121,7 +121,7 @@ class RedisManager:
|
|||
value = self.instance.hgetall(name)
|
||||
|
||||
if isinstance(value, bytes):
|
||||
return pickle.loads(value)
|
||||
return pickle.loads(value) # noqa
|
||||
return None
|
||||
|
||||
def set_map_item(self, key: str, item: str, value: t.Any) -> None:
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { useOpposingColor, useColorMode, useColorValue, useBreakpointValue } fro
|
|||
|
||||
import type { ButtonProps } from '@chakra-ui/react';
|
||||
|
||||
interface ColorModeToggleProps extends ButtonProps {
|
||||
size?: string;
|
||||
interface ColorModeToggleProps extends Omit<ButtonProps, 'size'> {
|
||||
size?: string | number;
|
||||
}
|
||||
|
||||
export const ColorModeToggle = forwardRef<HTMLButtonElement, ColorModeToggleProps>(
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export const Footer = (): JSX.Element => {
|
|||
whiteSpace="nowrap"
|
||||
color={footerColor}
|
||||
spacing={{ base: 8, lg: 6 }}
|
||||
d={{ base: 'inline-block', lg: 'flex' }}
|
||||
display={{ base: 'inline-block', lg: 'flex' }}
|
||||
overflowY={{ base: 'auto', lg: 'unset' }}
|
||||
justifyContent={{ base: 'center', lg: 'space-between' }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
|
|||
}
|
||||
}
|
||||
|
||||
const bg = useColorValue('white', 'blackSolid.800');
|
||||
const bg = useColorValue('white', 'blackSolid.600');
|
||||
const imageBorder = useColorValue('gray.600', 'whiteAlpha.800');
|
||||
const fg = useOpposingColor(bg);
|
||||
const checkedBorder = useColorValue('blue.400', 'blue.300');
|
||||
|
|
@ -74,6 +74,7 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
|
|||
handleChange(option);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<chakra.h2
|
||||
color={fg}
|
||||
|
|
@ -85,8 +86,6 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
|
|||
</chakra.h2>
|
||||
<Avatar
|
||||
color={fg}
|
||||
fit="cover"
|
||||
alt={label}
|
||||
name={label}
|
||||
boxSize={12}
|
||||
rounded="full"
|
||||
|
|
@ -103,6 +102,7 @@ export const LocationCard = (props: LocationCardProps): JSX.Element => {
|
|||
{option.data.description as string}
|
||||
</chakra.p>
|
||||
)}
|
||||
</>
|
||||
</LocationCardWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useConfig } from '~/context';
|
||||
import { useTheme } from '~/hooks';
|
||||
import { googleFontUrl } from '~/util';
|
||||
|
||||
export const Meta = (): JSX.Element => {
|
||||
const config = useConfig();
|
||||
const { fonts } = useTheme();
|
||||
const [location, setLocation] = useState('/');
|
||||
|
||||
const {
|
||||
|
|
@ -15,8 +12,6 @@ export const Meta = (): JSX.Element => {
|
|||
} = useConfig();
|
||||
|
||||
const siteName = `${title} - ${description}`;
|
||||
const primaryFont = useMemo(() => googleFontUrl(fonts.body), [fonts.body]);
|
||||
const monoFont = useMemo(() => googleFontUrl(fonts.mono), [fonts.mono]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && location === '/') {
|
||||
|
|
@ -30,12 +25,14 @@ export const Meta = (): JSX.Element => {
|
|||
<meta name="url" content={location} />
|
||||
<meta name="og:title" content={title} />
|
||||
<meta name="og:url" content={location} />
|
||||
<link href={monoFont} rel="stylesheet" />
|
||||
<link href={primaryFont} rel="stylesheet" />
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:image:alt" content={siteName} />
|
||||
<meta name="og:description" content={description} />
|
||||
<meta name="hyperglass-version" content={config.version} />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { forwardRef } from 'react';
|
||||
import { Text, Box, Tooltip, Menu, MenuButton, MenuList, Link } from '@chakra-ui/react';
|
||||
import { Text, Box, Tooltip, Menu, MenuButton, MenuList, Link, Flex } from '@chakra-ui/react';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
|
||||
import utcPlugin from 'dayjs/plugin/utc';
|
||||
|
|
@ -123,6 +123,7 @@ export const ASPath = (props: ASPathProps): JSX.Element => {
|
|||
color={color[+active]}
|
||||
boxSize={5}
|
||||
px={2}
|
||||
display="inline-flex"
|
||||
/>,
|
||||
);
|
||||
paths.push(
|
||||
|
|
@ -132,7 +133,7 @@ export const ASPath = (props: ASPathProps): JSX.Element => {
|
|||
);
|
||||
});
|
||||
|
||||
return <>{paths}</>;
|
||||
return <Flex>{paths}</Flex>;
|
||||
};
|
||||
|
||||
export const Communities = (props: CommunitiesProps): JSX.Element => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Box, Flex, SkeletonText, Badge, VStack } from '@chakra-ui/react';
|
||||
import ReactFlow, { Background, ReactFlowProvider, Handle, Position } from 'react-flow-renderer';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
ReactFlowProvider,
|
||||
Handle,
|
||||
Position,
|
||||
isNode,
|
||||
isEdge,
|
||||
} from 'react-flow-renderer';
|
||||
import { useConfig } from '~/context';
|
||||
import { useASNDetail, useColorValue, useColorToken } from '~/hooks';
|
||||
import { Controls } from './controls';
|
||||
|
|
@ -15,7 +23,7 @@ interface NodeProps<D extends unknown> extends Omit<ReactFlowNodeProps, 'data'>
|
|||
data: D;
|
||||
}
|
||||
|
||||
interface NodeData {
|
||||
export interface NodeData {
|
||||
asn: string;
|
||||
name: string;
|
||||
hasChildren: boolean;
|
||||
|
|
@ -30,14 +38,18 @@ export const Chart = (props: ChartProps): JSX.Element => {
|
|||
|
||||
const elements = useElements({ asn: primaryAsn, name: orgName }, data);
|
||||
|
||||
const nodes = useMemo(() => elements.filter(isNode), [elements]);
|
||||
const edges = useMemo(() => elements.filter(isEdge), [elements]);
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<Box w="100%" h={{ base: '100vh', lg: '70vh' }} zIndex={1}>
|
||||
<ReactFlow
|
||||
snapToGrid
|
||||
elements={elements}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={{ ASNode }}
|
||||
onLoad={inst => setTimeout(() => inst.fitView(), 0)}
|
||||
onInit={inst => setTimeout(() => inst.fitView(), 0)}
|
||||
>
|
||||
<Background color={dots} />
|
||||
<Controls />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { ButtonGroup, IconButton } from '@chakra-ui/react';
|
||||
import { useZoomPanHelper } from 'react-flow-renderer';
|
||||
import { useReactFlow } from 'react-flow-renderer';
|
||||
import { DynamicIcon } from '~/elements';
|
||||
|
||||
export const Controls = (): JSX.Element => {
|
||||
const { fitView, zoomIn, zoomOut } = useZoomPanHelper();
|
||||
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
||||
return (
|
||||
<ButtonGroup
|
||||
m={4}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,20 @@ import dagre from 'dagre';
|
|||
import { useMemo } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
import type { FlowElement } from 'react-flow-renderer';
|
||||
import type { Node, Edge } from 'react-flow-renderer';
|
||||
import type { NodeData } from './chart';
|
||||
|
||||
interface BasePath {
|
||||
asn: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type FlowElement<T> = Node<T> | Edge<T>;
|
||||
|
||||
const NODE_WIDTH = 200;
|
||||
const NODE_HEIGHT = 48;
|
||||
|
||||
export function useElements(base: BasePath, data: StructuredResponse): FlowElement[] {
|
||||
export function useElements(base: BasePath, data: StructuredResponse): FlowElement<NodeData>[] {
|
||||
return useMemo(() => {
|
||||
return [...buildElements(base, data)];
|
||||
}, [base, data]);
|
||||
|
|
@ -22,7 +25,10 @@ export function useElements(base: BasePath, data: StructuredResponse): FlowEleme
|
|||
* Calculate the positions for each AS Path.
|
||||
* @see https://github.com/MrBlenny/react-flow-chart/issues/61
|
||||
*/
|
||||
function* buildElements(base: BasePath, data: StructuredResponse): Generator<FlowElement> {
|
||||
function* buildElements(
|
||||
base: BasePath,
|
||||
data: StructuredResponse,
|
||||
): Generator<FlowElement<NodeData>> {
|
||||
const { routes } = data;
|
||||
// Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths.
|
||||
const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]);
|
||||
|
|
@ -107,7 +113,7 @@ function* buildElements(base: BasePath, data: StructuredResponse): Generator<Flo
|
|||
type: 'ASNode',
|
||||
position: { x, y },
|
||||
data: {
|
||||
asn,
|
||||
asn: `${asn}`,
|
||||
name: `AS${asn}`,
|
||||
hasChildren: idx < endIdx,
|
||||
hasParents: true,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Button, Tooltip } from '@chakra-ui/react';
|
|||
import { DynamicIcon } from '~/elements';
|
||||
|
||||
import type { ButtonProps } from '@chakra-ui/react';
|
||||
import type { UseQueryResult } from 'react-query';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
|
||||
interface RequeryButtonProps extends ButtonProps {
|
||||
requery: Get<UseQueryResult<QueryResponse>, 'refetch'>;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,13 @@ export const Option = <Opt extends SingleOption, IsMulti extends boolean>(
|
|||
const tags = Array.isArray(data.tags) ? (data.tags as string[]) : [];
|
||||
return (
|
||||
<components.Option<Opt, IsMulti, GroupBase<Opt>> {...props}>
|
||||
<chakra.span d={{ base: 'block', lg: 'inline' }}>{label}</chakra.span>
|
||||
<chakra.span display={{ base: 'block', lg: 'inline' }}>{label}</chakra.span>
|
||||
{tags.length > 0 && (
|
||||
<HStack d={{ base: 'flex', lg: 'inline-flex' }} ms={{ base: 0, lg: 2 }} alignItems="center">
|
||||
<HStack
|
||||
alignItems="center"
|
||||
ms={{ base: 0, lg: 2 }}
|
||||
display={{ base: 'flex', lg: 'inline-flex' }}
|
||||
>
|
||||
{tags.map(tag => (
|
||||
<Badge fontSize="xs" variant="subtle" key={tag} colorScheme="gray" textTransform="none">
|
||||
{tag}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ export const Select = forwardRef(
|
|||
isMulti={isMulti}
|
||||
theme={rsTheme}
|
||||
components={{ Option, ...components }}
|
||||
menuPortalTarget={document?.body ?? undefined}
|
||||
ref={ref}
|
||||
styles={{
|
||||
menu,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useToken } from '@chakra-ui/react';
|
||||
import { mergeWith } from '@chakra-ui/utils';
|
||||
import { merge } from 'merge-anything';
|
||||
import {
|
||||
useMobile,
|
||||
useColorValue,
|
||||
|
|
@ -150,7 +149,6 @@ export const useIndicatorSeparatorStyle = <Opt extends SingleOption, IsMulti ext
|
|||
): RSStyleFunction<'indicatorSeparator', Opt, IsMulti> => {
|
||||
const { colorMode } = props;
|
||||
const backgroundColor = useColorToken('colors', 'gray.200', 'whiteAlpha.300');
|
||||
// const backgroundColor = useColorToken('colors', 'gray.200', 'gray.600');
|
||||
const styles = { backgroundColor };
|
||||
|
||||
return useCallback(base => mergeWith({}, base, styles), [colorMode]);
|
||||
|
|
@ -220,7 +218,7 @@ export const useMultiValueRemoveStyle = <Opt extends SingleOption, IsMulti exten
|
|||
};
|
||||
|
||||
export const useRSTheme = (): RSThemeFunction => {
|
||||
const borderRadius = useToken('radii', 'md');
|
||||
const borderRadius = useToken('radii', 'md') as unknown as number;
|
||||
|
||||
return useCallback((t: ReactSelect.Theme): ReactSelect.Theme => ({ ...t, borderRadius }), []);
|
||||
};
|
||||
|
|
@ -232,8 +230,8 @@ export const useMenuPortal = <Opt extends SingleOption, IsMulti extends boolean>
|
|||
> => {
|
||||
const isMobile = useMobile();
|
||||
const styles = {
|
||||
zIndex: isMobile ? 1500 : 1,
|
||||
zIndex: 1500,
|
||||
};
|
||||
|
||||
return useCallback(base => merge(base, styles), [isMobile]);
|
||||
return useCallback(base => mergeWith({}, base, styles), [isMobile]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import * as ReactSelect from 'react-select';
|
||||
|
||||
import type { StylesProps, StylesConfigFunction } from 'react-select/dist/declarations/src/styles';
|
||||
import type { CSSObjectWithLabel } from 'react-select';
|
||||
import type { StylesProps } from 'react-select/dist/declarations/src/styles';
|
||||
import type { Theme, SingleOption } from '~/types';
|
||||
|
||||
type StylesConfigFunction<Props> = (base: CSSObjectWithLabel, props: Props) => CSSObjectWithLabel;
|
||||
|
||||
export type SelectOnChange<
|
||||
Opt extends SingleOption = SingleOption,
|
||||
IsMulti extends boolean = boolean,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export const Table = (props: TableProps): JSX.Element => {
|
|||
{...column.getSortByToggleProps()}
|
||||
>
|
||||
<Text fontSize="sm" fontWeight="bold" display="inline-block">
|
||||
{column.render('Header')}
|
||||
{column.render('Header') as React.ReactNode}
|
||||
</Text>
|
||||
<If condition={column.isSorted}>
|
||||
<Then>
|
||||
|
|
@ -142,7 +142,7 @@ export const Table = (props: TableProps): JSX.Element => {
|
|||
{typeof Cell !== 'undefined' ? (
|
||||
<Cell column={column} row={row} value={value} />
|
||||
) : (
|
||||
cell.render('Cell')
|
||||
(cell.render('Cell') as React.ReactNode)
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { makeTheme, defaultTheme } from '~/util';
|
||||
import { ChakraProvider, localStorageManager } from '@chakra-ui/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { makeTheme } from '~/util';
|
||||
|
||||
import type { Config } from '~/types';
|
||||
|
||||
|
|
@ -16,15 +16,15 @@ const queryClient = new QueryClient();
|
|||
|
||||
export const HyperglassProvider = (props: HyperglassProviderProps): JSX.Element => {
|
||||
const { config, children } = props;
|
||||
const value = useMemo(() => config, [config]);
|
||||
const userTheme = value && makeTheme(value.web.theme, value.web.theme.defaultColorMode);
|
||||
const theme = value ? userTheme : defaultTheme;
|
||||
const value = useMemo(() => config, []);
|
||||
const theme = useMemo(() => makeTheme(value.web.theme, value.web.theme.defaultColorMode), []);
|
||||
|
||||
return (
|
||||
<ChakraProvider theme={theme}>
|
||||
<HyperglassContext.Provider value={value}>
|
||||
<ChakraProvider theme={theme} colorModeManager={localStorageManager} resetCSS>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</HyperglassContext.Provider>
|
||||
</ChakraProvider>
|
||||
</HyperglassContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { QueryFunctionContext, QueryObserverResult, QueryFunction } from 'react-query';
|
||||
import type {
|
||||
QueryFunctionContext,
|
||||
QueryObserverResult,
|
||||
QueryFunction,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
interface ASNQuery {
|
||||
data: {
|
||||
|
|
@ -12,7 +16,7 @@ interface ASNQuery {
|
|||
};
|
||||
}
|
||||
|
||||
const query: QueryFunction<ASNQuery, string> = async (ctx: QueryFunctionContext) => {
|
||||
const query: QueryFunction<ASNQuery, string[]> = async (ctx: QueryFunctionContext) => {
|
||||
const asn = ctx.queryKey;
|
||||
const res = await fetch('https://api.asrank.caida.org/v2/graphql', {
|
||||
mode: 'cors',
|
||||
|
|
@ -29,8 +33,8 @@ const query: QueryFunction<ASNQuery, string> = async (ctx: QueryFunctionContext)
|
|||
* @see https://api.asrank.caida.org/v2/docs
|
||||
*/
|
||||
export function useASNDetail(asn: string): QueryObserverResult<ASNQuery> {
|
||||
return useQuery<ASNQuery, unknown, ASNQuery, string>({
|
||||
queryKey: asn,
|
||||
return useQuery<ASNQuery, unknown, ASNQuery, string[]>({
|
||||
queryKey: [asn],
|
||||
queryFn: query,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchInterval: false,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'isomorphic-fetch';
|
||||
import '@testing-library/jest-dom';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
|
||||
import { HyperglassContext } from '~/context';
|
||||
import { useDNSQuery } from './use-dns-query';
|
||||
|
||||
|
|
@ -41,6 +41,7 @@ describe('useDNSQuery Cloudflare', () => {
|
|||
const { result, waitFor } = renderHook(() => useDNSQuery('one.one.one.one', 4), {
|
||||
wrapper: CloudflareWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => result.current.isSuccess, { timeout: 5_000 });
|
||||
expect(result.current.data?.Answer.map(a => a.data)).toContain('1.1.1.1');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useConfig } from '~/context';
|
||||
import { fetchWithTimeout } from '~/util';
|
||||
|
||||
import type { QueryFunction, QueryFunctionContext, QueryObserverResult } from 'react-query';
|
||||
import type {
|
||||
QueryFunction,
|
||||
QueryFunctionContext,
|
||||
QueryObserverResult,
|
||||
} from '@tanstack/react-query';
|
||||
import type { DnsOverHttps } from '~/types';
|
||||
|
||||
type DNSQueryKey = [string, { target: string | null; family: 4 | 6 }];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getHyperglassConfig } from '~/util';
|
||||
|
||||
import type { UseQueryResult } from 'react-query';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import type { ConfigLoadError } from '~/util';
|
||||
import type { Config } from '~/types';
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export function useHyperglassConfig(): UseHyperglassConfig {
|
|||
const [initFailed, setInitFailed] = useState<boolean>(false);
|
||||
|
||||
const query = useQuery<Config, ConfigLoadError>({
|
||||
queryKey: 'hyperglass-ui-config',
|
||||
queryKey: ['hyperglass-ui-config'],
|
||||
queryFn: getHyperglassConfig,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchInterval: 10000,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useConfig } from '~/context';
|
||||
import { fetchWithTimeout } from '~/util';
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ import type {
|
|||
UseQueryOptions,
|
||||
QueryObserverResult,
|
||||
QueryFunctionContext,
|
||||
} from 'react-query';
|
||||
} from '@tanstack/react-query';
|
||||
import type { FormQuery } from '~/types';
|
||||
|
||||
type LGQueryKey = [string, FormQuery];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchWithTimeout } from '~/util';
|
||||
|
||||
import type {
|
||||
|
|
@ -6,7 +6,7 @@ import type {
|
|||
UseQueryResult,
|
||||
UseQueryOptions,
|
||||
QueryFunctionContext,
|
||||
} from 'react-query';
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
interface WtfIndividual {
|
||||
ip: string;
|
||||
|
|
@ -44,7 +44,9 @@ function transform(wtf: WtfIsMyIP): WtfIndividual {
|
|||
};
|
||||
}
|
||||
|
||||
const query: QueryFunction<WtfIndividual, string> = async (ctx: QueryFunctionContext<string>) => {
|
||||
const query: QueryFunction<WtfIndividual, string[]> = async (
|
||||
ctx: QueryFunctionContext<string[]>,
|
||||
) => {
|
||||
const controller = new AbortController();
|
||||
const [url] = ctx.queryKey;
|
||||
|
||||
|
|
@ -61,7 +63,7 @@ const query: QueryFunction<WtfIndividual, string> = async (ctx: QueryFunctionCon
|
|||
return transform(data);
|
||||
};
|
||||
|
||||
const common: UseQueryOptions<WtfIndividual, unknown, WtfIndividual, string> = {
|
||||
const common: UseQueryOptions<WtfIndividual, unknown, WtfIndividual, string[]> = {
|
||||
queryFn: query,
|
||||
enabled: false,
|
||||
refetchInterval: false,
|
||||
|
|
@ -72,12 +74,12 @@ const common: UseQueryOptions<WtfIndividual, unknown, WtfIndividual, string> = {
|
|||
};
|
||||
|
||||
export function useWtf(): Wtf {
|
||||
const ipv4 = useQuery<WtfIndividual, unknown, WtfIndividual, string>({
|
||||
queryKey: URL_IP4,
|
||||
const ipv4 = useQuery<WtfIndividual, unknown, WtfIndividual, string[]>({
|
||||
queryKey: [URL_IP4],
|
||||
...common,
|
||||
});
|
||||
const ipv6 = useQuery<WtfIndividual, unknown, WtfIndividual, string>({
|
||||
queryKey: URL_IP6,
|
||||
const ipv6 = useQuery<WtfIndividual, unknown, WtfIndividual, string[]>({
|
||||
queryKey: [URL_IP6],
|
||||
...common,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,8 @@ const nextConfig = {
|
|||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
// See: https://github.com/react-hook-form/resolvers/issues/271#issuecomment-986618265
|
||||
// See: https://github.com/vercel/next.js/issues/30750#issuecomment-962198711
|
||||
esmExternals: false,
|
||||
},
|
||||
swcMinify: true,
|
||||
productionBrowserSourceMaps: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
|
|
|||
50
hyperglass/ui/package.json
vendored
50
hyperglass/ui/package.json
vendored
|
|
@ -17,31 +17,31 @@
|
|||
},
|
||||
"browserslist": "> 0.25%, not dead",
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.7.2",
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@hookform/devtools": "^4.0.1",
|
||||
"@hookform/resolvers": "^2.8.4",
|
||||
"@chakra-ui/react": "^2.5.5",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@hookform/devtools": "^4.3.0",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@tanstack/react-query": "^4.22.0",
|
||||
"dagre": "^0.8.5",
|
||||
"dayjs": "^1.10.4",
|
||||
"framer-motion": "^5.4.1",
|
||||
"framer-motion": "^10.11.6",
|
||||
"lodash": "^4.17.21",
|
||||
"merge-anything": "^4.0.1",
|
||||
"next": "12",
|
||||
"palette-by-numbers": "^0.1.5",
|
||||
"next": "12.3.4",
|
||||
"palette-by-numbers": "^0.1.6",
|
||||
"plur": "^4.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-countdown": "^2.2.1",
|
||||
"react-device-detect": "^1.15.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-flow-renderer": "^9.6.0",
|
||||
"react-hook-form": "^7.21.0",
|
||||
"react-flow-renderer": "^10.3.17",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-if": "^4.1.1",
|
||||
"react-if": "^4.1.4",
|
||||
"react-markdown": "^5.0.3",
|
||||
"react-query": "^3.16.0",
|
||||
"react-select": "^5.2.1",
|
||||
"react-select": "^5.7.0",
|
||||
"react-string-replace": "^v0.5.0",
|
||||
"react-table": "^7.7.0",
|
||||
"remark-gfm": "^1.0.0",
|
||||
|
|
@ -49,20 +49,16 @@
|
|||
"vest": "^3.2.8",
|
||||
"zustand": "^3.6.6"
|
||||
},
|
||||
"resolutions": {
|
||||
"react-select/@emotion/react": "^11.7.0",
|
||||
"@hookform/devtools/@emotion/react": "^11.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/dagre": "^0.7.44",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/lodash": "^4.14.177",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/react": "^18.0.35",
|
||||
"@types/react-table": "^7.7.1",
|
||||
"@types/string-format": "^2.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.31.0",
|
||||
|
|
@ -85,8 +81,8 @@
|
|||
"jest": "^27.2.1",
|
||||
"prettier": "^2.3.2",
|
||||
"prettier-eslint": "^13.0.0",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"type-fest": "^2.3.2",
|
||||
"typescript": "^4.4.2"
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"type-fest": "^3.8.0",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Switch, Case, Default } from 'react-if';
|
||||
import { Meta, Layout } from '~/components';
|
||||
import { HyperglassProvider } from '~/context';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import fs from 'fs';
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
import { ColorModeScript } from '@chakra-ui/react';
|
||||
import { CustomJavascript, CustomHtml, Favicon } from '~/elements';
|
||||
import { getHyperglassConfig, googleFontUrl } from '~/util';
|
||||
import favicons from '../favicon-formats';
|
||||
|
||||
import type { DocumentContext, DocumentInitialProps } from 'next/document';
|
||||
import type { ThemeConfig } from '~/types';
|
||||
|
||||
interface DocumentExtra extends DocumentInitialProps {
|
||||
interface DocumentExtra
|
||||
extends DocumentInitialProps,
|
||||
Pick<ThemeConfig, 'defaultColorMode' | 'fonts'> {
|
||||
customJs: string;
|
||||
customHtml: string;
|
||||
}
|
||||
|
|
@ -15,13 +20,31 @@ class MyDocument extends Document<DocumentExtra> {
|
|||
const initialProps = await Document.getInitialProps(ctx);
|
||||
let customJs = '',
|
||||
customHtml = '';
|
||||
|
||||
if (fs.existsSync('custom.js')) {
|
||||
customJs = fs.readFileSync('custom.js').toString();
|
||||
}
|
||||
if (fs.existsSync('custom.html')) {
|
||||
customHtml = fs.readFileSync('custom.html').toString();
|
||||
}
|
||||
return { customJs, customHtml, ...initialProps };
|
||||
|
||||
let fonts = { body: '', mono: '' };
|
||||
let defaultColorMode: 'light' | 'dark' | null = null;
|
||||
|
||||
const hyperglassUrl = process.env.HYPERGLASS_URL ?? '';
|
||||
const {
|
||||
web: {
|
||||
theme: { fonts: themeFonts, defaultColorMode: themeDefaultColorMode },
|
||||
},
|
||||
} = await getHyperglassConfig(hyperglassUrl);
|
||||
|
||||
fonts = {
|
||||
body: googleFontUrl(themeFonts.body),
|
||||
mono: googleFontUrl(themeFonts.mono),
|
||||
};
|
||||
defaultColorMode = themeDefaultColorMode;
|
||||
|
||||
return { customJs, customHtml, fonts, defaultColorMode, ...initialProps };
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
|
|
@ -35,19 +58,18 @@ class MyDocument extends Document<DocumentExtra> {
|
|||
<meta name="og:image" content="/images/opengraph.jpg" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="true" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
||||
<link href={this.props.fonts.mono} rel="stylesheet" />
|
||||
<link href={this.props.fonts.body} rel="stylesheet" />
|
||||
{favicons.map((favicon, idx) => (
|
||||
<Favicon key={idx} {...favicon} />
|
||||
))}
|
||||
<CustomJavascript>{this.props.customJs}</CustomJavascript>
|
||||
</Head>
|
||||
<body>
|
||||
<ColorModeScript initialColorMode={this.props.defaultColorMode ?? 'system'} />
|
||||
<Main />
|
||||
<CustomHtml>{this.props.customHtml}</CustomHtml>
|
||||
<NextScript />
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import dynamic from 'next/dynamic';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { If, Then, Else } from 'react-if';
|
||||
import { Loading } from '~/elements';
|
||||
import { useView } from '~/hooks';
|
||||
|
||||
import type { NextPage } from 'next';
|
||||
import type { AnimatePresenceProps } from 'framer-motion';
|
||||
|
||||
const AnimatePresence = dynamic<AnimatePresenceProps>(() =>
|
||||
import('framer-motion').then(i => i.AnimatePresence),
|
||||
const LookingGlassForm = dynamic<Dict>(
|
||||
() => import('~/components/looking-glass-form').then(i => i.LookingGlassForm),
|
||||
{
|
||||
loading: Loading,
|
||||
},
|
||||
);
|
||||
|
||||
const LookingGlassForm = dynamic<Dict>(() => import('~/components').then(i => i.LookingGlassForm), {
|
||||
loading: Loading,
|
||||
});
|
||||
|
||||
const Results = dynamic<Dict>(() => import('~/components').then(i => i.Results), {
|
||||
const Results = dynamic<Dict>(() => import('~/components/results').then(i => i.Results), {
|
||||
loading: Loading,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES5",
|
||||
"module": "esnext",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"downlevelIteration": true,
|
||||
"strict": true,
|
||||
"baseUrl": "." /* Base directory to resolve non-absolute module names. */,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export function chunkArray<A extends unknown>(array: A[], size: number): A[][] {
|
|||
*/
|
||||
export function entries<O, K extends keyof O = keyof O>(obj: O): [K, O[K]][] {
|
||||
const _entries = [] as [K, O[K]][];
|
||||
const keys = Object.keys(obj) as K[];
|
||||
const keys = Object.keys(obj as Record<string, unknown>) as K[];
|
||||
for (const key of keys) {
|
||||
_entries.push([key, obj[key]]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { QueryFunctionContext } from '@tanstack/react-query';
|
||||
import { isObject } from '~/types';
|
||||
|
||||
import type { Config } from '~/types';
|
||||
|
|
@ -23,9 +24,15 @@ export class ConfigLoadError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getHyperglassConfig(): Promise<Config> {
|
||||
export async function getHyperglassConfig(url?: QueryFunctionContext | string): Promise<Config> {
|
||||
let mode: RequestInit['mode'];
|
||||
|
||||
let fetchUrl = '/ui/props/';
|
||||
|
||||
if (typeof url === 'string') {
|
||||
fetchUrl = url.replace(/(^\/)|(\/$)/g, '') + '/ui/props/';
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
mode = 'same-origin';
|
||||
} else if (process.env.NODE_ENV === 'development') {
|
||||
|
|
@ -34,7 +41,7 @@ export async function getHyperglassConfig(): Promise<Config> {
|
|||
const options: RequestInit = { method: 'GET', mode, headers: { 'user-agent': 'hyperglass-ui' } };
|
||||
|
||||
try {
|
||||
const response = await fetch('/ui/props/', options);
|
||||
const response = await fetch(fetchUrl, options);
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { extendTheme } from '@chakra-ui/react';
|
||||
import { mode } from '@chakra-ui/theme-tools';
|
||||
import { generateFontFamily, generatePalette } from 'palette-by-numbers';
|
||||
|
||||
import type { ChakraTheme } from '@chakra-ui/react';
|
||||
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
|
||||
import type { ThemeConfig, Theme } from '~/types';
|
||||
|
||||
function importFonts(userFonts: Theme.Fonts): ChakraTheme['fonts'] {
|
||||
|
|
@ -17,12 +15,8 @@ function importFonts(userFonts: Theme.Fonts): ChakraTheme['fonts'] {
|
|||
}
|
||||
|
||||
function importColors(userColors: ThemeConfig['colors']): Theme.Colors {
|
||||
const generatedColors = {} as Theme.Colors;
|
||||
for (const [k, v] of Object.entries<string>(userColors)) {
|
||||
generatedColors[k] = generatePalette(v);
|
||||
}
|
||||
|
||||
generatedColors.blackSolid = {
|
||||
const initial: Pick<Theme.Colors, 'blackSolid' | 'whiteSolid' | 'transparent' | 'current'> = {
|
||||
blackSolid: {
|
||||
50: '#444444',
|
||||
100: '#3c3c3c',
|
||||
200: '#353535',
|
||||
|
|
@ -33,8 +27,8 @@ function importColors(userColors: ThemeConfig['colors']): Theme.Colors {
|
|||
700: '#0f0f0f',
|
||||
800: '#080808',
|
||||
900: '#000000',
|
||||
};
|
||||
generatedColors.whiteSolid = {
|
||||
},
|
||||
whiteSolid: {
|
||||
50: '#ffffff',
|
||||
100: '#f7f7f7',
|
||||
200: '#f0f0f0',
|
||||
|
|
@ -45,13 +39,20 @@ function importColors(userColors: ThemeConfig['colors']): Theme.Colors {
|
|||
700: '#cacaca',
|
||||
800: '#c3c3c3',
|
||||
900: '#bbbbbb',
|
||||
};
|
||||
|
||||
return {
|
||||
...generatedColors,
|
||||
},
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
};
|
||||
|
||||
const generatedColors = Object.entries<string>(userColors).reduce<Theme.Colors>(
|
||||
(final, [k, v]) => {
|
||||
final[k] = generatePalette(v);
|
||||
return final;
|
||||
},
|
||||
initial,
|
||||
);
|
||||
|
||||
return generatedColors;
|
||||
}
|
||||
|
||||
export function makeTheme(
|
||||
|
|
@ -86,24 +87,34 @@ export function makeTheme(
|
|||
break;
|
||||
}
|
||||
|
||||
const defaultTheme = extendTheme({
|
||||
return extendTheme({
|
||||
fonts,
|
||||
colors,
|
||||
config,
|
||||
fontWeights,
|
||||
semanticTokens: {
|
||||
colors: {
|
||||
'body-bg': {
|
||||
default: 'light.500',
|
||||
_dark: 'dark.500',
|
||||
},
|
||||
'body-fg': {
|
||||
default: 'dark.500',
|
||||
_dark: 'light.500',
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
global: (props: StyleFunctionProps) => ({
|
||||
global: {
|
||||
html: { scrollBehavior: 'smooth', height: '-webkit-fill-available' },
|
||||
body: {
|
||||
background: mode('light.500', 'dark.500')(props),
|
||||
color: mode('black', 'white')(props),
|
||||
background: 'body-bg',
|
||||
color: 'body-fg',
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}) as Theme.Full;
|
||||
|
||||
return defaultTheme;
|
||||
}
|
||||
|
||||
export function googleFontUrl(fontFamily: string, weights: number[] = [300, 400, 700]): string {
|
||||
|
|
@ -112,5 +123,3 @@ export function googleFontUrl(fontFamily: string, weights: number[] = [300, 400,
|
|||
const urlFont = fontName.split(/ /).join('+');
|
||||
return `https://fonts.googleapis.com/css?family=${urlFont}:${urlWeights}&display=swap`;
|
||||
}
|
||||
|
||||
export { theme as defaultTheme } from '@chakra-ui/react';
|
||||
|
|
|
|||
3771
hyperglass/ui/yarn.lock
vendored
3771
hyperglass/ui/yarn.lock
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
"""Test file-related utilities."""
|
||||
|
||||
# Standard Library
|
||||
import random
|
||||
import string
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
# Third Party
|
||||
|
|
@ -17,6 +17,12 @@ KEY3=VALUE3
|
|||
"""
|
||||
|
||||
|
||||
def _random_string(length: int) -> str:
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
result = "".join(secrets.choice(alphabet) for i in range(length))
|
||||
return result
|
||||
|
||||
|
||||
def test_dotenv_to_dict_string():
|
||||
result = dotenv_to_dict(ENV_TEST)
|
||||
assert result.get("KEY1") == "VALUE1"
|
||||
|
|
@ -94,9 +100,7 @@ def test_check_path_raises(tmp_path_factory: pytest.TempPathFactory):
|
|||
async def test_move_files(tmp_path_factory: pytest.TempPathFactory):
|
||||
src = tmp_path_factory.mktemp("src")
|
||||
dst = tmp_path_factory.mktemp("dst")
|
||||
filenames = (
|
||||
"".join(random.choice(string.ascii_lowercase) for _ in range(8)) for _ in range(10)
|
||||
)
|
||||
filenames = ("".join(_random_string(8)) for _ in range(10))
|
||||
files = [src / name for name in filenames]
|
||||
[f.touch() for f in files]
|
||||
result = await move_files(src, dst, files)
|
||||
|
|
@ -109,9 +113,7 @@ async def test_move_files(tmp_path_factory: pytest.TempPathFactory):
|
|||
async def test_move_files_raise(tmp_path_factory: pytest.TempPathFactory):
|
||||
src = tmp_path_factory.mktemp("src")
|
||||
dst = tmp_path_factory.mktemp("dst")
|
||||
filenames = (
|
||||
"".join(random.choice(string.ascii_lowercase) for _ in range(8)) for _ in range(10)
|
||||
)
|
||||
filenames = ("".join(_random_string(8)) for _ in range(10))
|
||||
files = [src / name for name in filenames]
|
||||
with pytest.raises(RuntimeError):
|
||||
await move_files(src, dst, files)
|
||||
|
|
@ -120,9 +122,7 @@ async def test_move_files_raise(tmp_path_factory: pytest.TempPathFactory):
|
|||
def test_copyfiles(tmp_path_factory: pytest.TempPathFactory):
|
||||
src = tmp_path_factory.mktemp("src")
|
||||
dst = tmp_path_factory.mktemp("dst")
|
||||
filenames = [
|
||||
"".join(random.choice(string.ascii_lowercase) for _ in range(8)) for _ in range(10)
|
||||
]
|
||||
filenames = ["".join(_random_string(8)) for _ in range(10)]
|
||||
src_files = [src / name for name in filenames]
|
||||
dst_files = [dst / name for name in filenames]
|
||||
[f.touch() for f in src_files]
|
||||
|
|
@ -133,9 +133,7 @@ def test_copyfiles(tmp_path_factory: pytest.TempPathFactory):
|
|||
def test_copyfiles_wrong_length(tmp_path_factory: pytest.TempPathFactory):
|
||||
src = tmp_path_factory.mktemp("src")
|
||||
dst = tmp_path_factory.mktemp("dst")
|
||||
filenames = [
|
||||
"".join(random.choice(string.ascii_lowercase) for _ in range(8)) for _ in range(10)
|
||||
]
|
||||
filenames = ["".join(_random_string(8)) for _ in range(10)]
|
||||
dst_filenames = filenames[1:8]
|
||||
src_files = [src / name for name in filenames]
|
||||
dst_files = [dst / name for name in dst_filenames]
|
||||
|
|
|
|||
2421
poetry.lock
generated
2421
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -63,7 +63,7 @@ pre-commit = "^2.20.0"
|
|||
pytest = "^7.2.0"
|
||||
pytest-asyncio = "^0.20.3"
|
||||
pytest-dependency = "^0.5.1"
|
||||
ruff = "v0.0.194"
|
||||
ruff = "^0.0.261"
|
||||
stackprinter = "^0.2.10"
|
||||
taskipy = "^1.10.3"
|
||||
|
||||
|
|
@ -128,6 +128,7 @@ ignore = [
|
|||
"E731",
|
||||
"D203", # Blank line before docstring.
|
||||
"D213", # Multiline docstring summary on second line.
|
||||
"D107", # Don't require docstrings for __init__ functions.
|
||||
"D402",
|
||||
"D406",
|
||||
"D407",
|
||||
|
|
@ -137,10 +138,14 @@ ignore = [
|
|||
"N818", # Error suffix on custom exceptions.
|
||||
"RET501", # Explicitly return None
|
||||
"B905", # zip without `strict`
|
||||
"W293", # blank line contains whitespace
|
||||
]
|
||||
line-length = 100
|
||||
select = ["B", "C", "D", "E", "F", "I", "N", "S", "RET", "W"]
|
||||
|
||||
[tool.ruff.pydocstyle]
|
||||
convention = "pep257"
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
max-complexity = 10
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue