From 19bdfe74d02d23ed99b6de1d734c584ac254d391 Mon Sep 17 00:00:00 2001 From: thatmattlove <matt@stunninglyclear.com> Date: Sun, 24 Mar 2024 16:50:31 -0400 Subject: [PATCH] fix PeeringDB link rendering --- hyperglass/models/config/params.py | 14 ++++- hyperglass/ui/components/footer/footer.tsx | 63 +++++++++------------- hyperglass/ui/types/globals.d.ts | 3 ++ 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/hyperglass/models/config/params.py b/hyperglass/models/config/params.py index 4375ce76..6d67ae85 100644 --- a/hyperglass/models/config/params.py +++ b/hyperglass/models/config/params.py @@ -3,9 +3,10 @@ # Standard Library import typing as t from pathlib import Path +import urllib.parse # Third Party -from pydantic import Field, ConfigDict, ValidationInfo, field_validator +from pydantic import Field, ConfigDict, ValidationInfo, field_validator, HttpUrl # Project from hyperglass.settings import Settings @@ -108,6 +109,17 @@ def validate_plugins(cls: "Params", value: t.List[str]) -> t.List[str]: return [str(f) for f in matching_plugins] return [] + @field_validator("web", mode="after") + @classmethod + def validate_web(cls, web: Web, info: ValidationInfo) -> Web: + """String-format Link URLs.""" + for link in web.links: + url = urllib.parse.unquote(str(link.url), encoding="utf-8", errors="replace").format( + primary_asn=info.data.get("primary_asn", "65000") + ) + link.url = HttpUrl(url) + return web + def common_plugins(self) -> t.Tuple[Path, ...]: """Get all validated external common plugins as Path objects.""" return tuple(Path(p) for p in self.plugins) diff --git a/hyperglass/ui/components/footer/footer.tsx b/hyperglass/ui/components/footer/footer.tsx index 006aa048..17d3fea4 100644 --- a/hyperglass/ui/components/footer/footer.tsx +++ b/hyperglass/ui/components/footer/footer.tsx @@ -2,7 +2,7 @@ import { Flex, HStack, useToken } from '@chakra-ui/react'; import { useMemo } from 'react'; import { useConfig } from '~/context'; import { DynamicIcon } from '~/elements'; -import { useBreakpointValue, useColorValue, useMobile, useStrf } from '~/hooks'; +import { useBreakpointValue, useColorValue, useMobile } from '~/hooks'; import { isLink, isMenu } from '~/types'; import { FooterButton } from './button'; import { ColorModeToggle } from './color-mode'; @@ -11,7 +11,9 @@ import { FooterLink } from './link'; import type { ButtonProps, LinkProps } from '@chakra-ui/react'; import type { Link, Menu } from '~/types'; -function buildItems(links: Link[], menus: Menu[]): [(Link | Menu)[], (Link | Menu)[]] { +type MenuItems = (Link | Menu)[]; + +function buildItems(links: Link[], menus: Menu[]): [MenuItems, MenuItems] { const leftLinks = links.filter(link => link.side === 'left'); const leftMenus = menus.filter(menu => menu.side === 'left'); const rightLinks = links.filter(link => link.side === 'right'); @@ -22,8 +24,23 @@ function buildItems(links: Link[], menus: Menu[]): [(Link | Menu)[], (Link | Men return [left, right]; } +const LinkOnSide = (props: { item: ArrayElement<MenuItems>; side: 'left' | 'right' }) => { + const { item, side } = props; + if (isLink(item)) { + const icon: Partial<ButtonProps & LinkProps> = {}; + + if (item.showIcon) { + icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />; + } + return <FooterLink key={item.title} href={item.url} title={item.title} {...icon} />; + } + if (isMenu(item)) { + return <FooterButton key={item.title} side={side} content={item.content} title={item.title} />; + } +}; + export const Footer = (): JSX.Element => { - const { web, content, primaryAsn } = useConfig(); + const { web, content } = useConfig(); const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100'); const footerColor = useColorValue('black', 'white'); @@ -34,8 +51,6 @@ export const Footer = (): JSX.Element => { const [left, right] = useMemo(() => buildItems(web.links, web.menus), [web.links, web.menus]); - const strF = useStrf(); - return ( <HStack px={6} @@ -51,39 +66,13 @@ export const Footer = (): JSX.Element => { overflowY={{ base: 'auto', lg: 'unset' }} justifyContent={{ base: 'center', lg: 'space-between' }} > - {left.map(item => { - if (isLink(item)) { - const url = strF(item.url, { primaryAsn }, '/'); - const icon: Partial<ButtonProps & LinkProps> = {}; - - if (item.showIcon) { - icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />; - } - return <FooterLink key={item.title} href={url} title={item.title} {...icon} />; - } - if (isMenu(item)) { - return ( - <FooterButton key={item.title} side="left" content={item.content} title={item.title} /> - ); - } - })} + {left.map(item => ( + <LinkOnSide key={item.title} item={item} side="left" /> + ))} {!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />} - {right.map(item => { - if (isLink(item)) { - const url = strF(item.url, { primaryAsn }, '/'); - const icon: Partial<ButtonProps & LinkProps> = {}; - - if (item.showIcon) { - icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />; - } - return <FooterLink key={item.title} href={url} title={item.title} {...icon} />; - } - if (isMenu(item)) { - return ( - <FooterButton key={item.title} side="right" content={item.content} title={item.title} /> - ); - } - })} + {right.map(item => ( + <LinkOnSide key={item.title} item={item} side="right" /> + ))} {web.credit.enable && ( <FooterButton key="credit" diff --git a/hyperglass/ui/types/globals.d.ts b/hyperglass/ui/types/globals.d.ts index 4f4acee8..f920e599 100644 --- a/hyperglass/ui/types/globals.d.ts +++ b/hyperglass/ui/types/globals.d.ts @@ -9,6 +9,9 @@ export declare global { type Swap<T, K extends keyof T, V> = Record<K, V> & Omit<T, K>; + type ArrayElement<ArrayType extends readonly unknown[]> = + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; + type RPKIState = 0 | 1 | 2 | 3; type ResponseLevel = 'success' | 'warning' | 'error' | 'danger';