mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
Closes #140: Genericize footer links and menus and allow multiple definitions
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
# Standard Library
|
||||
import os
|
||||
import copy
|
||||
import json
|
||||
from typing import Dict, List
|
||||
from pathlib import Path
|
||||
@@ -18,6 +17,7 @@ from hyperglass.log import (
|
||||
enable_syslog_logging,
|
||||
)
|
||||
from hyperglass.util import set_app_path, set_cache_env, current_log_level
|
||||
from hyperglass.defaults import CREDIT, DEFAULT_DETAILS
|
||||
from hyperglass.constants import (
|
||||
SUPPORTED_QUERY_TYPES,
|
||||
PARSED_RESPONSE_FIELDS,
|
||||
@@ -28,12 +28,6 @@ from hyperglass.util.files import check_path
|
||||
from hyperglass.models.commands import Commands
|
||||
from hyperglass.models.config.params import Params
|
||||
from hyperglass.models.config.devices import Devices
|
||||
from hyperglass.configuration.defaults import (
|
||||
CREDIT,
|
||||
DEFAULT_HELP,
|
||||
DEFAULT_TERMS,
|
||||
DEFAULT_DETAILS,
|
||||
)
|
||||
|
||||
# Local
|
||||
from .markdown import get_markdown
|
||||
@@ -332,17 +326,6 @@ content_greeting = get_markdown(
|
||||
|
||||
content_vrf = _build_vrf_help()
|
||||
|
||||
content_help_params = copy.copy(content_params)
|
||||
content_help_params["title"] = params.web.help_menu.title
|
||||
content_help = get_markdown(
|
||||
config_path=params.web.help_menu, default=DEFAULT_HELP, params=content_help_params
|
||||
)
|
||||
|
||||
content_terms_params = copy.copy(content_params)
|
||||
content_terms_params["title"] = params.web.terms.title
|
||||
content_terms = get_markdown(
|
||||
config_path=params.web.terms, default=DEFAULT_TERMS, params=content_terms_params
|
||||
)
|
||||
content_credit = CREDIT.format(version=__version__)
|
||||
|
||||
networks = _build_networks()
|
||||
@@ -374,8 +357,6 @@ _frontend_params.update(
|
||||
"networks": networks,
|
||||
"parsed_data_fields": PARSED_RESPONSE_FIELDS,
|
||||
"content": {
|
||||
"help_menu": content_help,
|
||||
"terms": content_terms,
|
||||
"credit": content_credit,
|
||||
"vrf": content_vrf,
|
||||
"greeting": content_greeting,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Validate branding configuration variables."""
|
||||
|
||||
# Standard Library
|
||||
from typing import Union, Optional
|
||||
from typing import Union, Optional, Sequence
|
||||
from pathlib import Path
|
||||
|
||||
# Third Party
|
||||
@@ -18,6 +18,7 @@ from pydantic import (
|
||||
from pydantic.color import Color
|
||||
|
||||
# Project
|
||||
from hyperglass.defaults import DEFAULT_HELP, DEFAULT_TERMS
|
||||
from hyperglass.constants import DNS_OVER_HTTPS, FUNC_COLOR_MAP
|
||||
|
||||
# Local
|
||||
@@ -31,6 +32,7 @@ TitleMode = constr(regex=("logo_only|text_only|logo_title|logo_subtitle|all"))
|
||||
ColorMode = constr(regex=r"light|dark")
|
||||
DOHProvider = constr(regex="|".join(DNS_OVER_HTTPS.keys()))
|
||||
Title = constr(max_length=32)
|
||||
Side = constr(regex=r"left|right")
|
||||
|
||||
|
||||
class Analytics(HyperglassModel):
|
||||
@@ -64,20 +66,36 @@ class Credit(HyperglassModel):
|
||||
enable: StrictBool = True
|
||||
|
||||
|
||||
class ExternalLink(HyperglassModel):
|
||||
"""Validation model for external link."""
|
||||
class Link(HyperglassModel):
|
||||
"""Validation model for generic link."""
|
||||
|
||||
enable: StrictBool = True
|
||||
title: StrictStr = "PeeringDB"
|
||||
url: HttpUrl = "https://www.peeringdb.com/asn/{primary_asn}"
|
||||
title: StrictStr
|
||||
url: HttpUrl
|
||||
show_icon: StrictBool = True
|
||||
side: Side = "left"
|
||||
order: StrictInt = 0
|
||||
|
||||
|
||||
class HelpMenu(HyperglassModel):
|
||||
"""Validation model for generic help menu."""
|
||||
class Menu(HyperglassModel):
|
||||
"""Validation model for generic menu."""
|
||||
|
||||
enable: StrictBool = True
|
||||
file: Optional[FilePath]
|
||||
title: StrictStr = "Help"
|
||||
title: StrictStr
|
||||
content: StrictStr
|
||||
side: Side = "left"
|
||||
order: StrictInt = 0
|
||||
|
||||
@validator("content")
|
||||
def validate_content(cls, value):
|
||||
"""Read content from file if a path is provided."""
|
||||
|
||||
if len(value) < 260:
|
||||
path = Path(value)
|
||||
if path.exists():
|
||||
with path.open("r") as f:
|
||||
return f.read()
|
||||
else:
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
class Greeting(HyperglassModel):
|
||||
@@ -107,14 +125,6 @@ class Logo(HyperglassModel):
|
||||
height: Optional[Union[StrictInt, Percentage]]
|
||||
|
||||
|
||||
class Terms(HyperglassModel):
|
||||
"""Validation model for terms & conditions."""
|
||||
|
||||
enable: StrictBool = True
|
||||
file: Optional[FilePath]
|
||||
title: StrictStr = "Terms"
|
||||
|
||||
|
||||
class Text(HyperglassModel):
|
||||
"""Validation model for params.branding.text."""
|
||||
|
||||
@@ -236,11 +246,15 @@ class Web(HyperglassModel):
|
||||
|
||||
credit: Credit = Credit()
|
||||
dns_provider: DnsOverHttps = DnsOverHttps()
|
||||
external_link: ExternalLink = ExternalLink()
|
||||
links: Sequence[Link] = [
|
||||
Link(title="PeeringDB", url="https://www.peeringdb.com/asn/{primary_asn}")
|
||||
]
|
||||
menus: Sequence[Menu] = [
|
||||
Menu(title="Terms", content=DEFAULT_TERMS),
|
||||
Menu(title="Help", content=DEFAULT_HELP),
|
||||
]
|
||||
greeting: Greeting = Greeting()
|
||||
help_menu: HelpMenu = HelpMenu()
|
||||
logo: Logo = Logo()
|
||||
opengraph: OpenGraph = OpenGraph()
|
||||
terms: Terms = Terms()
|
||||
text: Text = Text()
|
||||
theme: Theme = Theme()
|
||||
|
@@ -1,16 +1,37 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Menu, MenuButton, MenuList } from '@chakra-ui/react';
|
||||
import { Markdown } from '~/components';
|
||||
import { useColorValue, useBreakpointValue } from '~/context';
|
||||
import { useOpposingColor } from '~/hooks';
|
||||
import { useColorValue, useBreakpointValue, useConfig } from '~/context';
|
||||
import { useOpposingColor, useStrf } from '~/hooks';
|
||||
|
||||
import type { IConfig } from '~/types';
|
||||
import type { TFooterButton } from './types';
|
||||
|
||||
/**
|
||||
* Filter the configuration object based on values that are strings for formatting.
|
||||
*/
|
||||
function getConfigFmt(config: IConfig): Record<string, string> {
|
||||
const fmt = {} as Record<string, string>;
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
if (typeof v === 'string') {
|
||||
fmt[k] = v;
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
|
||||
export const FooterButton: React.FC<TFooterButton> = (props: TFooterButton) => {
|
||||
const { content, title, side, ...rest } = props;
|
||||
|
||||
const config = useConfig();
|
||||
const fmt = useMemo(() => getConfigFmt(config), []);
|
||||
const fmtContent = useStrf(content, fmt);
|
||||
|
||||
const placement = side === 'left' ? 'top' : side === 'right' ? 'top-end' : undefined;
|
||||
const bg = useColorValue('white', 'gray.900');
|
||||
const color = useOpposingColor(bg);
|
||||
const size = useBreakpointValue({ base: 'xs', lg: 'sm' });
|
||||
|
||||
return (
|
||||
<Menu placement={placement} preventOverflow isLazy>
|
||||
<MenuButton
|
||||
@@ -32,11 +53,12 @@ export const FooterButton: React.FC<TFooterButton> = (props: TFooterButton) => {
|
||||
boxShadow="2xl"
|
||||
textAlign="left"
|
||||
overflowY="auto"
|
||||
whiteSpace="normal"
|
||||
mx={{ base: 1, lg: 2 }}
|
||||
maxW={{ base: '100%', lg: '50vw' }}
|
||||
{...rest}
|
||||
>
|
||||
<Markdown content={content} />
|
||||
<Markdown content={fmtContent} />
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
|
@@ -1,27 +1,43 @@
|
||||
import { useMemo } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Button, Flex, Link, Icon, HStack, useToken } from '@chakra-ui/react';
|
||||
import { Flex, Icon, HStack, useToken } from '@chakra-ui/react';
|
||||
import { If } from '~/components';
|
||||
import { useConfig, useMobile, useColorValue, useBreakpointValue } from '~/context';
|
||||
import { useStrf } from '~/hooks';
|
||||
import { FooterButton } from './button';
|
||||
import { ColorModeToggle } from './colorMode';
|
||||
import { FooterLink } from './link';
|
||||
import { isLink, isMenu } from './types';
|
||||
|
||||
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
|
||||
import type { TLink, TMenu } from '~/types';
|
||||
|
||||
const CodeIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCode));
|
||||
const ExtIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/go').then(i => i.GoLinkExternal));
|
||||
|
||||
function buildItems(links: TLink[], menus: TMenu[]): [(TLink | TMenu)[], (TLink | TMenu)[]] {
|
||||
const leftLinks = links.filter(link => link.side === 'left');
|
||||
const leftMenus = menus.filter(menu => menu.side === 'left');
|
||||
const rightLinks = links.filter(link => link.side === 'right');
|
||||
const rightMenus = menus.filter(menu => menu.side === 'right');
|
||||
|
||||
const left = [...leftLinks, ...leftMenus].sort((a, b) => (a.order > b.order ? 1 : -1));
|
||||
const right = [...rightLinks, ...rightMenus].sort((a, b) => (a.order > b.order ? 1 : -1));
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const { web, content, primary_asn } = useConfig();
|
||||
|
||||
const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
|
||||
const footerColor = useColorValue('black', 'white');
|
||||
|
||||
const extUrl = useStrf(web.external_link.url, { primary_asn }) ?? '/';
|
||||
|
||||
const size = useBreakpointValue({ base: useToken('sizes', 4), lg: useToken('sizes', 6) });
|
||||
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
|
||||
|
||||
const isMobile = useMobile();
|
||||
|
||||
const [left, right] = useMemo(() => buildItems(web.links, web.menus), []);
|
||||
|
||||
return (
|
||||
<HStack
|
||||
px={6}
|
||||
@@ -30,30 +46,42 @@ export const Footer: React.FC = () => {
|
||||
zIndex={1}
|
||||
as="footer"
|
||||
bg={footerBg}
|
||||
whiteSpace="nowrap"
|
||||
color={footerColor}
|
||||
spacing={{ base: 8, lg: 6 }}
|
||||
d={{ base: 'inline-block', lg: 'flex' }}
|
||||
overflowY={{ base: 'auto', lg: 'unset' }}
|
||||
justifyContent={{ base: 'center', lg: 'space-between' }}
|
||||
>
|
||||
<If c={web.terms.enable}>
|
||||
<FooterButton side="left" content={content.terms} title={web.terms.title} />
|
||||
</If>
|
||||
<If c={web.help_menu.enable}>
|
||||
<FooterButton side="left" content={content.help_menu} title={web.help_menu.title} />
|
||||
</If>
|
||||
<If c={web.external_link.enable}>
|
||||
<Button
|
||||
as={Link}
|
||||
isExternal
|
||||
href={extUrl}
|
||||
size={btnSize}
|
||||
variant="ghost"
|
||||
rightIcon={<ExtIcon />}
|
||||
aria-label={web.external_link.title}
|
||||
>
|
||||
{web.external_link.title}
|
||||
</Button>
|
||||
</If>
|
||||
{left.map(item => {
|
||||
if (isLink(item)) {
|
||||
const url = useStrf(item.url, { primary_asn }) ?? '/';
|
||||
const icon: Partial<ButtonProps & LinkProps> = {};
|
||||
|
||||
if (item.show_icon) {
|
||||
icon.rightIcon = <ExtIcon />;
|
||||
}
|
||||
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
|
||||
} else if (isMenu(item)) {
|
||||
return (
|
||||
<FooterButton key={item.title} side="left" content={item.content} title={item.title} />
|
||||
);
|
||||
}
|
||||
})}
|
||||
{!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />}
|
||||
{right.map(item => {
|
||||
if (isLink(item)) {
|
||||
const url = useStrf(item.url, { primary_asn }) ?? '/';
|
||||
const icon: Partial<ButtonProps & LinkProps> = {};
|
||||
|
||||
if (item.show_icon) {
|
||||
icon.rightIcon = <ExtIcon />;
|
||||
}
|
||||
return <FooterLink href={url} title={item.title} {...icon} />;
|
||||
} else if (isMenu(item)) {
|
||||
return <FooterButton side="right" content={item.content} title={item.title} />;
|
||||
}
|
||||
})}
|
||||
<If c={web.credit.enable}>
|
||||
<FooterButton
|
||||
side="right"
|
||||
|
13
hyperglass/ui/components/footer/link.tsx
Normal file
13
hyperglass/ui/components/footer/link.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Button, Link, useBreakpointValue } from '@chakra-ui/react';
|
||||
|
||||
import type { TFooterLink } from './types';
|
||||
|
||||
export const FooterLink: React.FC<TFooterLink> = (props: TFooterLink) => {
|
||||
const { title } = props;
|
||||
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
|
||||
return (
|
||||
<Button as={Link} isExternal size={btnSize} variant="ghost" aria-label={title} {...props}>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
};
|
@@ -1,4 +1,5 @@
|
||||
import type { ButtonProps, MenuListProps } from '@chakra-ui/react';
|
||||
import type { ButtonProps, LinkProps, MenuListProps } from '@chakra-ui/react';
|
||||
import type { TLink, TMenu } from '~/types';
|
||||
|
||||
type TFooterSide = 'left' | 'right';
|
||||
|
||||
@@ -8,8 +9,18 @@ export interface TFooterButton extends Omit<MenuListProps, 'title'> {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type TFooterLink = ButtonProps & LinkProps & { title: string };
|
||||
|
||||
export type TFooterItems = 'help' | 'credit' | 'terms';
|
||||
|
||||
export interface TColorModeToggle extends ButtonProps {
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export function isLink(item: TLink | TMenu): item is TLink {
|
||||
return 'url' in item;
|
||||
}
|
||||
|
||||
export function isMenu(item: TLink | TMenu): item is TMenu {
|
||||
return 'content' in item;
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@ import type { Theme } from './theme';
|
||||
|
||||
export type TQueryFields = 'query_type' | 'query_target' | 'query_location' | 'query_vrf';
|
||||
|
||||
type TSide = 'left' | 'right';
|
||||
|
||||
export interface IConfigMessages {
|
||||
no_input: string;
|
||||
acl_denied: string;
|
||||
@@ -62,10 +64,26 @@ export interface TConfigWebLogo {
|
||||
dark_format: string;
|
||||
}
|
||||
|
||||
export interface TLink {
|
||||
title: string;
|
||||
url: string;
|
||||
show_icon: boolean;
|
||||
side: TSide;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface TMenu {
|
||||
title: string;
|
||||
content: string;
|
||||
side: TSide;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface IConfigWeb {
|
||||
credit: { enable: boolean };
|
||||
dns_provider: { name: string; url: string };
|
||||
external_link: { enable: boolean; title: string; url: string };
|
||||
links: TLink[];
|
||||
menus: TMenu[];
|
||||
greeting: TConfigGreeting;
|
||||
help_menu: { enable: boolean; title: string };
|
||||
logo: TConfigWebLogo;
|
||||
@@ -149,8 +167,6 @@ export interface TQueryContent {
|
||||
}
|
||||
|
||||
export interface IConfigContent {
|
||||
help_menu: string;
|
||||
terms: string;
|
||||
credit: string;
|
||||
greeting: string;
|
||||
vrf: {
|
||||
|
Reference in New Issue
Block a user