Skip to content

Commit

Permalink
Closes #140: Genericize footer links and menus and allow multiple def…
Browse files Browse the repository at this point in the history
…initions
  • Loading branch information
thatmattlove committed May 30, 2021
1 parent 69f21a4 commit c0914f6
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 72 deletions.
21 changes: 1 addition & 20 deletions hyperglass/configuration/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

# Standard Library
import os
import copy
import json
from typing import Dict, List
from pathlib import Path
Expand All @@ -18,6 +17,7 @@
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,
Expand All @@ -28,12 +28,6 @@
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
Expand Down Expand Up @@ -332,17 +326,6 @@ def _build_vrf_help() -> Dict:

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()
Expand Down Expand Up @@ -374,8 +357,6 @@ def _build_vrf_help() -> Dict:
"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,
Expand Down
File renamed without changes.
58 changes: 36 additions & 22 deletions hyperglass/models/config/web.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,6 +18,7 @@
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
Expand All @@ -31,6 +32,7 @@
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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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()
28 changes: 25 additions & 3 deletions hyperglass/ui/components/footer/button.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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>
);
Expand Down
74 changes: 51 additions & 23 deletions hyperglass/ui/components/footer/footer.tsx
Original file line number Diff line number Diff line change
@@ -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}
Expand All @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions hyperglass/ui/components/footer/link.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
13 changes: 12 additions & 1 deletion hyperglass/ui/components/footer/types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
}
Loading

0 comments on commit c0914f6

Please sign in to comment.