lookingglass/hyperglass/models/config/web.py
2024-03-17 16:04:41 -04:00

246 lines
7.5 KiB
Python

"""Validate branding configuration variables."""
# Standard Library
import typing as t
from pathlib import Path
# Third Party
from pydantic import HttpUrl, FilePath, ValidationInfo, constr, field_validator, model_validator
from pydantic_extra_types.color import Color
# Project
from hyperglass.defaults import DEFAULT_HELP, DEFAULT_TERMS
from hyperglass.constants import DNS_OVER_HTTPS, FUNC_COLOR_MAP
# Local
from ..main import HyperglassModel
from .opengraph import OpenGraph
DEFAULT_IMAGES = Path(__file__).parent.parent.parent / "images"
Percentage = constr(pattern=r"^([1-9][0-9]?|100)\%$")
TitleMode = t.Literal["logo_only", "text_only", "logo_subtitle", "all"]
ColorMode = t.Literal["light", "dark"]
DOHProvider = constr(pattern="|".join(DNS_OVER_HTTPS.keys()))
Title = constr(max_length=32)
Side = t.Literal["left", "right"]
LocationDisplayMode = t.Literal["auto", "dropdown", "gallery"]
class Credit(HyperglassModel):
"""Validation model for developer credit."""
enable: bool = True
class Link(HyperglassModel):
"""Validation model for generic link."""
title: str
url: HttpUrl
show_icon: bool = True
side: Side = "left"
order: int = 0
class Menu(HyperglassModel):
"""Validation model for generic menu."""
title: str
content: str
side: Side = "left"
order: int = 0
@field_validator("content")
def validate_content(cls: "Menu", value: str) -> str:
"""Read content from file if a path is provided."""
if len(value) < 260:
path = Path(value)
if path.is_file() and path.exists():
with path.open("r") as f:
return f.read()
return value
class Greeting(HyperglassModel):
"""Validation model for greeting modal."""
enable: bool = False
file: t.Optional[FilePath] = None
title: str = "Welcome"
button: str = "Continue"
required: bool = False
@field_validator("file")
def validate_file(cls, value: str, info: ValidationInfo):
"""Ensure file is specified if greeting is enabled."""
if info.data.get("enable") and value is None:
raise ValueError("Greeting is enabled, but no file is specified.")
return value
class Logo(HyperglassModel):
"""Validation model for logo configuration."""
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
width: t.Optional[t.Union[int, Percentage]] = "100%"
height: t.Optional[t.Union[int, Percentage]] = None
class LogoPublic(Logo):
"""Public logo configuration."""
light_format: str
dark_format: str
class Text(HyperglassModel):
"""Validation model for params.branding.text."""
title_mode: TitleMode = "logo_only"
title: Title = "hyperglass"
subtitle: Title = "Network Looking Glass"
query_location: str = "Location"
query_type: str = "Query Type"
query_target: str = "Target"
fqdn_tooltip: str = "Use {protocol}" # Formatted by Javascript
fqdn_message: str = "Your browser has resolved {fqdn} to" # Formatted by Javascript
fqdn_error: str = "Unable to resolve {fqdn}" # Formatted by Javascript
fqdn_error_button: str = "Try Again"
cache_prefix: str = "Results cached for "
cache_icon: str = "Cached from {time} UTC" # Formatted by Javascript
complete_time: str = "Completed in {seconds}" # Formatted by Javascript
rpki_invalid: str = "Invalid"
rpki_valid: str = "Valid"
rpki_unknown: str = "No ROAs Exist"
rpki_unverified: str = "Not Verified"
no_communities: str = "No Communities"
ip_error: str = "Unable to determine IP Address"
no_ip: str = "No {protocol} Address"
ip_select: str = "Select an IP Address"
ip_button: str = "My IP"
@field_validator("cache_prefix")
def validate_cache_prefix(cls: "Text", value: str) -> str:
"""Ensure trailing whitespace."""
return " ".join(value.split()) + " "
class ThemeColors(HyperglassModel):
"""Validation model for theme colors."""
black: Color = "#000000"
white: Color = "#ffffff"
dark: Color = "#010101"
light: Color = "#f5f6f7"
gray: Color = "#c1c7cc"
red: Color = "#d84b4b"
orange: Color = "#ff6b35"
yellow: Color = "#edae49"
green: Color = "#35b246"
blue: Color = "#314cb6"
teal: Color = "#35b299"
cyan: Color = "#118ab2"
pink: Color = "#f2607d"
purple: Color = "#8d30b5"
primary: t.Optional[Color] = None
secondary: t.Optional[Color] = None
success: t.Optional[Color] = None
warning: t.Optional[Color] = None
error: t.Optional[Color] = None
danger: t.Optional[Color] = None
@field_validator(*FUNC_COLOR_MAP.keys(), mode="before")
def validate_colors(cls: "ThemeColors", value: str, info: ValidationInfo) -> str:
"""Set default functional color mapping."""
if value is None:
default_color = FUNC_COLOR_MAP[info.field_name]
value = str(info.data[default_color])
return value
def dict(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, str]:
"""Return dict for colors only."""
return {k: v.as_hex() for k, v in self.__dict__.items()}
class ThemeFonts(HyperglassModel):
"""Validation model for theme fonts."""
body: str = "Nunito"
mono: str = "Fira Code"
class Theme(HyperglassModel):
"""Validation model for theme variables."""
colors: ThemeColors = ThemeColors()
default_color_mode: t.Optional[ColorMode] = None
fonts: ThemeFonts = ThemeFonts()
class DnsOverHttps(HyperglassModel):
"""Validation model for DNS over HTTPS resolution."""
name: DOHProvider = "cloudflare"
url: str = ""
@model_validator(mode="before")
def validate_dns(cls, data: "DnsOverHttps") -> t.Dict[str, str]:
"""Assign url field to model based on selected provider."""
name = data.get("name", "cloudflare")
url = DNS_OVER_HTTPS[name]
return {
"name": name,
"url": url,
}
class HighlightPattern(HyperglassModel):
"""Validation model for highlight pattern configuration."""
pattern: str
label: t.Optional[str] = None
color: str = "primary"
@field_validator("color")
def validate_color(cls: "HighlightPattern", value: str) -> str:
"""Ensure highlight color is a valid theme color."""
colors = list(ThemeColors.model_fields.keys())
color_list = "\n - ".join(("", *colors))
if value not in colors:
raise ValueError(
"{!r} is not a supported color. Must be one of:{!s}".format(value, color_list)
)
return value
class Web(HyperglassModel):
"""Validation model for all web/browser-related configuration."""
credit: Credit = Credit()
dns_provider: DnsOverHttps = DnsOverHttps()
links: t.Sequence[Link] = [
Link(title="PeeringDB", url="https://www.peeringdb.com/asn/{primary_asn}")
]
menus: t.Sequence[Menu] = [
Menu(title="Terms", content=DEFAULT_TERMS),
Menu(title="Help", content=DEFAULT_HELP),
]
greeting: Greeting = Greeting()
logo: Logo = Logo()
opengraph: OpenGraph = OpenGraph()
text: Text = Text()
theme: Theme = Theme()
location_display_mode: LocationDisplayMode = "auto"
custom_javascript: t.Optional[FilePath] = None
custom_html: t.Optional[FilePath] = None
highlight: t.List[HighlightPattern] = []
class WebPublic(Web):
"""Public web configuration."""
logo: LogoPublic